PostgreSQL Multi-Tenancy mit RLS: 5 Architekturfallen
October 17, 2026
PostgreSQL Multi-Tenancy mit RLS: 5 Architekturfallen
Wer Multi-Tenant-Architekturen auf Basis von PostgreSQL entwirft, steht früh vor einer Richtungsentscheidung: Schema-per-Tenant oder Shared-Schema mit logischer Trennung? In den ersten Wochen fühlt sich ein Schema pro Mandant oft intuitiv an. Doch sobald die Kundenzahl wächst, rächt sich dieses Modell massiv.
Ab 500 bis 1.000 Schemata gerät PostgreSQL ins Schwitzen: Der Systemkatalog (pg_class, pg_attribute) bläht sich auf, führt zu spürbarem Cache-Thrashing im Shared Buffer und Datenbankmigrationen verwandeln sich in stundenlange Wartungsfenster, die DDL-Locks kaskadieren lassen. Shared-Schema-Architekturen mit Row-Level Security (RLS) skalieren hingegen mühelos auf zehntausende Tenants, ohne dass das Datenbankschema repliziert werden muss. Doch RLS ist kein Selbstläufer. Wer RLS naiv implementiert, riskiert fatale Datenlecks und massive Performance-Einbrüche.
In diesem Artikel zeige ich dir praxiserprobte Strategien, wie du PostgreSQL Row-Level Security performant und kompromisslos sicher in deine Backend-Dienste integrierst.
1. Die Connection-Pool-Falle: Session Leakage bei PgBouncer
In modernen Cloud-Setups läuft vor der PostgreSQL-Instanz nahezu immer ein Connection Pooler wie PgBouncer im Transaction-Pooling-Modus. Das bedeutet: Zwei aufeinanderfolgende Requests desselben Nutzers können auf völlig unterschiedlichen physischen Datenbankverbindungen landen. Schlimmer noch: Eine physische Verbindung wird nach dem Commit direkt an die nächste Anfrage eines völlig anderen Tenants weitergereicht.
Verwendest du zur Festlegung des Mandanten-Kontexts ein einfaches SET app.current_tenant = 'tenant-uuid', verbleibt dieser Wert auf der Server-Verbindung. Der nächste HTTP-Request erbt diesen Tenant-Status – ein klassisches Cross-Tenant-Sicherheitsdesaster.
Die Lösung: Der Tenant-Scope darf ausschließlich innerhalb einer Transaktion gültig sein. Das erreichst du über den dritten Parameter is_local = true in set_config oder den Befehl SET LOCAL.
BEGIN;
-- Der dritte Parameter 'true' bindet den Wert strikt an die aktuelle Transaktion:
SELECT set_config('app.current_tenant', 'a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11', true);
-- Alle Abfragen laufen jetzt isoliert im Tenant-Kontext:
SELECT * FROM customer_orders WHERE status = 'pending';
COMMIT;
Sobald das COMMIT abgesetzt wird, setzt PostgreSQL den Wert automatisch zurück. Die physische Verbindung kehrt ohne Altlasten in den Pool zurück.
2. RLS-Policies und die STABLE-Regel für Hilfsfunktionen
Damit die RLS-Policy den Mandanten-Kontext effizient verarbeiten kann, greifen wir auf eine kleine Helferfunktion zurück. Ein fataler Fehler, den man oft in Projekten sieht: Funktionen ohne definierte Volatilitäts-Kennzeichnung (VOLATILE).
Ist eine Funktion VOLATILE, muss der Query Planner sie für jede einzelne Tabellenzeile neu ausführen. Bei 500.000 Zeilen bedeutet das 500.000 Kontextwechsel und String-Parsing-Operationen. Dasselbe Phänomen sieht man regelmäßig bei Supabase-Implementierungen, wenn auth.jwt() unbedacht gecallt wird. Deklarierst du die Funktion hingegen als STABLE PARALLEL SAFE, cached die Engine den Rückgabewert pro Query und erlaubt dem Planner, Partition Pruning sowie Index-Scans vorab durchzurechnen.
CREATE OR REPLACE FUNCTION current_tenant_id()
RETURNS uuid
LANGUAGE sql
STABLE
PARALLEL SAFE
AS $$
SELECT NULLIF(current_setting('app.current_tenant', true), '')::uuid;
$$;
3. Absicherung mit FORCE ROW LEVEL SECURITY
Ein oft übersehenes Verhalten von PostgreSQL: Tabellen-Owner und Superuser umgehen RLS standardmäßig komplett! Wenn dein Backend mit dem Benutzer operiert, der auch die Tabellen via Migration angelegt hat, greift keine einzige RLS-Regel.
Um dieses Sicherheitsrisiko auszuschließen, trennst du idealerweise die Rollen in einen Migrations-User und einen Laufzeit-User (app_role). Zusätzlich aktivierst du RLS immer mit dem Zusatz FORCE:
-- RLS aktivieren
ALTER TABLE customer_orders ENABLE ROW LEVEL SECURITY;
-- Sicherheitsnetz: Auch Tabellen-Owner unterliegen strikt den Policies
ALTER TABLE customer_orders FORCE ROW LEVEL SECURITY;
-- Policy für Lese- und Schreiboperationen etablieren
CREATE POLICY tenant_isolation_policy ON customer_orders
FOR ALL
TO app_role
USING (tenant_id = current_tenant_id())
WITH CHECK (tenant_id = current_tenant_id());
USING filtert eingehende SELECT-, UPDATE- und DELETE-Queries. WITH CHECK garantiert, dass über INSERT- oder UPDATE-Operationen keine Datensätze für fremde Tenants eingeschleust werden können.
4. Performance-Tuning: Indexierung und Generic Plans
Sauber aufgebautes RLS erzeugt lediglich einen minimalen Overhead von ca. 2% bis 8% CPU- und Latenzzeit. Dieser minimale Aufschlag gilt allerdings nur dann, wenn passende B-Tree-Indizes existieren, die mit der tenant_id als führender Spalte arbeiten.
Schlecht formulierte Policies (z. B. Subqueries auf unindexierte Berechtigungstabellen) können Abfragen dagegen um den Faktor 100x verlangsamen. Achte bei Mandantentabellen auf zusammengesetzte Indizes, die häufige Filterkriterien und Sortierungen abdecken:
-- Optimierter B-Tree-Index mit Tenant-Präfix
CREATE INDEX idx_orders_tenant_lookup
ON customer_orders (tenant_id, created_at DESC)
INCLUDE (total_amount);
Die Falle mit Prepared Statements und Generic Plans
Wenn du PgBouncer mit Transaction Pooling und parametrisierten Prepared Statements kombinierst, schlägt gelegentlich der Query Planner zu. PostgreSQL entscheidet nach fünf Ausführungen oft, einen generischen Plan (Generic Plan) anstelle eines spezifischen Plans (Custom Plan) zu wählen. Da die Session-Variable app.current_tenant erst zur Laufzeit dynamisch evaluiert wird, unterschätzt der Planner möglicherweise die Selektivität des tenant_id-Filters massiv und wechselt von einem effizienten Index-Scan auf einen verheerenden Bitmap Heap Scan oder Parallel Sequential Scan. Prüfe deine Slow-Query-Logs mittels EXPLAIN (ANALYZE, BUFFERS) regelmäßig auf solche Plan-Regressionen.
5. Datenlecks über Foreign Keys verhindern
Ein extrem subtiler Angriffsvektor bei RLS sind Foreign Key Constraints. PostgreSQL prüft Fremdschlüssel-Beziehungen standardmäßig mit Systemrechten – und ignoriert dabei RLS auf der referenzierten Tabelle.
Ein konkretes Szenario: Ein Angreifer in Tenant B errät eine ID aus Tenant A und referenziert diese in einem Datensatz. Schlägt die Foreign-Key-Prüfung nicht fehl, weiß der Angreifer, dass der Datensatz bei einem anderen Kunden existiert (Oracle-Leak). Schlimmer: Er könnte referenzierte Aktionen triggern.
Verhindere das, indem du Fremdschlüssel über zusammengesetzte Constraints abbildest:
-- Eindeutigkeit auf Eltern-Tabelle inklusive Tenant sicherstellen
ALTER TABLE customer_orders
ADD CONSTRAINT uq_orders_tenant_order
UNIQUE (tenant_id, id);
-- Kind-Tabelle bindet Tenant physisch an die Referenz
ALTER TABLE order_items
ADD CONSTRAINT fk_order_items_parent
FOREIGN KEY (tenant_id, order_id)
REFERENCES customer_orders (tenant_id, id)
ON DELETE CASCADE;
Dadurch ist es auf Datenbankebene unmöglich, Zeilen über Mandantengrenzen hinweg zu verknüpfen.
Horizontale Skalierung mit Citus
Wächst dein Datenbestand so stark, dass ein Node nicht mehr ausreicht, lässt sich dieses RLS-Setup nahtlos auf distribuierte Lösungen wie Citus übertragen. Der Schlüssel hierbei: Die tenant_id fungiert als Distribution Column (Sharding-Key). Da PostgreSQL dank der RLS-Policy die tenant_id in jedem Filter erzwingt, kann der Citus-Query-Router die Abfrage via Router-Exec direkt und punktgenau an den zuständigen Worker-Node senden – teure Cross-Node-Netzwerkoperationen entfallen vollständig.
Fazit
PostgreSQL Row-Level Security bietet das perfekte Fundament für moderne SaaS-Architekturen: schlank im Betrieb, kosteneffizient in der Infrastruktur und skalierbar bis in Millionen Datensätze. Entscheidend ist, dass Security nicht erst im ORM, sondern direkt im PostgreSQL-Core verankert wird.
Planst du eine hochsichere Multi-Tenant-Architektur oder möchtest bestehende PostgreSQL-Datenbanken gegen Datenlecks abhärten? Als spezialisierter Backend-Entwickler unterstütze ich dich bei Konzeption, Performance-Optimierung und Security-Reviews unter buengener-software.de.