Zum Inhalt springen
Zurück zu Insights
Architektur9 Min. Lesezeit

Multi-Tenant-Isolation: Warum Row-Level Security nicht ausreicht

Vier Schichten der Mandantenisolation vom API-Gateway bis zur Datenbank. Defense in Depth für Finanzinfrastruktur.

Multi-Tenant-Isolation in Finanzsystemen: Row-Level Security reicht nicht aus


Ein Entwickler vergisst eine WHERE-Klausel. Das ORM generiert eine ungescoppte Abfrage. Ein Admin-Endpunkt umgeht den Mandantenfilter aus „operativer Bequemlichkeit". Ein Bug, eine Abfrage, und Mandant A sieht die Transaktionshistorie von Mandant B.

In einem SaaS-Produkt ist das ein Datenleck. In einem Finanzsystem ist es zusätzlich ein regulatorischer Verstoß. PSD2 fordert den Schutz von Zahlungsdaten. DSGVO verhängt Bußgelder pro betroffener Person. DORA fordert, dass IKT-Systeme unautorisierten Zugriff auf Finanzdaten verhindern. Ein einziges mandantenübergreifendes Datenleck löst alle drei aus.

Row-Level Security (RLS) soll dies verhindern. Die Datenbank erzwingt Mandantengrenzen unabhängig davon, was der Anwendungscode tut. Aber RLS ist die letzte Verteidigungslinie, nicht die einzige. Sie beantwortet „kann diese Datenbanksitzung auf diese Zeile zugreifen?", aber jemand muss den Sitzungskontext korrekt setzen. Jemand muss sicherstellen, dass die durch das System fließende Mandanten-ID authentisch ist, nicht vom Aufrufer geliefert.

Multi-Tenant-Isolation in Finanzsystemen erfordert drei Schichten. Jede Schicht fängt Fehler ab, die die anderen übersehen.

Schicht 1: Das Gateway

Das API-Gateway sitzt zwischen dem öffentlichen Internet und der Anwendung. Seine Aufgabe: den Aufrufer authentifizieren, bestimmen, zu welchem Mandanten er gehört, und diese Identität als vertrauenswürdigen Header injizieren.

Die kritische Eigenschaft: Die Anwendung liest die Mandantenidentität niemals aus dem Request Body des Aufrufers, Query-Parametern oder JWT Claims, die der Aufrufer kontrolliert. Das Gateway validiert das OAuth2-Token (oder den API-Key), löst den zugehörigen Mandanten auf und setzt einen HTTP-Header, X-Tenant-ID, auf dem internen Request. Die Anwendung liest den Header. Der Aufrufer kann ihn nicht fälschen.

Dies verhindert eine Angriffskategorie, die anwendungsseitige Filterung nicht kann: ein kompromittierter oder böswilliger Aufrufer, der ein gültiges Authentifizierungstoken sendet, aber den Mandantenkontext manipuliert. Wenn die Anwendung tenant_id aus dem Request Body liest, kann ein Aufrufer mit gültigen Credentials vorgeben, jeder beliebige Mandant zu sein. Wenn die Anwendung X-Tenant-ID aus einem gateway-injizierten Header liest, wird der Anspruch verifiziert, bevor der Request den Anwendungscode erreicht.

Implementierung: Traefik ForwardAuth-Middleware, Kong-Plugins oder ein Custom-Gateway, das einen Auth-Service aufruft. Der Auth-Service validiert das Token, löst den Mandanten auf und gibt die Mandanten-ID als Response-Header zurück. Das Gateway injiziert sie. Die Anwendung vertraut ihr.

Schicht 2: Die Anwendung

Jede Service-Methode empfängt den Mandantenkontext aus dem Gateway-Header. Der Kontext ist für die Dauer des Requests immutable. Der Service kann keine Abfrage für einen anderen Mandanten konstruieren, nicht weil es per Konvention verboten ist, sondern weil die API keine Mandanten-ID als Parameter akzeptiert. Sie liest sie aus dem injizierten Header.

