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
postgresoderrootan 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 viaCOPY ... 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 PRIVILEGESerben neu angelegte Tabellen automatisch sichere Berechtigungen. -
Row-Level Security (RLS)
Schutz vor Mandanten-Datenlecks: Abfragen scannen physikalisch nur Zeilen, die zur authentifizierten
tenant_idder Session gehören. -
Session-Kontext & Testing
Sichere Übergabe des Anwendungskontexts im Connection-Pool via
SET LOCAL app.current_tenant_idund Verifikation der Policies viaSET 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;