Zum Inhalt

Datenbank-Sicherheit: RBAC, Least Privilege & Row-Level Security (RLS)

Erstellt: 2026-11-08  ·  Zuletzt aktualisiert: 2026-11-08  ·  Nejat Hakan

Management Summary: Webanwendungen mit dem Superuser postgres oder root an Produktionsdatenbanken anzubinden, ist ein gravierendes Architektur-Antipattern, das fundamentale Compliance- und Defense-in-Depth-Prinzipien verletzt. Eine einzige SQL-Injection-Lücke oder fehlerhafte ORM-Abfrage unter Superuser-Rechten ermöglicht Angreifern das Auslesen privater Schlüssel, das Löschen ganzer Tabellen oder gar die Ausführung beliebiger Betriebssystem-Befehle via COPY ... PROGRAM. Dieser Leitfaden demonstriert die Implementierung eines Drei-Stufen-RBAC-Modells (Role-Based Access Control), die Konfiguration automatischer Default-Schema-Privilegien und die Durchsetzung kompromissloser Row-Level Security (RLS) zur physikalischen Mandantenisolierung direkt im Datenbankkern.

  • Drei-Stufen-RBAC-Architektur


    Durchsetzung des Least-Privilege-Prinzips: Trennung von Migrator-Rollen (nur DDL), Runtime-Rollen (nur DML) und Read-Only-Rollen (BI-Analysen).

  • Default-Schema-Privilegien


    Keine manuelle Rechtepflege bei Schema-Updates: Durch ALTER DEFAULT PRIVILEGES erben neu angelegte Tabellen automatisch sichere Berechtigungen.

  • Row-Level Security (RLS)


    Schutz vor Mandanten-Datenlecks: Abfragen scannen physikalisch nur Zeilen, die zur authentifizierten tenant_id der Session gehören.

  • Session-Kontext & Testing


    Sichere Übergabe des Anwendungskontexts im Connection-Pool via SET LOCAL app.current_tenant_id und Verifikation der Policies via SET ROLE.


1. Das Drei-Stufen-RBAC-Modell (Role-Based Access Control)

Eine gehärtete Produktionsdatenbank unterteilt Zugriffe in drei getrennte Rollenebenen:

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    SUPER["PostgreSQL-Superuser<br/>(Notfall-Wartung)"]

    subgraph Tier1 ["1. Schema Owner / Migrator (DDL)"]
        direction TB
        MIG["Rolle app_migrator (NOLOGIN)<br/>Besitzt Tabellen, Migrationen"]
    end

    subgraph Tier2 ["2. Applikations-Laufzeit (DML)"]
        direction TB
        RUN["Rolle app_runtime (LOGIN)<br/>SELECT, INSERT, UPDATE, DELETE<br/>Kein DROP, TRUNCATE, ALTER!"]
    end

    subgraph Tier3 ["3. Read-Only Analytik (DQL)"]
        direction TB
        READ["Rolle app_readonly (LOGIN)<br/>Nur SELECT auf Schemas<br/>Für Grafana, BI-Exporte"]
    end

    SUPER -.->|"Richtet ein"| Tier1
    Tier1 ==> Tier2 ==> Tier3

Berechtigungsmatrix nach Stufen

Rollen-Stufe Erlaubte Befehle Verbotene Befehle Typischer Einsatz
Stufe 1: Migrator CREATE, ALTER, DROP, TRUNCATE Direkte Abfragen aus der Web-Laufzeit CI/CD-Pipelines (Flyway, Liquibase, Alembic).
Stufe 2: Runtime SELECT, INSERT, UPDATE, DELETE CREATE TABLE, DROP TABLE, TRUNCATE Backend-Microservices, Webanwendungen, Worker.
Stufe 3: Read-Only SELECT INSERT, UPDATE, DELETE, sämtliches DDL BI-Reporting-Tools, Audit-Dashboards, Analysten.

2. Implementierung der RBAC-Hierarchie in PostgreSQL

Führen Sie dieses administrative Skript als Administrator postgres aus, um die Rollenhierarchie zu etablieren und Default-Privilegien einzurichten:

-- Datei: /opt/db-ops/setup_tiered_rbac.sql
BEGIN;

-- 1. Stufe 1 anlegen: Schema-Migrations-Rolle (Besitzer der Datenbankobjekte)
CREATE ROLE app_migrator WITH NOLOGIN;

-- 2. Stufe 2 anlegen: Applikations-Laufzeit-Rolle (DML)
CREATE ROLE role_runtime WITH NOLOGIN;
CREATE USER app_runtime_user WITH LOGIN PASSWORD 'Runtime_Secret_2026!';
GRANT role_runtime TO app_runtime_user;

-- 3. Stufe 3 anlegen: Read-Only Reporting-Rolle (Analytik)
CREATE ROLE role_readonly WITH NOLOGIN;
CREATE USER app_bi_user WITH LOGIN PASSWORD 'Reporting_Secret_2026!';
GRANT role_readonly TO app_bi_user;

-- 4. Verbindungsrechte auf die Produktionsdatenbank vergeben
GRANT CONNECT ON DATABASE enterprise_prod TO role_runtime, role_readonly;

-- 5. Schema-Nutzungsrechte vergeben (public Schema)
GRANT USAGE ON SCHEMA public TO role_runtime, role_readonly;

