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_isolationgilt für die Anwendung.USINGfiltert das Lesen,WITH CHECKdas Schreiben: Eine Zeile mit fremdertenant_idlässt sich weder lesen noch anlegen noch dorthin verschieben.app.tenant_idsist 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_accessgilt 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.