Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
136 lines
13 KiB
Markdown
136 lines
13 KiB
Markdown
# Konzept — Datensicherung, Wiederherstellung, DSGVO-Export & Löschung (certvia)
|
|
|
|
> Status: **Konzept/Entscheidungsvorlage** (kein Code). Produkt: **certvia** (ISMS-Tool, Single-DB + Postgres-RLS, alle Mandanten in denselben Tabellen mit `tenant_id`). Kernidee: **eine mandanten-scoped Traversierungs-Engine** bedient vier Zwecke — Per-Tenant-Backup, gezielten Restore, DSGVO-Auskunft/-Export und DSGVO-Löschung/Offboarding.
|
|
|
|
## 0. Zielbild
|
|
1. **Disaster-Recovery** der gesamten DB (Totalausfall, Ransomware, „Zeitpunkt-T-Wiederherstellung").
|
|
2. **Gezielter Einzelkunden-Restore** (Kunde A zurück, Kunde B unangetastet) — über das **Betreiber-Portal**.
|
|
3. **DSGVO-Auskunft/-Datenportabilität** (Art. 15/20) — je Mandant und je betroffener Person.
|
|
4. **DSGVO-Löschung** (Art. 17) — ganzer Mandant (Offboarding) und einzelne Person, mit ISMS-konformer Aufbewahrung.
|
|
|
|
Alle vier teilen sich **einen** Baustein: das Durchlaufen aller mandantengescopten Tabellen in FK-Reihenfolge, gefiltert auf `tenant_id` (bzw. auf eine Person).
|
|
|
|
## 1. Architektur-Grundlage (bestehend, wird genutzt)
|
|
- **`TENANT_MODELS`** (`src/server/db.ts`) = autoritative Menge aller mandantengescopten Tabellen. Plus deren FK-Topologie ⇒ die verbindliche Reihenfolge für Export (parent→child) und Löschung (child→parent).
|
|
- **`tenant_id`-Spalte + RLS** ⇒ jede Operation `WHERE tenant_id = A` ist **beweisbar** auf einen Mandanten begrenzt.
|
|
- **cuid-Primärschlüssel** (kein Serial) ⇒ Reinsert kollisionsfrei, IDs bleiben erhalten, keine Sequenz-Konflikte.
|
|
- **Owner-`prisma`-Client** (BYPASSRLS) für Backup/Restore/Löschung; **nie** der RLS-Client.
|
|
- **Globale Tabellen** (Kataloge, `Permission`, `Tenant`-Stammsatz, künftig **`Identity`**) sind **nicht** tenant-scoped → über Schicht A gesichert, nicht Teil des Tenant-Artefakts.
|
|
- **MinIO/S3** hält Dateien/Logos je Mandant (Prefix) — DB-Backup allein reicht nicht.
|
|
|
|
## 2. Schicht A — Cluster-Disaster-Recovery (ganze DB)
|
|
- **pgBackRest** oder **wal-g**: periodisches Vollbackup + kontinuierliches **WAL-Archiving** nach S3/offsite ⇒ **PITR** (Point-in-Time-Recovery).
|
|
- Zweck: Totalausfall/Ransomware/menschlicher Massenfehler → „DB auf Zeitpunkt T".
|
|
- **Verschlüsselt** (SSE-KMS oder client-seitig) — dockt an die (vorerst geparkte) DB-Härtung an.
|
|
- Deckt auch die **globalen** Tabellen inkl. `Identity`/Credentials ab, die Schicht B bewusst nicht anfasst.
|
|
|
|
## 3. Schicht B — Mandanten-scoped Logical Export/Restore
|
|
### Export (Backup-Artefakt je Mandant)
|
|
- Aus **einem konsistenten Snapshot** (`REPEATABLE READ`-Transaktion), damit die FK-Integrität innerhalb des Mandanten stimmt.
|
|
- Je Tabelle aus `TENANT_MODELS`: `COPY (SELECT * FROM t WHERE tenant_id = 'A') TO …` → ein Artefakt (z. B. NDJSON/COPY) pro Mandant → gzip → verschlüsselt nach S3.
|
|
- Enthält **zusätzlich** den MinIO-Prefix des Mandanten (Datei-Snapshot) und ein **Manifest** (Schema-/Migrationsversion, Zeitstempel, Zeilenzahlen je Tabelle, Prüfsummen).
|
|
- Zeitplan: nächtlich + **on-demand vor riskanten Operationen** (Restore, Massen-Import).
|
|
|
|
### Restore (gezielt Kunde A)
|
|
- Owner-Client, **eine Transaktion**, FK-Reihenfolge, alles `WHERE tenant_id = 'A'`:
|
|
1. Mandant **sperren** (`status = SUSPENDED`) → keine parallelen Schreibzugriffe.
|
|
2. **Pre-Restore-Sicherheitsschnappschuss** des aktuellen Standes (Restore ist damit reversibel).
|
|
3. **Replace**: `DELETE … WHERE tenant_id='A'` (child→parent) + Reinsert aus dem Artefakt (parent→child). Constraints ggf. `DEFERRED`.
|
|
4. MinIO-Prefix des Mandanten wiederherstellen.
|
|
5. Mandant reaktivieren, **Audit** schreiben.
|
|
- **Beweisbar sicher** für andere Mandanten: keine Operation verlässt `tenant_id='A'`.
|
|
- Schema-Version im Manifest gegen aktuelle Migration prüfen; bei Differenz Artefakt vor Reinsert migrieren/abweisen.
|
|
|
|
## 4. Betreiber-Portal — Restore-Flow
|
|
Restore ist eine **Betreiber**-Fähigkeit (kein Mandanten-Admin) → Plattform-Portal (`/admin`, `platformAuth`).
|
|
- **Auslöser:** Aktion „Wiederherstellen" auf `/admin/[id]`, gegated durch **`requirePlatformFullAdmin` + frischer MFA-Step-up** (`assertPlatformStepUp`).
|
|
- **Auswahl:** Mandant + Sicherungspunkt (Liste der Per-Tenant-Snapshots / PITR-Zeitpunkt) + **Vorschau/Dry-run** (Zeilenzahlen, Snapshot-Zeit) + **getippte Bestätigung** („RESTORE kunde-a").
|
|
- **Ausführung als Hintergrund-Job im Worker** (BullMQ, vorhanden) — **nicht** inline in der Server-Action (Timeouts/Progress/Audit). Die Action **enqueued** nur.
|
|
- **Kontrollen:** Sperre während Restore · Pre-Restore-Schnappschuss · `tenant_id`-Scope · Plattform-Audit (wer/Mandant/Snapshot/wann) · MinIO mit.
|
|
|
|
## 5. DSGVO-Export (Art. 15 Auskunft / Art. 20 Portabilität)
|
|
**Rollen (wichtig):** certvia ist **Auftragsverarbeiter**, der Mandant ist **Verantwortlicher**. Anfragen richten sich an den Mandanten; certvia liefert das **Werkzeug**. Deshalb existiert der Export an **zwei** Stellen:
|
|
- **Mandanten-Self-Service** (Mandanten-Admin, für die eigenen Betroffenen),
|
|
- **Betreiber-Portal** (AVV-Unterstützung / Ausfallhilfe).
|
|
|
|
**Zwei Granularitäten:**
|
|
1. **Per-Mandant** — der gesamte Kundendatensatz (= das Backup-Artefakt aus §3, maschinenlesbar/JSON). Nutzen: Portabilität beim Anbieterwechsel, Offboarding-Kopie.
|
|
2. **Per-Betroffener** (eine natürliche Person) — alle personenbezogenen Zeilen dieser Person: `User` (Mitgliedschaft), Auth-Daten aus **`Identity`** (Existenz/E-Mail/MFA-Status — **keine** Secrets), sowie alle Referenzen (`owner_id`, `assigneeId`, `createdBy`, `AuditLog.actorId`, `TaskParticipant`, `Audit.*UserId`, …) und Freitext mit Personenbezug.
|
|
|
|
**Format/Zustellung:** ZIP (JSON + zugehörige Dateien aus MinIO), erzeugt vom **Worker-Job**, Zustellung über **zeitlich begrenzten signierten Link** (nicht per Mail). Export wird **auditiert**.
|
|
|
|
## 6. DSGVO-Löschung (Art. 17 „Recht auf Vergessenwerden")
|
|
**Zwei Scopes:**
|
|
1. **Mandanten-Löschung / Offboarding** — alle `tenant_id='A'`-Zeilen (child→parent, eine Transaktion) + MinIO-Prefix. Danach **globale Aufräumung**: eine `Identity` **ohne verbleibende Mitgliedschaft** wird gelöscht/anonymisiert.
|
|
2. **Einzelne Person (Betroffener)** — Kernspannung im ISMS: **Löschung vs. Nachweis-/Aufbewahrungspflicht**.
|
|
|
|
**Löschen vs. Anonymisieren (die zentrale Design-Regel):**
|
|
- Datensätze, die aus **ISMS-/Nachweisgründen** oder wegen **rechtlicher Pflicht** (Art. 17 Abs. 3) erhalten bleiben müssen (v. a. **Audit-Trail**, Freigaben, Nachweise), werden **anonymisiert/pseudonymisiert** (Name/E-Mail → Tombstone, referenzielle Struktur bleibt), **nicht** hart gelöscht — sonst bricht die Nachvollziehbarkeit.
|
|
- Wo keine Aufbewahrungspflicht greift: **Hard-Delete**.
|
|
- Ergebnis: **Löschnachweis/„Deletion Certificate"** (wer/wann/Scope/was gelöscht vs. anonymisiert), auditiert.
|
|
|
|
**Personen über mehrere Mandanten (Auth-Umbau!):** Jeder Mandant ist ein **eigener Verantwortlicher**. Löschung erfolgt **pro Mandant-Scope** (Mitgliedschaft + PII in dessen Datensätzen) — **nicht** global über alle Mandanten. Erst wenn **keine** Mitgliedschaft der Person mehr existiert, wird die **globale `Identity`** entfernt/anonymisiert.
|
|
|
|
**Backups vs. Löschung (bekannte Spannung):** Unveränderliche Backups lassen sich nicht punktuell „aufbohren". Standard: Löschung wirkt auf **Live-Daten** + wird über eine **Tombstone-/Löschliste beim Restore erneut angewandt** (ein alter Snapshot bringt gelöschte PII nicht zurück); Backups laufen über **Retention** aus. Diese Regel muss dokumentiert und im Restore-Job erzwungen werden.
|
|
|
|
## 7. Gemeinsame Engine (der Architektur-Gewinn)
|
|
Backup-Export, DSGVO-Export, Offboarding und Löschung teilen **eine** tenant-scoped Traversierung über `TENANT_MODELS` + FK-Topologie:
|
|
- Export = SELECT je Tabelle, `tenant_id`- oder personen-gefiltert.
|
|
- Restore = DELETE+INSERT, `tenant_id`-gescopt.
|
|
- Löschung = DELETE (child→parent) bzw. UPDATE-Anonymisierung, `tenant_id`- oder personen-gescopt.
|
|
Ein Baustein, vier Anwendungsfälle → geringe Redundanz, konsistentes Verhalten, ein Testfokus (Isolation).
|
|
|
|
## 8. Schnittstelle zum Auth-Umbau (Identity)
|
|
- **Export einer Person** vereint globale `Identity` (Existenz/E-Mail/MFA-Status, **keine** Secrets) + alle Mitgliedschaften + zugewiesene/erstellte Objekte.
|
|
- **Restore eines Mandanten** holt **Mitgliedschaften** (`User`) zurück, **nicht** den globalen Credential-Store (der liegt in Schicht A). Fehlt beim Reinsert die referenzierte `Identity` → sauber behandeln (neu verknüpfen/Einladung).
|
|
- **Löschung** ist controller-scoped (pro Mandant); globale `Identity` erst bei 0 Mitgliedschaften.
|
|
⇒ Diese Punkte **jetzt** mitdesignen, **umsetzen nach** WS0 (Identity-Fundament).
|
|
|
|
## 9. Verschlüsselung & Schlüssel (Entscheidung)
|
|
**Entscheidung:** **Client-seitige AES-256-Verschlüsselung mit einem Schlüssel pro Umgebung** — **nicht** allein auf Storage-SSE verlassen. Grund: das Artefakt ist verschlüsselt, **bevor** es S3/MinIO erreicht → der Speicher-Betreiber sieht nie Klartext (at-rest **und** in-transit **und** zero-knowledge vom Speicher). Beide Backup-Tools können das nativ (keine Zusatzkomponente). Dies ist das **gemeinsame Primitiv**, auf dem die Backup-Lane und die Härtungs-Lane bauen.
|
|
|
|
**Stack (durchgängig AES-256, konsistent zur bestehenden TOTP-Verschlüsselung AES-256-GCM):**
|
|
| Schutzobjekt | Mechanismus |
|
|
|---|---|
|
|
| Host at-rest (`pgdata`, MinIO) | **LUKS / provider-verschlüsseltes Volume** (deckt physischen Plattendiebstahl/Decommission; transparent im Betrieb) |
|
|
| Cluster-Backup (Schicht A) | **pgBackRest** mit nativer **AES-256-Repo-Verschlüsselung** (`repo-cipher-type=aes-256-cbc` + `repo-cipher-pass`) → S3/MinIO |
|
|
| Per-Tenant-Export + DSGVO-Pakete (Schicht B) | **`age`** je Artefakt (X25519, encrypt-then-upload) |
|
|
| MinIO-Dateien | **restic** (eingebaute Verschlüsselung) oder MinIO-SSE als Defense-in-Depth |
|
|
|
|
**Warum nicht SSE-only:** SSE (AWS SSE-KMS / MinIO KES+KMS) ist transparent, aber der Speicher-Betreiber hält die Schlüssel, und self-hosted MinIO-SSE bräuchte KES+KMS als Extra-Komponente. SSE gern **zusätzlich**, nicht als alleinige Zusicherung.
|
|
|
|
**Schlüsselverwaltung:**
|
|
- **Jetzt (self-hosted VPS):** **ein** AES-256-Schlüssel (pgBackRest) + **ein** `age`-Keypair **pro Umgebung** (test/dev/prod getrennt), als **Coolify-Env-Secret** + **Offline-Kopie im Org-Passwortmanager** (versiegelt).
|
|
- **Register „restore-kritische Secrets":** führt zusammen — **Backup-Keys**, **Pepper**, **`MFA_ENC_KEY`**, `AUTH_SECRET`. Regeln: **niemals** im selben Bucket wie die verschlüsselten Artefakte; **Verlust des Keys = Verlust der Wiederherstellbarkeit**.
|
|
- ⚠ **Umgebungs-Secret-Kohärenz beim Restore:** Pepper und `MFA_ENC_KEY` stehen **nicht** im per-Mandant-Artefakt. Ein Restore in eine Umgebung mit **anderem** Pepper/`MFA_ENC_KEY` bricht **alle** Passwort-/MFA-Prüfungen. Restore daher nur in eine Umgebung mit **passenden** Secrets (oder Passwort-/MFA-Reset einplanen).
|
|
- **Später (Phase 2):** Upgrade-Pfad auf **HashiCorp Vault** oder Provider-KMS (Rotation/Audit/Trennung) — bewusst offen, nicht jetzt bauen.
|
|
|
|
**Aufgabenteilung der Lanes:** die **Backup-Lane** ruft die Verschlüsselung auf (pgBackRest-Repo-Key bzw. `age`-Recipient); die **Härtungs-Lane** stellt Host-Encryption (LUKS) bereit und verwaltet/rotiert die Keys + das Secrets-Register.
|
|
|
|
**Aufbewahrung & Test:**
|
|
- **Retention** je Schicht dokumentieren (DR-WAL/Base, Per-Tenant-Snapshots, DSGVO-Exporte) — dem DSB vorzulegen.
|
|
- **Restore-Test** regelmäßig + dokumentiert (der Prod-Runbook fordert das bereits für `pgdata`).
|
|
|
|
## 10. Governance-/Sicherheitskontrollen (Zusammenfassung)
|
|
| Operation | Wer | Zusatzschutz |
|
|
|---|---|---|
|
|
| Cluster-PITR | Betreiber/Ops | Offsite-Zugriff, 4-Augen empfohlen |
|
|
| Tenant-Restore | Plattform-Full-Admin | **MFA-Step-up**, getippte Bestätigung, Mandant-Sperre, Pre-Restore-Snapshot, Audit |
|
|
| DSGVO-Export | Mandanten-Admin **oder** Betreiber | signierter Link, Audit |
|
|
| Löschung Person | Mandanten-Admin (Verantwortlicher) | Anonymisierungs-Regeln, Löschnachweis, Audit |
|
|
| Mandanten-Löschung | Plattform-Full-Admin | **MFA-Step-up**, getippte Bestätigung, Löschnachweis, MinIO + globale Identity-Aufräumung |
|
|
|
|
## 11. Phasen & offene Entscheidungen
|
|
**Phasen:**
|
|
1. Schicht A (pgBackRest/wal-g + PITR + verschlüsselt) — unabhängig, **sofort** möglich.
|
|
2. Traversierungs-Engine über `TENANT_MODELS` (Export/Restore-Kern) — **nach WS0**.
|
|
3. Betreiber-Portal-Restore (Worker-Job + Kontrollen).
|
|
4. DSGVO-Export (per-Mandant + per-Person).
|
|
5. DSGVO-Löschung (Anonymisierung vs. Hard-Delete + Löschnachweis + Tombstone-on-Restore).
|
|
|
|
**Offene Entscheidungen (für PM/DSB):**
|
|
- Retention-Fristen je Schicht (DSB-Vorgabe).
|
|
- Welche Tabellen/Felder bei Personen-Löschung **anonymisiert** (Nachweispflicht) vs. **hart gelöscht** werden — eine explizite **Feld-Klassifikation** ist nötig.
|
|
- Aufbewahrung/Weg der DSGVO-Export-Pakete (Ablauf des signierten Links).
|
|
- Ob per-Mandant-Snapshots als DSGVO-Portabilitätsformat genügen oder ein zusätzliches „menschenlesbares" Format nötig ist.
|