-- 6. DML-Rechte an Runtime-Rolle vergeben, SELECT an Reporting
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO role_runtime;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO role_runtime;

GRANT SELECT ON ALL TABLES IN SCHEMA public TO role_readonly;

-- 7. ESSENZIELL: Default-Privilegien für künftige Tabellen
-- Werden durch app_migrator im Rahmen von CI/CD neue Tabellen angelegt, greifen diese Rechte automatisch:
ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA public
    GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO role_runtime;

ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA public
    GRANT USAGE, SELECT ON SEQUENCES TO role_runtime;

ALTER DEFAULT PRIVILEGES FOR ROLE app_migrator IN SCHEMA public
    GRANT SELECT ON TABLES TO role_readonly;

COMMIT;

3. Mandanten-Isolation mit Row-Level Security (RLS)

In Mandanten-fähigen SaaS-Plattformen ist das Speichern aller Kundendaten in gemeinsamen Tabellen (Shared Database / Shared Schema) ressourceneffizient. Sich jedoch darauf zu verlassen, dass Entwickler in jedem ORM-Aufruf oder SQL-Statement diszipliniert WHERE tenant_id = :id ergänzen, führt unweigerlich zu fatalen Datenpannen.

Row-Level Security (RLS) verlagert die Mandantentrennung direkt in den Datenbankkern: PostgreSQL injiziert automatisch und manipulationssicher ein Sicherheitsprädikat in jedes SELECT, UPDATE und DELETE, das über die Session abgesetzt wird.

%%{init: {'flowchart': {'wrappingWidth': 350}}}%%
flowchart TB
    APP["Client Web-Request<br/>(Mandant 42)"]
    POOL["Connection-Pooler<br/>(Setzt Session-Variable)"]
    SQL["Eingehendes SQL:<br/>SELECT * FROM invoices"]
    RLS{"PostgreSQL RLS Engine<br/>Injiziert Policy-Prädikat"}
    DATA["Physikalischer Heap<br/>Rechnungen: Mandant 10, 42, 99"]
    RES["Ergebnis: Strikt nur Mandant 42!<br/>Andere physikalisch unsichtbar"]

    APP --> POOL
    POOL -->|"SET LOCAL tenant_id = 42"| SQL
    SQL --> RLS
    RLS --> DATA
    DATA --> RES

Produktions-Workflow für RLS

-- Datei: /opt/db-ops/configure_row_level_security.sql
BEGIN;

-- 1. Mandantenfähige Rechnungstabelle anlegen
CREATE TABLE customer_invoices (
    invoice_id   BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    tenant_id    BIGINT NOT NULL,
    amount_cents BIGINT NOT NULL CHECK (amount_cents >= 0),
    issued_at    TIMESTAMPTZ NOT NULL DEFAULT clock_timestamp()
);

-- Indexierung auf tenant_id (KRITISCH für die RLS-Abfrageperformance!)
CREATE INDEX idx_invoices_tenant_id ON customer_invoices (tenant_id);

-- 2. Row-Level Security auf der Tabelle aktivieren
ALTER TABLE customer_invoices ENABLE ROW LEVEL SECURITY;

-- 3. RLS auch für den Tabellen-Owner erzwingen (verhindert versehentliches Bypassen)
ALTER TABLE customer_invoices FORCE ROW LEVEL SECURITY;

-- 4. Sicherheitsrichtlinie definieren, basierend auf einer Session-Konfigurationsvariable
CREATE POLICY tenant_isolation_policy ON customer_invoices
    AS PERMISSIVE
    FOR ALL
    TO role_runtime
    USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::bigint)
    WITH CHECK (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::bigint);

COMMIT;

4. Testen der RLS-Isolation & Kontextübergabe

In einem Connection-Pool (z. B. PgBouncer oder Applikations-Pool) werden physikalische Verbindungen zwischen Mandanten geteilt. Bei jedem Request setzt die Anwendung die Mandanten-ID transaktionslokal via SET LOCAL:

-- Datei: /opt/db-ops/test_rls_simulation.sql

-- Testdaten als Administrator für mehrere Mandanten einfügen
INSERT INTO customer_invoices (tenant_id, amount_cents) VALUES
    (101, 15000),
    (101, 35000),
    (202, 99000);

-- Ausführungskontext auf die unprivilegierte Applikations-Rolle umschalten
SET ROLE app_runtime_user;

-- Transaktion A: Authentifiziert als Mandant 101
BEGIN;
SET LOCAL app.current_tenant_id = '101';

-- Die SQL-Abfrage enthält KEIN WHERE tenant_id, RLS filtert automatisch!
SELECT invoice_id, tenant_id, amount_cents FROM customer_invoices;
-- Rückgabe: Ausschließlich die zwei Zeilen von Mandant 101! Mandant 202 bleibt unsichtbar!

-- Ein Insert-Versuch mit fremder Mandanten-ID wird durch WITH CHECK abgewiesen:
-- INSERT INTO customer_invoices (tenant_id, amount_cents) VALUES (202, 5000);
-- Bricht ab mit: ERROR: new row violates row-level security policy for table "customer_invoices"

COMMIT;

-- Rolle wieder auf Administrator zurücksetzen
RESET ROLE;