Zugriffskontrolle direkt auf Zeilenebene
Row Level Security verlagert Zugriffsregeln aus der Anwendungsschicht in die Datenbank selbst und filtert jede Zeile automatisch nach dem aktuellen Sicherheitskontext. Wer Multi-Tenancy oder mandantenfeindliche Anwendungslogik ohne Row Level Security absichert, verlaesst sich auf disziplinierte WHERE-Klauseln in jedem einzelnen Query, und genau das bricht frueher oder spaeter.
Inhaltsverzeichnis
- 1. Was Row Level Security wirklich loest
- 2. Row Level Security in PostgreSQL: Policies und Predicates
- 3. Row Level Security ohne natives Feature: View-basierte Muster
- 4. Multi-Tenancy mit Row Level Security absichern
- 5. Session-Kontext: wie die Datenbank weiss wer fragt
- 6. Performance-Auswirkungen von Row Level Security
- 7. Row Level Security testen und verifizieren
- 8. Typische Fehler bei Row Level Security
- 9. Row Level Security im Vergleich der Ansaetze
- 10. Zusammenfassung
- 11. FAQ
1. Was Row Level Security wirklich loest
Row Level Security beantwortet eine einfache, aber oft falsch geloeste Frage: Darf dieser Datenbank-Benutzer genau diese Zeile lesen oder schreiben? Ohne Row Level Security landet diese Pruefung typischerweise in der Anwendungsschicht, verteilt ueber Dutzende Controller, Repositories und Services. Jede neue Query braucht eine korrekte WHERE-Klausel mit Mandanten-ID oder Benutzer-ID, und jede vergessene Klausel oeffnet ein Datenleck zwischen Mandanten oder Benutzern.
Row Level Security verschiebt diese Regel dorthin, wo sie nicht umgangen werden kann: in die Datenbank-Engine selbst. Eine einmal definierte Policy gilt fuer jede Abfrage, jeden Report, jedes Ad-hoc-Skript und jeden neuen Entwickler, der eine Tabelle direkt abfragt. Das ist der entscheidende Unterschied zu anwendungsseitigen Filtern: Row Level Security kann nicht durch einen vergessenen WHERE-Zusatz umgangen werden, weil die Engine selbst jede Zeile vor der Rueckgabe prueft.
Besonders relevant wird Row Level Security in drei Szenarien: Multi-Tenant-SaaS-Anwendungen, in denen Mandanten strikt getrennte Daten in derselben Tabelle teilen, regulierte Branchen mit gesetzlicher Pflicht zur Datentrennung, und interne Systeme, in denen unterschiedliche Rollen unterschiedliche Sichten auf dieselbe Tabelle brauchen. In allen drei Faellen ersetzt eine zentrale Policy hunderte verstreute Anwendungspruefungen.
2. Row Level Security in PostgreSQL: Policies und Predicates
PostgreSQL bietet natives Row Level Security seit Version 9.5 ueber CREATE POLICY. Eine Policy definiert einen booleschen Ausdruck, der fuer jede Zeile ausgewertet wird, bevor sie sichtbar wird. Erst muss Row Level Security fuer die Tabelle aktiviert werden mit ALTER TABLE ... ENABLE ROW LEVEL SECURITY, danach greifen die definierten Policies fuer alle Benutzer ausser dem Tabellen-Owner und Superuser, die standardmaessig von Policies ausgenommen sind.
Eine Policy kann fuer SELECT, INSERT, UPDATE und DELETE getrennt definiert werden, mit unterschiedlichen Ausdruecken fuer USING (welche existierenden Zeilen sichtbar sind) und WITH CHECK (welche neuen oder geaenderten Zeilen erlaubt sind). Diese Trennung ist entscheidend: Ein Benutzer darf zum Beispiel eigene Zeilen lesen, aber keine neuen Zeilen mit fremder Mandanten-ID anlegen, selbst wenn er versehentlich die falsche ID mitgibt.
-- Enable Row Level Security on the table
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY;
-- Policy for SELECT/UPDATE/DELETE: only rows of the caller's tenant
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.current_tenant')::int);
-- Separate WITH CHECK: prevents inserting rows for another tenant
CREATE POLICY tenant_isolation_insert ON invoices
FOR INSERT
WITH CHECK (tenant_id = current_setting('app.current_tenant')::int);
-- Optional: bypass policy for a maintenance role
ALTER TABLE invoices FORCE ROW LEVEL SECURITY;
CREATE POLICY admin_full_access ON invoices
TO db_admin_role
USING (true);
Wichtig ist FORCE ROW LEVEL SECURITY: Ohne diese Anweisung ignoriert der Tabellen-Owner alle Policies standardmaessig, was in der Praxis oft zu falscher Sicherheit fuehrt, wenn Anwendungen versehentlich mit dem Owner-Account verbinden. Mit aktivem Row Level Security und korrektem Rollenmodell wird die Mandantentrennung zu einer Eigenschaft der Datenbank, nicht des Anwendungscodes.
3. Row Level Security ohne natives Feature: View-basierte Muster
Nicht jede Datenbank bietet natives Row Level Security. MySQL etwa hat kein eingebautes Policy-System, aber das Konzept laesst sich mit einer Kombination aus Views und Session-Variablen nachbauen. Die Grundidee bleibt identisch: eine Zwischenschicht filtert Zeilen basierend auf dem aktuellen Kontext, bevor Anwendungscode die Daten sieht, und diese Schicht liegt in der Datenbank statt im Code.
Der Nachteil dieses Ersatzes fuer natives Row Level Security: Anwendungscode muss diszipliniert ausschliesslich ueber die View zugreifen, niemals direkt auf die Basistabelle. Ein Datenbank-Grant, der direkten Zugriff auf die Basistabelle verweigert und nur die View erlaubt, macht diese Regel technisch durchsetzbar statt einer reinen Konvention.
-- MySQL has no native Row Level Security, emulate it with a view
-- Session variable set per connection by the application layer
SET @current_tenant = 42;
CREATE VIEW invoices_scoped AS
SELECT * FROM invoices
WHERE tenant_id = @current_tenant;
-- Revoke direct table access, force usage of the scoped view
REVOKE SELECT ON invoices FROM app_user;
GRANT SELECT ON invoices_scoped TO app_user;
-- Application always queries the view, never the base table
SELECT id, amount, status FROM invoices_scoped
WHERE status = 'open';
Diese View-basierte Alternative erreicht nicht ganz dieselbe Robustheit wie natives Row Level Security, weil Session-Variablen manipuliert werden koennen, wenn Anwendungscode ungeprueften Input direkt in SET-Anweisungen einbaut. Deshalb muss die Zuweisung der Session-Variable ausschliesslich aus einer verifizierten, serverseitigen Quelle stammen, niemals aus einem Client-Parameter.
4. Multi-Tenancy mit Row Level Security absichern
Multi-Tenant-Architekturen mit gemeinsamer Datenbank und gemeinsamem Schema sind der klassische Anwendungsfall fuer Row Level Security. Statt fuer jeden Mandanten ein eigenes Schema oder eine eigene Datenbank zu betreiben, teilen sich alle Mandanten dieselben Tabellen, unterschieden nur durch eine tenant_id-Spalte. Diese Architektur ist deutlich guenstiger im Betrieb, birgt aber genau das Risiko, das Row Level Security adressiert: eine vergessene Filterbedingung zeigt Daten des falschen Mandanten.
In der Praxis kombiniert man Row Level Security mit einem Verbindungspool, der pro Request die richtige tenant_id als Session-Variable setzt, bevor irgendeine Query ausgefuehrt wird. Middleware im Anwendungscode extrahiert die Mandanten-ID aus dem authentifizierten Request und setzt sie zu Beginn jeder Transaktion. Danach braucht kein einziges Repository mehr eine explizite Mandanten-Filterung, weil Row Level Security das transparent uebernimmt.
Ein haeufig uebersehener Punkt: Auch Hintergrundjobs, Cronjobs und Reporting-Skripte muessen denselben Session-Kontext setzen, sonst sehen sie entweder gar keine Zeilen (wenn die Variable fehlt und die Policy restriktiv ist) oder, schlimmer, alle Zeilen aller Mandanten (wenn ein Superuser-Account ohne FORCE ROW LEVEL SECURITY verwendet wird).
5. Session-Kontext: wie die Datenbank weiss wer fragt
Damit Row Level Security ueberhaupt entscheiden kann, welche Zeilen sichtbar sind, braucht die Datenbank Informationen ueber den aktuellen Aufrufer. In PostgreSQL uebernimmt current_setting() in Kombination mit SET oder SET LOCAL diese Aufgabe. SET LOCAL ist fuer Row Level Security in Verbindungspools besonders wichtig, weil der Wert nur innerhalb der aktuellen Transaktion gilt und beim Commit oder Rollback automatisch zurueckgesetzt wird, statt in der gepoolten Verbindung fuer die naechste Anfrage haengen zu bleiben.
Ein zweiter Ansatz nutzt die Datenbank-Rolle selbst als Kontext, ohne Session-Variablen: Jeder Mandant oder jede Benutzergruppe bekommt eine eigene Datenbank-Rolle, und Policies pruefen current_user oder Rollenmitgliedschaft direkt. Dieser Ansatz skaliert schlechter bei tausenden Mandanten, weil jede Rolle einzeln verwaltet werden muss, ist aber robuster gegen versehentlich vergessenes Setzen einer Session-Variable, weil die Rolle bereits bei der Verbindung feststeht.
-- SET LOCAL scopes the variable to the current transaction only
BEGIN;
SET LOCAL app.current_tenant = '42';
SET LOCAL app.current_role = 'accountant';
-- Row Level Security policies read both settings
SELECT * FROM invoices; -- automatically scoped
COMMIT;
-- After commit, the pooled connection has no leftover tenant context
-- Policy combining tenant scope with role-based visibility
CREATE POLICY role_scoped_invoices ON invoices
USING (
tenant_id = current_setting('app.current_tenant')::int
AND (
current_setting('app.current_role') = 'admin'
OR status != 'confidential'
)
);
6. Performance-Auswirkungen von Row Level Security
Ein berechtigter Einwand gegen Row Level Security: Jede Query wird um zusaetzliche Bedingungen erweitert, was Auswirkungen auf den Ausfuehrungsplan haben kann. In der Praxis ist der Overhead gering, solange die Spalten, auf denen Policies pruefen, korrekt indiziert sind. Eine Policy, die auf tenant_id filtert, profitiert direkt von einem Index auf tenant_id, genau wie eine manuell geschriebene WHERE-Klausel.
Ein subtileres Performance-Problem entsteht, wenn Policies komplexe Subqueries oder Funktionsaufrufe enthalten, die pro Zeile ausgewertet werden. PostgreSQL versucht, Policy-Ausdruecke in den Query-Plan zu integrieren und mit anderen Filtern zu kombinieren, aber teure Funktionen in einer Policy koennen diese Optimierung verhindern. Der Rat aus der Praxis: Policies so einfach wie moeglich halten, idealerweise als direkter Spaltenvergleich, und komplexere Logik in eine indizierte Hilfsspalte auslagern statt in eine Laufzeitfunktion.
Ein Vergleich von EXPLAIN-Plaenen mit und ohne aktiviertem Row Level Security zeigt in gut indizierten Schemas meist nur marginale Unterschiede in der Ausfuehrungszeit. Der eigentliche Kostenfaktor ist selten Row Level Security selbst, sondern fehlende Indizes auf den Spalten, die in Policies referenziert werden, ein Fehler, der auch ohne Row Level Security die gleiche Query verlangsamen wuerde.
7. Row Level Security testen und verifizieren
Policies fuer Row Level Security muessen genauso getestet werden wie Anwendungslogik, denn eine fehlerhafte Policy ist ein direktes Datenleck. Der Testansatz simuliert unterschiedliche Session-Kontexte innerhalb derselben Testsuite: Fuer jeden Mandanten oder jede Rolle wird eine Transaktion mit dem entsprechenden Kontext geoeffnet, eine Abfrage ausgefuehrt, und das Ergebnis gegen die erwartete Zeilenmenge geprueft.
Ein wichtiger Testfall fuer Row Level Security, der oft vergessen wird: der negative Test. Es reicht nicht zu pruefen, dass Mandant A seine eigenen Zeilen sieht, sondern explizit zu verifizieren, dass Mandant A die Zeilen von Mandant B unter keinen Umstaenden sieht, auch nicht ueber JOINs mit anderen Tabellen, Aggregatfunktionen oder Subqueries, die Zeilen indirekt referenzieren.
-- Test harness pattern: verify Row Level Security isolation
-- Run as tenant 42, expect only tenant 42 rows
BEGIN;
SET LOCAL app.current_tenant = '42';
SELECT count(*) FROM invoices WHERE tenant_id != 42;
-- Expected result: 0, Row Level Security must hide foreign rows
ROLLBACK;
-- Negative test: attempt cross-tenant insert should fail
BEGIN;
SET LOCAL app.current_tenant = '42';
INSERT INTO invoices (tenant_id, amount) VALUES (99, 100.00);
-- Expected: error, WITH CHECK policy rejects mismatched tenant_id
ROLLBACK;
-- Aggregation must still respect Row Level Security
BEGIN;
SET LOCAL app.current_tenant = '42';
SELECT sum(amount) FROM invoices;
-- Must equal manual sum WHERE tenant_id = 42, nothing more
ROLLBACK;
8. Typische Fehler bei Row Level Security
Der haeufigste Fehler bei Row Level Security: Vergessen von FORCE ROW LEVEL SECURITY, wodurch der Tabellen-Owner alle Policies umgeht. Wenn Anwendungscode versehentlich mit dem Owner-Account statt einer eingeschraenkten Rolle verbindet, was in Entwicklungsumgebungen oft passiert, greifen die Policies gar nicht, und der Fehler faellt erst in Produktion auf, wenn ein Datenleck bereits entstanden ist.
Ein zweiter haeufiger Fehler: Policies nur fuer SELECT definieren, aber WITH CHECK fuer INSERT und UPDATE vergessen. Ohne WITH CHECK kann ein Benutzer zwar keine fremden Zeilen lesen, aber durchaus neue Zeilen mit einer fremden tenant_id anlegen, weil die Schreibrichtung ungeprueft bleibt. Ein dritter Fehler ist das Setzen von Session-Variablen mit SET statt SET LOCAL in gepoolten Verbindungen, wodurch der Mandanten-Kontext einer Anfrage bei der naechsten Anfrage auf derselben Connection haengen bleibt.
9. Row Level Security im Vergleich der Ansaetze
Es gibt mehrere Wege, Zugriff auf Zeilenebene zu steuern, mit deutlichen Unterschieden bei Sicherheit, Wartbarkeit und Datenbank-Unterstuetzung. Die Wahl des richtigen Ansatzes fuer Row Level Security haengt stark von der eingesetzten Datenbank und der Anzahl der Mandanten ab.
| Ansatz | Unsicher / Fragil | Empfohlenes Muster | Vorteil |
|---|---|---|---|
| Mandanten-Filter | WHERE in jedem Query manuell | Native Row Level Security Policy | Kann nicht vergessen werden |
| Ohne natives Feature | Direkter Tabellenzugriff | View plus REVOKE auf Basistabelle | Erzwungene Zwischenschicht |
| Session-Kontext | SET auf gepoolter Connection | SET LOCAL je Transaktion | Kein Leck zwischen Requests |
| Owner-Zugriff | Policies ohne FORCE ignoriert | FORCE ROW LEVEL SECURITY | Gilt auch fuer Owner-Rolle |
| Schreibrichtung | Nur USING definiert | USING plus WITH CHECK | Verhindert falsche Inserts |
In gut indizierten Schemas mit einer Handvoll Mandanten bis zu mehreren tausend Mandanten liefert native Row Level Security die robusteste Loesung, weil sie unabhaengig vom Anwendungscode arbeitet. Fuer Datenbanken ohne natives Feature bleibt die View-basierte Variante die praktikabelste Alternative, solange direkter Tabellenzugriff konsequent per Grant verweigert wird.
Mironsoft
Datenbank-Sicherheit, Zugriffskontrolle und Multi-Tenant-Architektur
Row Level Security fuer eure Multi-Tenant-Datenbank?
Wir pruefen bestehende Schemas auf verstreute Mandanten-Filter, entwerfen Row Level Security Policies und richten Session-Kontext, Grants und Tests fuer eine belastbare Zugriffskontrolle ein.
Policy-Design
Row Level Security Policies fuer Mandanten- und Rollentrennung entwerfen
Migration
Verstreute Mandanten-Filter im Code durch zentrale Policies ersetzen
Audit & Tests
Negativ-Tests fuer Mandantentrennung und laufendes Access-Audit aufbauen
10. Zusammenfassung
Row Level Security verschiebt Zugriffskontrolle auf Zeilenebene aus verstreutem Anwendungscode in eine zentrale, von der Datenbank erzwungene Regel. In PostgreSQL entsteht Row Level Security ueber CREATE POLICY mit getrennten USING- und WITH CHECK-Ausdruecken, aktiviert durch ENABLE ROW LEVEL SECURITY und, fuer den Owner, durch FORCE ROW LEVEL SECURITY. Datenbanken ohne natives Feature koennen dasselbe Konzept ueber Views mit Session-Variablen und konsequenten Grants nachbilden.
Der groesste Gewinn von Row Level Security zeigt sich in Multi-Tenant-Systemen: eine einzige, testbare Policy ersetzt hunderte verstreute WHERE-Klauseln und schuetzt auch Hintergrundjobs, Reports und zukuenftigen Code, der die Regel sonst neu implementieren muesste. Performance-Overhead bleibt gering, solange die referenzierten Spalten indiziert sind, und negative Tests gegen Cross-Tenant-Zugriff sind Pflicht, nicht optional.
Row Level Security: Das Wichtigste auf einen Blick
Aktivierung
ENABLE ROW LEVEL SECURITY plus FORCE, damit auch der Tabellen-Owner den Policies unterliegt.
USING vs. WITH CHECK
USING filtert sichtbare Zeilen, WITH CHECK schuetzt vor falschen Inserts und Updates. Beide Richtungen definieren.
Session-Kontext
SET LOCAL statt SET in gepoolten Verbindungen, damit kein Mandanten-Kontext zwischen Requests haengen bleibt.
Ohne natives Feature
View plus Session-Variable plus REVOKE auf Basistabelle als praktikable Alternative fuer MySQL und aehnliche Systeme.