diff --git a/docs/vs-nfd/20-massnahmenplan.md b/docs/vs-nfd/20-massnahmenplan.md index 0d62aa9..6bb922e 100644 --- a/docs/vs-nfd/20-massnahmenplan.md +++ b/docs/vs-nfd/20-massnahmenplan.md @@ -189,8 +189,9 @@ nach sich zieht. Backup nur lokal · 3 AT · #227 - [x] **Sicherheitsdokumentation**: Architektur, Datenflüsse, Netzplan, Ports/Dienste, Vertrauensgrenzen · 4 AT · #228 → `60-sicherheitsdokumentation.md` -- [ ] **Betriebshandbuch**: Installation (inkl. Airgap), Update, Backup/Restore, - Löschung und Vernichtung, Rollentrennung · 4–5 AT · #229 +- [x] **Betriebshandbuch**: Installation (inkl. Airgap), Update, Backup/Restore, + Löschung und Vernichtung, Rollentrennung · 4–5 AT · #229 → `70-betriebshandbuch.md` + (Airgap-Kapitel verweist auf offene #218–#221) - [ ] **Zuarbeit IT-Grundschutz** APP.3.1 und CON.11.1, je Anforderung „Produkt / Betreiber / nicht anwendbar" · 3–4 AT · #230 - [ ] **Restrisikoliste** mit bewusst offenen Punkten · 1 AT · #231 diff --git a/docs/vs-nfd/70-betriebshandbuch.md b/docs/vs-nfd/70-betriebshandbuch.md new file mode 100644 index 0000000..061ec78 --- /dev/null +++ b/docs/vs-nfd/70-betriebshandbuch.md @@ -0,0 +1,210 @@ +# Betriebshandbuch (VS-NfD, Issue #229) + +Zweck: der Betreiber führt die Instanz ohne uns — auch an dem Tag, an +dem etwas ausfällt. Dieses Handbuch bündelt die betrieblichen +Prozeduren und verweist für Schrittfolgen auf die maßgeblichen +Runbooks im Repository, statt sie zu duplizieren. Das Kapitel mit dem +größten VS-Gewicht ist **§5 Löschung und Vernichtung**; es baut auf der +vollständigen Kopienliste der Sicherheitsdokumentation auf +(`60-sicherheitsdokumentation.md` §5). + +**Belegstufen.** Jede Prozedur nennt am Ende ihren Erprobungsstand: + +- ✅ _erprobt_ — vom Autor bzw. automatisiert mindestens einmal real + ausgeführt, mit Beleg. +- ⚠️ _Mechanik vorhanden, nicht geprobt_ — implementiert und getestet, + aber noch nie im Ernstfall/als Übung durchgespielt. +- ⏳ _offen_ — kommt mit dem genannten Issue. + +--- + +## 1 Installation + +Referenz: `deploy/stages.md` (maßgebliche Schrittfolge), +`docs/self-hosting/README.md`. + +1. Host-Voraussetzungen: Docker + Compose, ein Reverse Proxy mit TLS + (oder das mitgelieferte `caddy`-Compose-Profil). +2. `deploy/compose/docker-compose.yml` + `.env` (Vorlage + `.env.example`; **niemals** eine echte `.env` ins Repo — CI-Zaun + #198) auf den Host bringen; `COMPOSE_PROJECT_NAME`, Ports + (`WEB_PORT`/`API_PORT`/`COLLAB_PORT`), `POSTGRES_PASSWORD`, Secrets + setzen. +3. `docker compose pull && docker compose up -d` — die api wendet + Migrationen beim Start selbst an (`MIGRATE_ON_START`); es gibt + keinen separaten Migrationsschritt. +4. Erststart: der Setup-Wizard (Issue #80) legt das Admin-Konto an und + verriegelt sich danach dauerhaft; bis zum Abschluss ist nur + `/setup/*` erreichbar. +5. Reverse Proxy: `/` → web, `/api` → api, `/collab` → collab mit + WebSocket-Upgrade (Portliste: Sicherheitsdokumentation §2). +6. Für VS-NfD-Betrieb anschließend die Referenzkonfiguration aus dem + Härtungsleitfaden (`50-haertungsleitfaden.md`) anwenden und mit + dessen Checkliste verifizieren. + +Belegstufe: ✅ erprobt — die Stages Test/Int/Prod auf ONE sind exakt +nach dieser Prozedur aufgesetzt und laufen produktiv (`deploy/stages.md` +dokumentiert die realen Instanzen). + +**Airgap-/Offline-Variante:** ⏳ offen — Mirror-Verfahren (#218), +netzloser Build (#219), Testlauf in isolierter Umgebung (#220), +Offline-Update-Pfad (#221); Meilenstein M28. Bereits vorhanden als +Grundlage: alle Dritt-Images digest-gepinnt (#203), ein authoritativer +Node-Pin (#236), SBOMs je Release (#202). + +## 2 Update und Rollback + +Referenz: `docs/architecture/operations.md` §Update strategy, +`deploy/stages.md`. + +- **Stages:** Merge auf `main` → CD baut Images, deployt Test, führt + Smoke-Tests aus, promotet Int. CD synct **keine** Compose-Dateien — + Compose-Änderungen werden von Hand auf den Host übernommen, sonst + räumt `--remove-orphans` manuell ergänzte Container ab. + Belegstufe: ✅ erprobt (läuft bei jedem Merge; zuletzt CD-Lauf 568). +- **Produktion:** ausschließlich getaggte Releases. `git tag vX.Y.Z` + baut die Versions-Images und führt die Update-Simulation als Gate + aus; `git tag prod-vX.Y.Z-initial` pinnt die Version in der + Prod-`.env` und rollt aus (readyz-Poll). Migrationen laufen beim + api-Start; **Rückwärtsmigrationen gibt es nicht** — Rollback setzt + deshalb ein DB-kompatibles Vorgängerimage voraus (Migrations-Kaveat; + im Zweifel Restore aus dem Backup-Set der Vorversion). + Belegstufe: ✅ erprobt (alle Prod-Versionen bis v0.12.0 so + ausgerollt). +- **Rollback:** `git tag prod-v-rollback1` pinnt die Vorversion + zurück. Belegstufe: ⚠️ Mechanik vorhanden, nicht geprobt — ein realer + Rollback war bisher nie nötig; die Übung steht aus. +- **Dritt-Image-Digest heben:** Prozedur `deploy/stages.md` §5a + (imagetools inspect → Compose-Referenz ändern → CI bestätigt → Stage- + Composes von Hand nachziehen → `docker inspect` verifiziert). + Belegstufe: ✅ erprobt (Rollout #203 am 31.07.2026). +- **Offline-Update:** ⏳ offen (#221, M28). + +## 3 Backup und Restore + +Referenz: `docs/operations/restore-runbook.md` (maßgeblich), +`deploy/backup-basel.md`, ADR 0015/0026. + +- Nächtlich erzeugt der backup-Sidecar ein konsistentes Restore-Set + (`pg_dump -Fc` + Tar der Volumes `uploads`/`plugins`) auf dem + `backups`-Volume; Retention `BACKUP_RETENTION_DAYS` (Default 30, + Test/Int 7). Status in `status.json`, Fehler alarmieren per Mail + direkt via SMTP (bewusst nicht über die api — sie könnte das kaputte + Teil sein). +- **Zielbeschränkung (#192, ADR 0026):** Fernziele (rsync-Spiegel, + WebDAV) funktionieren nur gegen Hosts der **Deploy-Allowlist** + `BACKUP_ALLOWED_TARGETS`; eine leere Allowlist schaltet alle + Fernziele hart ab. Die Allowlist ist bewusst NICHT über die Admin-UI + änderbar (Rollentrennung, §6). +- **Restore:** `deploy/backup/restore.sh` fährt den Stack kontrolliert + herunter, spielt Dump + Volumes zurück und startet neu; Details und + Totalverlust-Szenario im Runbook. +- **Geprobter Restore:** ✅ erprobt — ein automatischer monatlicher + Restore-Drill (`drill-*`-Tag → `drill.yml`) stellt das jüngste + Prod-Set in einer Wegwerf-Umgebung wieder her und prüft Inhalte; das + Protokoll jedes Laufs steht als Kommentar an Issue #98. + Backups sind unverschlüsselt by design — Datenträgerschutz ist + Plattformsache (Abgrenzungserklärung). + +## 4 Wartungsjobs (Scheduler) + +Alle Jobs laufen in der api (in-app Scheduler, `jobs`-Tabelle), sind im +Site-Admin-Systempanel sichtbar und dort manuell auslösbar (auditiert +als `job.triggered`). Aktueller Bestand (9): + +| Job | Rhythmus | Wirkung | +| -------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------- | +| `trash-purge` | täglich | endgültiges Löschen abgelaufener Papierkorb-Seiten **und** -Teiche (`trash.retentionDays`, #31/#193) | +| `orphan-file-sweep` | täglich | verwaiste Uploads entfernen (#194); trägt nachts auch den SHA-256-Backfill (#199) | +| `page-compaction` | stündlich | `page_updates`-Log in den Zustand mergen | +| `version-thinning` | täglich | automatische Versions-Snapshots ausdünnen | +| `audit-retention` | täglich | `audit_log` nach `audit.retentionDays` beschneiden (#196), Lücke selbst auditiert (`audit.pruned`) | +| `conversion-payload-prune` | täglich | Import-/Export-Payloads fertiger Jobs nullen (#233) | +| `mail-outbox-retention` | täglich | SENT/endgültig FAILED Outbox-Zeilen löschen (#234) | +| `data-export-purge` | stündlich | abgelaufene DSGVO-Datenexporte entfernen (#68) | +| `notification-digest` | alle 15 min (Versand nach Nutzer-Präferenz) | Benachrichtigungs-Digests versenden | + +Der e2e-Zaun `apps/web/e2e/system.spec.ts` pinnt diese Zahl — ein +neuer Job ohne Handbuch-/Zaun-Anpassung wird rot. + +## 5 Löschung und Vernichtung + +Grundsatz: „Gelöscht" heißt in Dorfteich erst dann gelöscht, wenn alle +Kopien aus `60-sicherheitsdokumentation.md` §5 erreicht sind. Die +Tabelle nennt je Inhaltstyp, was die Löschung tut, welche Kopien sie +erreicht und was **stehen bleibt**. + +| Vorgang | Wirkung | erreichte Kopien | Rückstände / Fristen | +| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Seite in den Papierkorb | Soft-Delete (`deleted_at`), aus Suchindex entfernt (#195) | Suchvektor sofort | Inhalt vollständig vorhanden, wiederherstellbar bis Purge | +| Seiten-Purge (automatisch nach `trash.retentionDays`, Default 30, oder manuell) | löscht Zustand, Update-Log, Content-Cache, Versionen, Kommentare, Attachments (Bytes + Quota), Watches; Kinder rücken auf | DB-Zeilen + `uploads`-Bytes | `page_links.target_slug` bleibt bewusst (#235, Restrisiko I-24); Backups/Spiegel bis Ablauf ihrer Retention; Export-/Endgeräte-Kopien organisatorisch | +| Teich-Purge (#193, automatisch/manuell, Site-Admin) | löscht alle Seiten samt Kaskade, Labels, Grants, Nutzungszähler, Plugin-Opt-ins, Conversion-Jobs, Files | wie oben, teichweit | wie oben | +| Nutzer löschen / DSGVO | Konto löschen bzw. Autorschaft pseudonymisieren (`user.pseudonymized`) | Identitätsdaten | von ihm erstellte Inhalte gehören dem Teich | +| Export-Payloads | `conversion-payload-prune` nullt `input`/`result` fertiger Jobs (Default 30 d) | DB | Job-Zeile bleibt für Status/Audit | +| Mail-Kopien | `mail-outbox-retention` löscht SENT/endgültig FAILED (Default 30 d) | DB | zugestellte Mails beim Empfänger (Restrisiko I-23) | +| Backup-Sets | Retention des Sidecars bzw. des Spiegels | `backups`-Volume, Spiegel | ein gelöschter Inhalt lebt maximal bis zum Ablauf der längsten Backup-Retention weiter — bei sofortigem Vernichtungsbedarf Sets manuell löschen (Host-Zugriff) und Spiegel bereinigen | +| Audit-Trail | `audit-retention` (Default 365 d) | DB | Metadaten, nie Inhalt | + +**Sofortige Vernichtung einzelner Inhalte** (über die Fristen hinaus): +Seite manuell purgen (Papierkorb → endgültig löschen), danach auf dem +Host die Backup-Sets der Aufbewahrungskette löschen bzw. den Spiegel +bereinigen und Endgeräte-/Exportkopien organisatorisch einsammeln. +Belegstufe Purge-Pfade: ✅ erprobt (laufen täglich produktiv; Purge- +Semantik durch Tests gepinnt). Belegstufe „Backup-Kette manuell +vernichten": ⚠️ nicht geprobt. + +**Außerbetriebnahme einer Instanz:** `docker compose down -v` entfernt +Container und **alle benannten Volumes** (`db-data`, `uploads`, +`plugins`, `secrets`, `backups`, Caddy-Volumes); anschließend Spiegel- +Bestände löschen und Host-Datenträger nach Betreiber-Vorgabe +vernichten — die physische Vernichtung ist Plattformsache +(Abgrenzungserklärung). Belegstufe: ✅ erprobt für den Stack-Teil (der +stillgelegte Alt-VPS wurde so zurückgebaut; Datenträger dort noch als +Rollback-Reserve vorhanden). + +## 6 Rollentrennung + +| Aufgabe | braucht | +| --------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- | +| Instanz-Settings, Nutzer-/Quota-Verwaltung, Plugins, Wartungsjobs anstoßen, Teich-Purge, Audit-Panel | **Site-Admin** (App-Rolle) | +| Deploy, `.env`/Compose ändern, Backup-Allowlist, Secrets-Volume, Restore, Backup-Sets vernichten, DB-Direktzugriff, Log-Collector | **Plattformzugriff** (Host) | +| Release nach Prod | Plattformzugriff + Freigabeprozess (Tag) | + +Was ein Site-Admin **nicht** kann (bewusste Grenzen): + +- die Backup-Zielliste ändern oder Fernziele aktivieren + (`BACKUP_ALLOWED_TARGETS` ist Deploy-Konfiguration, ADR 0026); +- Secrets lesen oder setzen, die im Secret-Store/der `.env` liegen + (die Settings-Tabelle trägt nie Secrets, security.md §Secrets); +- Deploy-/Composeänderungen, Image-Versionen, Migrationen auslösen; +- den Audit-Trail editieren (nur lesen; Retention läuft als Job und + protokolliert sich selbst); +- Backups herunterladen ist möglich (In-App-Restore-Pfad, #103), aber + ihre Vernichtung auf Host/Spiegel nicht. + +Zu beachten: ein Site-Admin **liest** per Rollen-Bypass jeden Inhalt +der Instanz (permissions.md) — die Trennung „wer darf Inhalte sehen" +von „wer betreibt die Plattform" ist damit App-seitig bewusst NICHT +absolut; wo das nicht tragbar ist, ist es organisatorisch zu regeln +(Vier-Augen-Prinzip bei Vergabe der Site-Admin-Rolle). Der Lesetrail +für eingestufte Inhalte (M29, #222–#225) macht solche Zugriffe +nachvollziehbar. + +## 7 Monitoring und Störung + +Referenz: `deploy/monitoring.md`, `docs/architecture/operations.md` +§Health & monitoring. + +- `GET /api/v1/readyz` prüft DB, Migrationen, Konverter, Renderer, + Backup-Status; `healthz` je Container. Externe Überwachung der + Referenzinstanzen: Uptime-Kuma mit Alarmierung. +- Logs: alle Dienste loggen JSON auf stdout (Docker json-file mit + Rotation); Audit-Ereignisse mit `msg`-Präfix `audit: ` und + `severity`-Feld — Weiterleitung an SIEM/Syslog übernimmt der + Collector des Betreibers (Katalog: + `docs/architecture/audit-events.md`, v1.1, mit + Kompatibilitätsversprechen). +- Integritätsalarm: `file.integrity_failed` (critical) = Download-Hash + ≠ Upload-Hash → Objekt als manipuliert/korrupt behandeln, Datei aus + Backup-Set wiederherstellen (Runbook), erneut laden; der + Audit-Eintrag trägt beide Hashes.