Dies eliminiert das WHERE-Klausel-Problem. Die Anwendung fügt nicht manuell WHERE tenant_id = ? zu jeder Abfrage hinzu. Der Mandantenkontext wird auf der Datenbankverbindung zu Beginn des Requests gesetzt, und RLS (Schicht 3) erzwingt ihn transparent.

Aber die Anwendungsschicht fügt etwas hinzu, das die Datenbank nicht kann: request-scoped Validierung. Vor jeder Schreiboperation verifiziert der Service, dass die zu modifizierenden Ressourcen zum aktuellen Mandanten gehören. Eine Überweisung von Konto A zu Konto B? Beide Konten müssen dem anfragenden Mandanten gehören. Eine Kundenaktualisierung? Der Kunde muss dem anfragenden Mandanten gehören. Diese Prüfungen geschehen im Anwendungscode, bevor die Datenbank berührt wird.

Warum nicht allein auf RLS vertrauen? Weil RLS auf Zeilenebene operiert. Es kann das Lesen von Zeilen anderer Mandanten verhindern. Es kann keine semantisch ungültigen Operationen innerhalb einer einzelnen Abfrage verhindern, zum Beispiel eine Überweisung, bei der Belastungs- und Gutschriftskonto zu verschiedenen Mandanten gehören. Diese Validierung erfordert Anwendungslogik.

Schicht 3: Die Datenbank

PostgreSQL Row-Level Security Policies auf jeder Tabelle mit Mandantendaten. Die Policy:

CREATE POLICY tenant_isolation ON finance_transfer
  USING (tenant_id = current_setting('app.current_tenant_id')::uuid);

Die Anwendung setzt app.current_tenant_id zu Beginn jeder Datenbankverbindung (aus dem gateway-injizierten Header). Jede Abfrage wird automatisch gefiltert. Eine vergessene WHERE-Klausel ist irrelevant, die Datenbank gibt unabhängig von der Abfrage keine Zeilen anderer Mandanten zurück.

Das Schlüsseldetail: Die von der Anwendung verwendete Datenbankrolle kann RLS nicht umgehen. Nur die Migrationsrolle (für Schemaänderungen, nie für Anwendungsabfragen) hat BYPASSRLS-Berechtigung. Wenn die Anwendungsrolle RLS umgehen könnte, würde ein einziger SET ROLE-Bug oder eine SQL-Injection das gesamte Isolationsmodell aushebeln.

-- Anwendungsrolle: RLS erzwungen
CREATE ROLE finance_api NOINHERIT NOBYPASSRLS;

-- Migrationsrolle: RLS umgangen (nur Schemaänderungen, nie von Anwendung verwendet)
CREATE ROLE finance_admin BYPASSRLS;

Schicht 4: Das Ledger

Finanzsysteme haben ein Anliegen, das generische SaaS-Plattformen nicht haben: Das Ledger muss Mandantenisolation unabhängig von der relationalen Datenbank erzwingen.

Die Ledger-Engine operiert auf Fixed-Size Records mit einem user_data_128-Feld, das kodierte Mandanteninformation trägt. Eine Überweisung zwischen zwei Konten ist nur gültig, wenn beide Konten dieselbe Mandantenkodierung teilen. Das Ledger lehnt mandantenübergreifende Überweisungen auf Protokollebene ab, bevor die Überweisung den Speicher erreicht.

Das ist nicht redundant. Es ist Defense in Depth. Betrachten Sie den Fehlermodus: Ein Bug in der Anwendungsschicht konstruiert eine Überweisung, bei der das Belastungskonto Mandant A gehört und das Gutschriftskonto Mandant B. Die Prüfung der Anwendungsschicht (Schicht 2) sollte das auffangen. Aber wenn nicht, eine fehlende Validierung, eine Race Condition, ein Codepfad eines neuen Entwicklers, der die Prüfung nicht kannte, lehnt die Ledger-Engine die Überweisung ab. Die Finanzdaten werden nie korrumpiert.

