Row Level Security: Konzepte und Umsetzung in der Praxis
AI generated
SELECT
JOIN
SQL · Zugriffskontrolle · Datenbank Sicherheit
Row Level Security: Konzepte und Umsetzung
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.

18 Min. Lesezeit Row Level Security · Policies · Multi-Tenancy · Session-Kontext PostgreSQL · MySQL · Datenbank-agnostisch

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.

11. FAQ: Row Level Security

1Was ist Row Level Security genau?
Ein Datenbank-Feature, das jede Zeile gegen eine Policy prueft, bevor sie sichtbar oder schreibbar wird, unabhaengig vom Anwendungscode.
2Welche Datenbanken unterstuetzen es nativ?
PostgreSQL seit 9.5 ueber CREATE POLICY, ebenso Oracle und SQL Server. MySQL hat kein natives Row Level Security.
3Warum reicht WHERE-Filterung nicht?
Manuelle Filter muessen ueberall wiederholt werden. Eine vergessene Klausel ist ein Datenleck. Policies erzwingen die Regel zentral.
4Was macht FORCE ROW LEVEL SECURITY?
Erzwingt Policies auch fuer den Tabellen-Owner, der sie sonst standardmaessig umgeht.
5USING vs. WITH CHECK?
USING steuert Sichtbarkeit bestehender Zeilen, WITH CHECK steuert erlaubte neue oder geaenderte Zeilen. Beide getrennt definieren.
6Ohne native Unterstuetzung umsetzen?
View mit Session-Variable plus REVOKE auf die Basistabelle, damit nur die View erreichbar ist.
7Performance-Auswirkung?
Bei Indizes auf referenzierten Spalten meist marginal. Teure Funktionen in Policies vermeiden.
8SET LOCAL statt SET?
SET LOCAL gilt nur je Transaktion, verhindert dass Mandanten-Kontext in gepoolten Verbindungen haengen bleibt.
9Wie zuverlaessig testen?
Positive und negative Tests, insbesondere pruefen dass fremde Zeilen auch ueber Aggregation oder JOIN nicht sichtbar sind.
10Ersetzt es Anwendungs-Autorisierung?
Nein, es ergaenzt sie als letzte, unumgehbare Verteidigungslinie auf Zeilenebene.