OpexGuard

Technische Dokumentation

Mandantentrennung: Row-Level Security und der Selbsttest

Wie PostgreSQL bei OpexGuard die Grenze zwischen Mandanten zieht – Richtlinien je Tabelle, Sitzungsvariable, Systemrolle – und was der Selbsttest bei jedem Start prüft.

Stand 24. September 2026 · geprüft gegen Quellcode und Infrastruktur der Plattform

Mehrere Kunden auf einer Plattform sind nur vertretbar, wenn die Trennung nicht davon abhängt, dass jede Abfrage im Anwendungscode den richtigen Filter trägt. Bei OpexGuard zieht die Datenbank die Grenze. Diese Seite beschreibt, wie – und wie die Anwendung bei jedem Start prüft, dass die Grenze hält.

Das Prinzip

Jede Tabelle mit Kundendaten trägt eine Spalte tenant_id. Auf jeder dieser Tabellen ist Row-Level Security eingeschaltet – und zwar erzwungen (FORCE), damit sie auch für den Eigentümer der Tabelle gilt. Eine Abfrage, gleich woher sie kommt, sieht nur Zeilen, deren tenant_id zum Aufrufer passt. Ein vergessener Filter in der Anwendung liefert dann ein leeres Ergebnis – keine fremden Daten.

Die Richtlinien

Auf jeder geschützten Tabelle liegen zwei Richtlinien:

CREATE POLICY tenant_isolation ON <schema>.<tabelle>
    USING      (tenant_id = ANY (string_to_array(current_setting('app.tenant_ids', true), ',')::uuid[]))
    WITH CHECK (tenant_id = ANY (…));

CREATE POLICY system_access ON <schema>.<tabelle>
    TO <systemrolle> USING (true) WITH CHECK (true);
  • tenant_isolation gilt für die Anwendung. USING filtert das Lesen, WITH CHECK das Schreiben: Eine Zeile mit fremder tenant_id lässt sich weder lesen noch anlegen noch dorthin verschieben.
  • app.tenant_ids ist eine Sitzungsvariable, die die Anwendung zu Beginn jeder Anfrage aus der geprüften Identität setzt – für einen Benutzer die Mandanten, denen er angehört, für einen API-Schlüssel den Mandanten, für den er ausgestellt wurde. Sie ist eine Liste, weil ein Aufrufer mehrere Mandanten umfassen kann (etwa wir als Betreiber); für einen Kunden ist es genau einer.
  • system_access gilt nur für eine eigene Datenbankrolle, die die Anwendung für vertrauenswürdige Systemarbeit annimmt – etwa um beim Schreiben eines Protokolleintrags den Hash des Vorgängers zu lesen, auch wenn der mandantenlos ist. Die Erhöhung ist ein Rollenwechsel, keine Variable, die ein Aufrufer setzen könnte.

Die Anwendung verbindet sich mit einer unprivilegierten Rolle: kein Superuser, nicht der Eigentümer. Das ist entscheidend, weil PostgreSQL Row-Level Security für Superuser grundsätzlich nicht anwendet – eine Verbindung als postgres würde jede Richtlinie still umgehen.

Das Änderungsprotokoll ist die einzige Tabelle, deren Lese- und Schreibregel sich unterscheiden: gelesen wird der eigene Mandant, geschrieben der eigene Mandant oder ein mandantenloser Eintrag – damit eine Änderung an globalen Stammdaten protokolliert werden kann, ohne dass jemand einen Eintrag in einen fremden Mandanten schmuggeln könnte.

Was außerhalb der Datenbank liegt

Row-Level Security reicht so weit wie die Datenbank. Alles, was außerhalb liegt – das Archiv des Protokolls im Blob-Speicher, Zwischenspeicher, Dateien –, muss den Mandanten selbst tragen: als Teil des Pfads, des Schlüssels, des Dateinamens. Das ist eine Entwurfsregel im Code, keine Konvention: Die Abfrage-Schicht für das Archiv nimmt eine Liste von Mandanten entgegen und liest nur deren Pakete.

Der Selbsttest beim Start

Nach den Datenbankmigrationen und vor der ersten Anfrage prüft die Anwendung die Trennung. Fällt eine der Prüfungen durch, startet die Anwendung nicht – eine Umgebung, in der die Trennung nicht greift, liefert keine Kundendaten aus. Der Abbruch lässt sich nur auf Entwicklerrechnern abschalten, wo die lokale Datenbank ohnehin als Superuser läuft; in einer ausgelieferten Umgebung gibt es keinen Schalter.

Prüfung Frage Warum
0 · Schema Ist das Schema überhaupt vorhanden? Voraussetzung für alles Weitere
1 · Rolle Verbindet die Anwendung sich als unprivilegierte Rolle? Für Superuser gilt keine Richtlinie – ein Superuser-Zugang wäre eine Trennung nur auf dem Papier
1b · Erhöhung Funktioniert die Systemrolle nur über den Rollenwechsel? Eine Erhöhung per Sitzungsvariable könnte ein Aufrufer nachahmen
2 · Konfiguration Trägt jede Tabelle mit tenant_id beide Richtlinien? Der wahrscheinlichste künftige Fehler: eine neue Tabelle, an der jemand die Richtlinie vergisst
3 · Verhalten Liefert die Abfrage nach einem Mandanten, den es nicht gibt, wirklich nichts? Misst statt liest: die Richtlinie muss wirken, nicht nur existieren. Scharf, sobald echte Zeilen vorhanden sind
4 · Form Hat eine Richtlinie noch eine alte Hintertür (OR …)? Warnung, kein Abbruch – kostet Leistung, nicht Trennung

Was daraus für Sie folgt

  • Ein Fehler in der Anwendung kann fremde Daten nicht anzeigen; er zeigt im schlimmsten Fall zu wenig.
  • Wir als Betreiber sehen Ihre Daten nur mit einer Identität, die Ihren Mandanten ausdrücklich umfasst – und diese Zugriffe stehen im Protokoll.
  • Dieselben Richtlinien und derselbe Selbsttest laufen in jeder Umgebung – auf der gemeinsamen Plattform, in einer dedizierten Instanz und bei Ihnen vor Ort.

Fragen zu dieser Seite beantworten wir direkt: info@opexguard.de