AnwendungBug: transfer(debit=MandantA:konto1, credit=MandantB:konto2)
Schicht 2Anwendungsprüfung: sollte auffangenÜbersehen (Bug)
Schicht 3Datenbank RLS: beide Konten für MandantA sichtbar?Blockiert
Schicht 4Ledger-Engine: Konten haben unterschiedliche MandantenkodierungAbgelehnt

Zwei unabhängige Sicherheitsnetze hinter der Anwendung. Jedes einzelne reicht aus, um die Korruption zu verhindern. Beide zusammen machen sie strukturell unmöglich.

Workflow-Propagation

Mehrstufige Workflows umspannen mehrere Services. Kontoerstellung ruft den Finance-Service auf, dann den IBAN-Service, dann den KYC-Service. Jeder Aufruf muss den korrekten Mandantenkontext tragen.

Die Durable-Execution-Engine propagiert die Mandanten-ID in jedem Workflow-Aufruf. Wenn ein Workflow den Finance-Service aufruft, injiziert er X-Tenant-ID im HTTP-Request-Header. Der Finance-Service validiert ihn gegen den gateway-injizierten Wert. Wenn ein Workflow-Schritt einen externen Provider aufruft (KYC, AML-Screening), wird der Mandantenkontext verwendet, um die korrekte Provider-Konfiguration auszuwählen, ohne die Mandanten-ID dem externen System preiszugeben.

Wenn die Workflow-Engine den Mandantenkontext nicht propagiert, können Cross-Service-Aufrufe im falschen Mandanten-Scope ausgeführt werden. Ein häufiger Bug, kein hypothetischer, in Multi-Tenant-Systemen, die Workflow-Orchestrierung nachträglich hinzufügen.

Fünf Fragen zur Bewertung Ihrer Isolation

Binäre Antworten. Kein Teilkredit.

1. Kann ein Aufrufer seine eigene Mandantenidentität setzen? Wenn die Anwendung tenant_id aus dem Request Body oder einem vom Aufrufer kontrollierten JWT Claim liest: ja. Ihre Gateway-Schicht fehlt oder ist unvollständig.

2. Kann Anwendungscode eine Abfrage für einen anderen Mandanten konstruieren? Wenn irgendein Codepfad tenant_id als Funktionsparameter akzeptiert, anstatt ihn aus dem Request-Kontext zu lesen: ja. Ihre Anwendungsschicht hat eine Lücke.

3. Hat Ihre Datenbankrolle Berechtigungen, RLS zu umgehen? Wenn die Anwendung sich mit einer Rolle verbindet, die BYPASSRLS, SUPERUSER hat oder die Tabellen besitzt: ja. Ihre Datenbankschicht ist kaputt. RLS wird erzwungen, ist aber bedeutungslos.

4. Kann eine Überweisung Gelder zwischen Konten verschiedener Mandanten bewegen? Wenn das Ledger nicht unabhängig die Mandantenzugehörigkeit beider Konten in einer Überweisung verifiziert: ja. Ihre Ledger-Schicht fehlt.

5. Propagiert Ihre Workflow-Engine den Mandantenkontext über Service-Grenzen? Wenn Cross-Service-Aufrufe innerhalb eines Workflows keinen Mandantenkontext tragen: nein. Ihre mehrstufigen Prozesse könnten Scope leaken.

Jedes „Ja" bei Fragen 1-4 und „Nein" bei Frage 5 ist eine Lücke. In einem Finanzsystem ist jede Lücke ein potenzieller regulatorischer Befund.


Weiterlesen: Das Ledger | Sicherheit & Compliance


Quellen:

  • DORA, Verordnung (EU) 2022/2554, Art. 9 (Schutz und Prävention unautorisierten Zugriffs)
  • PSD2, Richtlinie 2015/2366, Art. 94 (Schutz personenbezogener Daten)
  • DSGVO, Verordnung 2016/679, Art. 32 (Sicherheit der Verarbeitung)
  • PostgreSQL Dokumentation: Row Security Policies (https://www.postgresql.org/docs/current/ddl-rowsecurity.html)