Sichere Multi-Tenant-DB in PostgreSQL: HealthTech-Fallstudie
Architektur einer Multi-Tenant-PostgreSQL-Datenbank für B2B-SaaS mit strikter Mandantenisolation durch Row-Level Security (RLS).
Auf dieser Seite
Building a B2B-SaaS-Plattform stellt eine klassische architektonische Herausforderung dar: Wie bedienen Sie Hunderte oder Tausende von Kunden aus einer einzigen Anwendungsinstanz und stellen gleichzeitig sicher, dass ihre Daten vollständig isoliert sind? Das ist besonders im HealthTech-Bereich entscheidend, wo Plattformen, die Daten zum Wohlbefinden von Mitarbeitenden erfassen, strenge Sicherheits- und Datenschutzstandards einhalten müssen und sich häufig an Grundsätzen orientieren, wie sie in Vorschriften wie HIPAA zu finden sind.
In dieser Fallstudie entwerfen wir die Datenbankarchitektur für „WellnessCorp“, eine fiktive B2B-Wellness-Plattform, die Unternehmen dabei unterstützt, Fitness-Herausforderungen und psychische Gesundheits-Check-ins ihrer Mitarbeitenden zu verfolgen. Wir gehen das Multi-Tenancy-Problem direkt an, wählen ein effizientes Modell und setzen eine unumstößliche Datentrennung unmittelbar in unserer PostgreSQL-Datenbank um.
Wir erstellen ein robustes, skalierbares und sicheres Multi-Tenant-Datenbankschema mithilfe eines Shared-Schema-Modells, das durch die leistungsstarke Row-Level-Security-(RLS-)Funktion von PostgreSQL abgesichert wird.
Voraussetzungen:
- Grundlegendes Verständnis von SQL und Datenbankkonzepten.
- Vertrautheit mit PostgreSQL (Version 9.5+ für RLS).
- Zugriff auf eine laufende PostgreSQL-Instanz, um die Beispiele zu testen.
Das Problem verstehen: Das Multi-Tenancy-Trilemma
Beim Entwurf einer Multi-Tenant-Datenbank stehen Ihnen im Allgemeinen drei Hauptoptionen zur Verfügung, jeweils mit eigenen Abwägungen zwischen Isolation, Kosten und betrieblicher Komplexität.
- Datenbank pro Mandant (Silo): Die höchste Isolationsstufe. Jeder Kunde erhält eine eigene Datenbank. Das bietet maximale Sicherheit, ist jedoch teuer, ressourcenintensiv und im großen Maßstab ein Albtraum bei Verwaltung und Migration.
- Schema pro Mandant (Bridge): Ein Mittelweg. Alle Kunden teilen sich eine einzige Datenbank, doch jeder hat ein eigenes Schema (eine logische Gruppierung von Tabellen). Das bietet eine gute Isolation, kann bei einer großen Anzahl von Mandanten jedoch komplex zu verwalten sein, und bei Tausenden von Schemas können bestimmte Leistungsprobleme in PostgreSQL auftreten.
- Gemeinsames Schema, gemeinsame Tabellen (Pool): Der kosteneffizienteste und skalierbarste Ansatz. Alle Kunden teilen sich dieselben Tabellen, und eine bestimmte Spalte (z. B.
organization_id) kennzeichnet, welche Zeile zu welchem Mandanten gehört. Die zentrale Herausforderung besteht hier darin, die Datenisolation durchzusetzen. Ein einzelner Fehler in Ihrem Anwendungscode (etwa das Vergessen einerWHERE organization_id = ?-Klausel) könnte die Daten eines Mandanten einem anderen offenlegen.
Für WellnessCorp wählen wir das Shared-Schema-Modell wegen seiner Skalierbarkeit und Kosteneffizienz. Um das Problem von Datenlecks zu lösen, verlassen wir uns nicht auf Anwendungscode. Stattdessen übertragen wir die Verantwortung für die Datenisolation mithilfe von Row-Level Security auf die Datenbank selbst.
Voraussetzungen
Stellen Sie vor dem Start sicher, dass PostgreSQL installiert ist. Wir erstellen unsere Hauptdatenbank und eine dedizierte Rolle ohne Superuser-Rechte, die unsere Anwendung verwenden soll. Das ist eine entscheidende Sicherheitsmaßnahme.
-- Connect as a superuser (e.g., 'postgres') to run these commands
-- Create a new database for our platform
CREATE DATABASE wellness_corp_db;
-- Create a dedicated role for our application with login privileges
-- NEVER use a superuser for your application's day-to-day operations
CREATE ROLE wellness_app WITH LOGIN PASSWORD 'a_very_secure_password';
-- Grant the ability to connect to the new database
GRANT CONNECT ON DATABASE wellness_corp_db TO wellness_app;
-- Now, connect to the new database to continue setup
\c wellness_corp_db
-- Grant usage on the public schema to our app role
GRANT USAGE ON SCHEMA public TO wellness_app;
Erwartete Ausgabe:
Sie sollten die Bestätigungen CREATE DATABASE, CREATE ROLE und GRANT sehen, und Ihre psql-Eingabeaufforderung sollte zu wellness_corp_db=> wechseln.
Schritt 1: Entwurf des zentralen Multi-Tenant-Schemas
Was wir tun
Zuerst definieren wir die zentralen Tabellen unserer Plattform. Der wesentliche Entwurfsgrundsatz lautet: Jede Tabelle mit mandantenspezifischen Daten muss eine Spalte organization_id enthalten. Diese Spalte ist die Grundlage unserer Multi-Tenancy-Strategie.
Umsetzung
Wir erstellen die Tabellen für Organisationen (unsere Mandanten), Benutzer und Wellness-Einträge.
-- src/db/schema.sql
-- Stores our tenants (corporate clients)
CREATE TABLE organizations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Stores the employees of each organization
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(organization_id, email) -- Email must be unique within an organization
);
-- Stores wellness data points for each user
CREATE TABLE wellness_entries (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
organization_id UUID NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
entry_type TEXT NOT NULL, -- e.g., 'steps', 'mood_check_in'
value INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Grant our application role permissions on these tables
GRANT SELECT, INSERT, UPDATE, DELETE ON organizations, users, wellness_entries TO wellness_app;
So funktioniert es
organizations: Diese Tabelle ist unser Mandantenverzeichnis.users: Jeder Benutzer ist direkt mit einerorganization_idverknüpft.ON DELETE CASCADEstellt sicher, dass beim Löschen eines Unternehmenskontos automatisch alle zugehörigen Benutzer entfernt werden.wellness_entries: Diese Tabelle enthält ebenfallsorganization_id. Technisch könnten wir über die Tabelleuserszur Organisation zurückverknüpfen, doch die Duplizierung hier vereinfacht unsere Sicherheitsrichtlinien und kann die Leistung verbessern, indem zusätzliche Joins für Sicherheitsprüfungen vermieden werden.
Schritt 2: Implementierung von Row-Level Security (RLS)
Was wir tun
Hier geschieht die Magie ✨. Wir aktivieren RLS für unsere Tabellen und erstellen Sicherheitsrichtlinien. Diese Richtlinien sind Regeln, die PostgreSQL für jede einzelne Abfrage durchsetzt. Unsere Regel ist einfach: „Ein Benutzer darf nur Daten sehen, die zu seiner organization_id passen.“
Umsetzung
Zuerst benötigen wir eine Möglichkeit, PostgreSQL mitzuteilen, welcher Mandant für eine bestimmte Sitzung gerade aktiv ist. Wir verwenden eine Sitzungsvariable, also eine temporäre Einstellung für die aktuelle Datenbankverbindung.
-- In your application's connection setup, after a user authenticates:
-- SET session.current_organization_id = 'the-user-org-id-from-db';
Nun aktivieren wir RLS und erstellen die Richtlinien. Dies muss ein Superuser oder der Tabelleneigentümer tun.
-- src/db/security_policies.sql
-- Enable RLS on the tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE wellness_entries ENABLE ROW LEVEL SECURITY;
-- Create a policy to allow users to see ONLY the data from their own organization
CREATE POLICY tenant_isolation_policy ON users
FOR ALL
USING (organization_id = current_setting('session.current_organization_id')::UUID);
CREATE POLICY tenant_isolation_policy ON wellness_entries
FOR ALL
USING (organization_id = current_setting('session.current_organization_id')::UUID);
So funktioniert es
ALTER TABLE ... ENABLE ROW LEVEL SECURITY;: Dieser Befehl aktiviert RLS für die angegebene Tabelle. Standardmäßig handelt es sich um eine „Alles-verweigern“-Richtlinie, sodass keine Zeilen sichtbar sind, bis eine zulassende Richtlinie erstellt wird.CREATE POLICY ...: Wir definieren eine Richtlinie namenstenant_isolation_policy.FOR ALL: Dies wendet die Richtlinie auf die BefehleSELECT,INSERT,UPDATEundDELETEan.USING (...): Dies ist der Kern der Richtlinie. Damit eine vorhandene Zeile sichtbar ist (fürSELECT,UPDATE,DELETE), muss diese Bedingung wahr sein. Wir prüfen, ob dieorganization_idder Zeile mit der ID übereinstimmt, die wir in unserer Sitzungsvariable gesetzt haben.current_setting('session.current_organization_id')::UUID: Diese Funktion ruft unsere Sitzungsvariable sicher ab und wandelt sie zum Vergleich in den TypUUIDum.
Alles zusammenfügen: Ein praktischer Test
Wir testen unser Setup, um RLS in Aktion zu sehen.
1. Beispieldaten einfügen (als Superuser)
-- Insert two different companies
INSERT INTO organizations (id, name) VALUES
('11111111-1111-1111-1111-111111111111', 'Innovate Inc.'),
('22222222-2222-2222-2222-222222222222', 'Synergy Corp');
-- Insert users for each company
INSERT INTO users (organization_id, email, password_hash) VALUES
('11111111-1111-1111-1111-111111111111', '[email protected]', 'hash1'),
('22222222-2222-2222-2222-222222222222', '[email protected]', 'hash2');
2. Versuch, Daten ohne gesetzten Mandanten auszuwählen
Verbinden Sie sich als Rolle wellness_app und versuchen Sie, die Benutzer anzuzeigen.
SET ROLE wellness_app;
SELECT * FROM users;
Erwartete Ausgabe:
-- (0 rows)
Erfolg! RLS funktioniert. Da wir unsere Sitzungsvariable nicht gesetzt haben, ist die Richtlinienbedingung nicht erfüllt, und die Datenbank verhält sich so, als wäre die Tabelle leer.
3. Daten als bestimmter Mandant auswählen
Nun nehmen wir an, wir sind ein authentifizierter Benutzer von „Innovate Inc.“
-- Set the tenant context for this session
SET session.current_organization_id = '11111111-1111-1111-1111-111111111111';
-- Now, run the query again
SELECT * FROM users;
Erwartete Ausgabe:
-- id | organization_id | email | ...
-- --------------------------------------+--------------------------------------+--------------------+----
-- ... | 11111111-1111-1111-1111-111111111111 | [email protected] | ...
-- (1 row)
Es funktioniert einwandfrei! Unsere Verbindung ist nun auf Innovate Inc. „beschränkt“, und wir sehen nur deren Daten. Bob von Synergy Corp ist für uns völlig unsichtbar. Diese Isolation wird von der Datenbank durchgesetzt und verringert das Risiko versehentlicher Datenlecks auf der Anwendungsebene erheblich.
Bewährte Sicherheitspraktiken für eine Wellness-Plattform
Der Umgang mit gesundheitsbezogenen Daten erfordert besondere Sorgfalt. Hier sind zentrale Sicherheitsaspekte:
- Verschlüsselung: Erzwingen Sie stets SSL/TLS für Daten während der Übertragung (
sslmode=requirein Verbindungszeichenfolgen). Für ruhende Daten verwenden Sie eine vollständige Festplattenverschlüsselung auf Ihrem Datenbankserver oder nutzen verwaltete Datenbankdienste (wie AWS RDS), die dies für Sie übernehmen. - HIPAA-Konformität: PostgreSQL kann HIPAA-konform gemacht werden, ist jedoch nicht von Haus aus konform. Zentrale Verantwortlichkeiten umfassen:
- Zugriffskontrolle: Unsere RLS-Implementierung ist eine leistungsstarke Form der Zugriffskontrolle.
- Audit-Protokollierung: Verwenden Sie Erweiterungen wie
pgaudit, um ein detailliertes Protokoll aller Zugriffe und Änderungen an sensiblen Daten zu erstellen. - Datenanonymisierung: Verwenden Sie für Analysen Techniken zur Datenmaskierung oder Tokenisierung, um geschützte Gesundheitsinformationen (Protected Health Information, PHI) zu de-identifizieren. Die Erweiterung
pgcryptokann dabei nützlich sein.
- Geringste Berechtigung: Die von uns erstellte Rolle
wellness_appsollte nur die unbedingt erforderlichen Berechtigungen besitzen. Sie sollte kein Superuser sein und in der Produktion keine Rechte zum Ändern von Tabellen haben.
Alternative Ansätze
RLS passt hervorragend zu unserem Anwendungsfall, dennoch lohnt es sich, die Alternativen zu kennen.
- Schema pro Mandant:
- Vorteile: Stärkere logische Trennung. Das Sichern und Wiederherstellen der Daten eines einzelnen Mandanten kann einfacher sein.
- Nachteile: Komplexere Migrationsskripte (müssen alle Schemas durchlaufen). Bei Tausenden von Mandanten kann ein Leistungsaufwand entstehen. Es wird eine individuelle Routing-Logik in der Anwendung benötigt, um auf das richtige Schema zu verweisen.
- Durchsetzung auf Anwendungsebene:
- Vorteile: Es werden keine komplexen Datenbankfunktionen benötigt. Kann in jeder Datenbank umgesetzt werden.
- Nachteile: Extrem fehleranfällig. Eine vergessene
WHERE-Klausel in einer von Tausenden Abfragen kann eine massive Datenschutzverletzung verursachen. Die gesamte Sicherheitslast liegt bei den Entwicklern für jede einzelne Codezeile, die sie schreiben.
Fazit
Wir haben erfolgreich eine Multi-Tenant-Datenbank für eine B2B-Wellness-Plattform mit einem skalierbaren Shared-Schema-Ansatz entworfen. Durch den Einsatz der Row-Level Security von PostgreSQL haben wir die entscheidende Verantwortung für die Datenisolation von der Anwendungsebene in die Datenbank selbst verlagert und so ein sichereres und robusteres System geschaffen.
Diese Architektur bietet die Kostenvorteile einer gemeinsamen Infrastruktur und liefert zugleich die starken Datengarantien, die eine moderne, sichere SaaS-Anwendung benötigt – insbesondere in einem sensiblen Bereich wie HealthTech.
Nächste Schritte für Leserinnen und Leser:
- Versuchen Sie, den Richtlinien eine
WITH CHECK-Klausel hinzuzufügen, um durchzusetzen, dass neue Daten nur für den aktuellen Mandanten eingefügt werden können. - Erkunden Sie die Verwendung von JSONB, um flexible Wellness-Daten in der Tabelle
wellness_entrieszu speichern. - Untersuchen Sie die Erweiterung
pgaudit, um einen umfassenden Audit-Trail einzurichten.
Ressourcen
- Offizielle Dokumentation: PostgreSQL Row-Level Security Policies
- SaaS-Architekturen: AWS SaaS Tenant Isolation Strategies
Kurzreferenz
| Aspekt | Wichtige Information |
|---|---|
| Thema | Entwicklung |
| Relevanz | Patientenaufklärung und Bewusstsein |
| Maßnahme | Besprechen Sie dies mit Ihrer Ärztin oder Ihrem Arzt |
| Häufigkeit | Wie von Ihrem Behandlungsteam empfohlen |
| Evidenz | Basierend auf aktuellen klinischen Leitlinien |
Häufig gestellte Fragen
F: Woran erkenne ich, ob dies auf mich zutrifft? A: Besprechen Sie Ihre persönliche und familiäre Krankengeschichte mit Ihrer Ärztin oder Ihrem Arzt, um Ihre individuellen Risikofaktoren einzuschätzen und geeignete nächste Schritte festzulegen.
F: Was sollte ich meine Ärztin oder meinen Arzt fragen? A: Gute Fragen sind: „Was bedeuten meine Ergebnisse?“, „Welche Optionen habe ich?“, „Welche Änderungen des Lebensstils würden helfen?“ und „Wann sollte ich zur Nachuntersuchung kommen?“
F: Gibt es Änderungen des Lebensstils, die helfen können? A: Die meisten Gesundheitszustände profitieren von grundlegenden Maßnahmen: ausgewogene Ernährung, regelmäßige körperliche Aktivität (150+ Minuten/Woche), guter Schlaf (7–9 Stunden), Stressbewältigung und der Verzicht auf Tabak.
Medizinischer Haftungsausschluss
Dieser Artikel dient ausschließlich Bildungszwecken und stellt keine medizinische Beratung dar. Konsultieren Sie stets eine qualifizierte medizinische Fachkraft, bevor Sie Entscheidungen über Ihre Gesundheit, Behandlung oder Versorgung treffen. Individuelle Umstände unterscheiden sich, und was für eine Person angemessen ist, muss für eine andere nicht angemessen sein.
Wenn Sie einen medizinischen Notfall erleben, rufen Sie sofort 911 (oder Ihre örtliche Notrufnummer) an.