docs(PROJ-66): Backup & Restore Runbook schreiben
docs/BACKUP_RESTORE_RUNBOOK.md deckt beide Sicherungsebenen ab: App-eigenes CLI-Backup (archivmail backup/restore, PROJ-66) und Infrastruktur-Ebene (PBS + Sync auf Zweitserver, pct restore). Enthaelt Bitwarden-Keyfile- Escrow-Anleitung, Pflicht-Verifikationsschritte (reconcile/reindex/ Stichprobe), Rotation-vs-DSGVO-Hinweis und den offenen Punkt "noch kein Restore-Test durchgefuehrt" als groesste verbleibende Luecke. AC 8 aus PROJ-66 damit erfuellt; AC 6 (Restore-Test) und AC 7 (Alerting) bleiben offen.
This commit is contained in:
@@ -0,0 +1,170 @@
|
|||||||
|
# Backup & Restore Runbook – archivmail
|
||||||
|
|
||||||
|
Bezug: `features/PROJ-66-backup-strategie.md` (Spec + Entscheidungsverlauf),
|
||||||
|
`docs/GOBD_DSGVO_CHECKLIST.md` Punkt 7 (Löschsperre) und 15 (Mandantentrennung).
|
||||||
|
Ziel: belegen können, dass ein Totalausfall von Store, Keyfile oder
|
||||||
|
PostgreSQL nicht zu unwiederbringlichem Mail-Verlust führt — und dass ein
|
||||||
|
Restore im Ernstfall tatsächlich funktioniert, nicht nur theoretisch existiert.
|
||||||
|
|
||||||
|
Gilt für: `archivmail backup`/`archivmail restore` (PROJ-66, App-eigenes
|
||||||
|
CLI-Backup) und ergänzend die Proxmox Backup Server (PBS) + Sync-auf-
|
||||||
|
Zweitserver-Sicherung auf Infrastruktur-Ebene.
|
||||||
|
|
||||||
|
## Grundsatz
|
||||||
|
|
||||||
|
Store, Keyfile und PostgreSQL-Metadaten sind **nur gemeinsam** nutzbar:
|
||||||
|
- Store ohne DB: Dateien vorhanden, aber keine Zuordnung zu Mail/Tenant.
|
||||||
|
- Keyfile ohne Store: irrelevant, nichts zu entschlüsseln.
|
||||||
|
- DB ohne Keyfile: Metadaten da, Mail-Inhalte nicht mehr lesbar (GoBD-Verstoß).
|
||||||
|
|
||||||
|
Ein Backup, das nur eine dieser drei Komponenten sichert, ist wertlos. Ein
|
||||||
|
Backup, das nie zurückgespielt wurde, ist nicht verifiziert — siehe
|
||||||
|
"Regelmäßiger Restore-Test" unten.
|
||||||
|
|
||||||
|
## Zwei unabhängige Sicherungsebenen
|
||||||
|
|
||||||
|
1. **Infrastruktur-Ebene (PBS + Sync auf Zweitserver)** — läuft vom
|
||||||
|
Proxmox-Host aus, sichert den kompletten Container (Store+Keyfile+
|
||||||
|
Config+PostgreSQL-Datendir+Audit-Log in einem Snapshot). Von innerhalb
|
||||||
|
des Containers nicht sichtbar/steuerbar. Siehe Memory
|
||||||
|
`project_backup_infra_pbs` für CLI-Befehle (`vzdump`, `pct restore`).
|
||||||
|
2. **App-eigenes Backup (`archivmail backup`/`restore`)** — ergänzt Ebene 1
|
||||||
|
um einen anwendungskonsistenten `pg_dump` (statt reinem Crash-Consistency-
|
||||||
|
Snapshot) und einen selektiven, von PBS unabhängigen Restore-Weg.
|
||||||
|
|
||||||
|
Keine der beiden Ebenen ersetzt die andere. Ein vollständiger Restore-Test
|
||||||
|
sollte mindestens einmal **beide** Wege durchlaufen haben (siehe unten).
|
||||||
|
|
||||||
|
## Backup erstellen
|
||||||
|
|
||||||
|
```bash
|
||||||
|
archivmail backup --config /etc/archivmail/config.yml --dest <ziel> --keep 14
|
||||||
|
```
|
||||||
|
|
||||||
|
- `<ziel>` MUSS auf einem vom Produktivsystem physisch getrennten Ziel
|
||||||
|
liegen (zweiter Host, NAS, o.ä.) — siehe PROJ-66-Spec, Abschnitt
|
||||||
|
"Backup-Ziel: On-Site vs. Off-Site". Ein Backup auf derselben Platte
|
||||||
|
schützt nicht vor Hardware-Ausfall.
|
||||||
|
- Schreibt `<ziel>/<timestamp>/`: `postgres.dump` (`pg_dump -Fc`), `store/`
|
||||||
|
(Hardlink-erhaltende Kopie, PROJ-65-Tenant-Ordner bleiben ohne
|
||||||
|
Speicherplatz-Verdopplung erhalten), `keyfile`, `config.yml`, `audit.log`
|
||||||
|
(best-effort).
|
||||||
|
- `--keep N` behält die letzten N Backup-Verzeichnisse, löscht ältere —
|
||||||
|
**nur nach erfolgreichem Lauf**. Ein fehlgeschlagener Lauf lässt den
|
||||||
|
partiellen Ordner stehen und rotiert nichts weg (Diagnose-Möglichkeit,
|
||||||
|
kein Verlust eines guten alten Backups).
|
||||||
|
- Cron-Aktivierung: `deploy/cron.d/archivmail` enthält eine auskommentierte
|
||||||
|
Beispielzeile. Erst aktivieren, wenn ein konkretes `--dest`-Ziel feststeht.
|
||||||
|
|
||||||
|
## Keyfile-Escrow (Bitwarden)
|
||||||
|
|
||||||
|
Zusätzlich zum Backup: das Keyfile als unabhängige Kopie in Bitwarden
|
||||||
|
hinterlegen (Secure Note, eigene Collection mit eingeschränktem Zugriff —
|
||||||
|
das Keyfile entschlüsselt das **gesamte Archiv aller Mandanten**).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat /etc/archivmail/keyfile
|
||||||
|
# → Wert 1:1 in eine neue Bitwarden Secure Note kopieren, Metadaten
|
||||||
|
# ergänzen (Server/Installation, Datum, wer hinterlegt hat).
|
||||||
|
```
|
||||||
|
|
||||||
|
Zweck: unabhängiger Wiederherstellungsweg, falls PBS/Snapshot-Infrastruktur
|
||||||
|
selbst nicht verfügbar ist. Ersetzt nicht das App-Backup — beide parallel
|
||||||
|
pflegen. Bei künftiger Keyfile-Rotation (aktuell nicht vorgesehen) den
|
||||||
|
Bitwarden-Eintrag im selben Schritt aktualisieren.
|
||||||
|
|
||||||
|
## Restore durchführen
|
||||||
|
|
||||||
|
**Vorbereitung:**
|
||||||
|
1. Zielsystem: frischer Server/Container oder vorhandenes System im
|
||||||
|
Wartungsmodus. `archivmail`/`archivmail-web` müssen gestoppt sein —
|
||||||
|
`archivmail restore` stoppt/startet die Dienste NICHT selbst.
|
||||||
|
2. Backup-Verzeichnis identifizieren: `<ziel>/<timestamp>/`.
|
||||||
|
|
||||||
|
**Durchführung:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl stop archivmail archivmail-web
|
||||||
|
|
||||||
|
archivmail restore --config /etc/archivmail/config.yml \
|
||||||
|
--source <ziel>/<timestamp> [--force]
|
||||||
|
```
|
||||||
|
|
||||||
|
- Ohne `--force`: bricht ab, wenn `store_path`/Keyfile bereits Inhalt haben
|
||||||
|
— Schutz gegen versehentliches Überschreiben eines laufenden Systems.
|
||||||
|
`--force` nur auf einem bewusst freigeräumten/frischen Zielsystem nutzen.
|
||||||
|
- `--skip-db`: überspringt `pg_restore`, falls die Datenbank bereits separat
|
||||||
|
wiederhergestellt wurde (z.B. via PBS-Snapshot der DB-Ebene).
|
||||||
|
- Stellt Store (Hardlinks erhalten), Keyfile, Config und PostgreSQL
|
||||||
|
(`pg_restore --clean --if-exists`) wieder her. Gibt am Ende die
|
||||||
|
Pflicht-Verifikationsschritte aus (siehe unten).
|
||||||
|
|
||||||
|
**Verifikation (Pflicht, nicht optional):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
systemctl start archivmail archivmail-web
|
||||||
|
# Health-Check wie im Standard-Deploy-Workflow (Backend ✓, Frontend ✓)
|
||||||
|
|
||||||
|
archivmail reconcile --days 7 # PROJ-52: Zählvergleich Store vs. DB muss aufgehen
|
||||||
|
archivmail reindex # Manticore aus Store+DB neu aufbauen
|
||||||
|
# Stichprobe: 3-5 zufällige Mails über die UI öffnen und lesbar entschlüsseln
|
||||||
|
# lassen — beweist, dass Keyfile+Store+DB zueinander passen.
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore-Ergebnis (Mail-Zahl, Zeitstempel, Dauer, welche Verifikationsschritte
|
||||||
|
liefen) dokumentieren — Prinzip "Zählen, Prüfen, Belegen" wie im
|
||||||
|
Migrations-Runbook.
|
||||||
|
|
||||||
|
## Restore aus einem PBS-Snapshot (Infrastruktur-Ebene)
|
||||||
|
|
||||||
|
Auf dem Proxmox-**Host** (nicht im Container):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pct restore <neue-id> <pbs-storage>:backup/ct/<quell-id>/<timestamp> --storage <ziel-storage>
|
||||||
|
```
|
||||||
|
|
||||||
|
Restored als neuer Container mit `<neue-id>` — der Quell-Container bleibt
|
||||||
|
unangetastet. Danach dieselbe Verifikation wie oben (`reconcile`, `reindex`,
|
||||||
|
Stichprobe) im neuen Container durchführen. PostgreSQL kam dabei aus einem
|
||||||
|
Crash-Consistency-Snapshot, nicht aus einem `pg_dump` — WAL-Replay beim
|
||||||
|
Start ist der Normalfall, sollte aber beim Test explizit beobachtet werden
|
||||||
|
(`journalctl -u postgresql` auf Recovery-Meldungen prüfen).
|
||||||
|
|
||||||
|
## Regelmäßiger Restore-Test
|
||||||
|
|
||||||
|
Ein Backup, das nie zurückgespielt wurde, ist nicht verifiziert. Empfehlung:
|
||||||
|
mindestens vierteljährlich einen vollständigen Restore auf einen isolierten
|
||||||
|
Test-Container durchführen (**nicht auf 131/132!**) — einmal über
|
||||||
|
`archivmail restore` (App-Backup) und mindestens einmal über `pct restore`
|
||||||
|
(PBS-Snapshot), jeweils mit vollständiger Verifikation. Bislang (Stand
|
||||||
|
2026-07-05) wurde noch kein Restore-Test durchgeführt — das ist die
|
||||||
|
kritischste offene Lücke in der Backup-Strategie, nicht das Fehlen eines
|
||||||
|
Backup-Ziels.
|
||||||
|
|
||||||
|
## Rotation vs. DSGVO-Löschung
|
||||||
|
|
||||||
|
Backup-Rotation (`--keep N`, bzw. PBS-Retention) ist unabhängig von der
|
||||||
|
GoBD-Aufbewahrungsfrist der Mails selbst (PROJ-51). Eine in Produktion
|
||||||
|
bereits DSGVO-gelöschte Mail (PROJ-50) kann in einem älteren Backup noch
|
||||||
|
enthalten sein. Rotation muss kurz genug sein, dass gelöschte Daten nach
|
||||||
|
spätestens N Zyklen auch aus Backups verschwinden — Zeitraum mit
|
||||||
|
Datenschutzbeauftragtem/Nutzer abstimmen, kein rein technisches Thema.
|
||||||
|
|
||||||
|
## Monitoring / Alerting (noch offen)
|
||||||
|
|
||||||
|
Größtes Risiko bei Cron-Backups: der Job schlägt still fehl, niemand merkt
|
||||||
|
es bis zum Ernstfall (Lehre aus PROJ-58: Cron-Zeilen fehlten wochenlang
|
||||||
|
trotz aktivem Code). Aktuell **nicht gebaut**: ein sichtbarer Alarm, wenn
|
||||||
|
das letzte erfolgreiche Backup älter als ein Schwellenwert ist. Bis dahin:
|
||||||
|
Log-Datei des Backup-Cron-Jobs (`/var/log/archivmail/backup.log`, sobald der
|
||||||
|
Cron aktiviert ist) manuell im Rahmen der regulären Betriebs-Checks prüfen.
|
||||||
|
|
||||||
|
## Verweis
|
||||||
|
|
||||||
|
- `features/PROJ-66-backup-strategie.md` — vollständige Spec, Optionsabwägung,
|
||||||
|
QA-Ergebnisse.
|
||||||
|
- `docs/MIGRATION_RUNBOOK.md` — Vorbild für den "Zählen, Prüfen, Belegen"-Stil.
|
||||||
|
- Memory `project_backup_infra_pbs` — PBS/Sync-Infrastruktur-Details,
|
||||||
|
`vzdump`/`pct restore`-Befehle.
|
||||||
|
- Memory `project_132_teilproduktiv` — 132 ist kein reiner Testserver,
|
||||||
|
Backup-Sorgfalt gilt dort genauso wie auf 131.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# PROJ-66: Backup-Strategie für archivmail (Produktiv + Teilproduktiv)
|
# PROJ-66: Backup-Strategie für archivmail (Produktiv + Teilproduktiv)
|
||||||
|
|
||||||
**Status:** In Review (BUG-1/BUG-2 aus QA-Runde 1 gefixt, Re-Test steht aus)
|
**Status:** Deployed (nur CLI-Kommandos `backup`/`restore` auf 192.168.1.131 — Cron-Aktivierung, Alerting, Bitwarden-Escrow und PBS-Restore-Verifikation bleiben offen, siehe "## Deployment")
|
||||||
**Erstellt:** 2026-07-04
|
**Erstellt:** 2026-07-04
|
||||||
|
|
||||||
## Problem / Ausgangslage
|
## Problem / Ausgangslage
|
||||||
@@ -392,6 +392,10 @@ Anforderungen:
|
|||||||
8. Ein Backup-Runbook (analog `docs/MIGRATION_RUNBOOK.md`) existiert unter
|
8. Ein Backup-Runbook (analog `docs/MIGRATION_RUNBOOK.md`) existiert unter
|
||||||
`docs/BACKUP_RESTORE_RUNBOOK.md` mit Schritt-für-Schritt-Anleitung für
|
`docs/BACKUP_RESTORE_RUNBOOK.md` mit Schritt-für-Schritt-Anleitung für
|
||||||
Backup, Restore und Verifikation.
|
Backup, Restore und Verifikation.
|
||||||
|
**✓ Erfüllt (2026-07-05)** — `docs/BACKUP_RESTORE_RUNBOOK.md` angelegt,
|
||||||
|
deckt App-Backup (`archivmail backup`/`restore`) und Infrastruktur-Ebene
|
||||||
|
(PBS/`pct restore`) ab. AC 6 (Restore-Test) und AC 7 (Alerting) bleiben
|
||||||
|
davon unberührt weiterhin offen.
|
||||||
9. `update.sh`-Vollständigkeits-Check: Falls das Backup-Skript/der Cron-Job
|
9. `update.sh`-Vollständigkeits-Check: Falls das Backup-Skript/der Cron-Job
|
||||||
als Datei unter `deploy/` im Repo gepflegt wird, synct `update.sh` diese
|
als Datei unter `deploy/` im Repo gepflegt wird, synct `update.sh` diese
|
||||||
Datei nachweislich mit aus (Lehre aus PROJ-58, siehe Deploy-Vollständigkeits-
|
Datei nachweislich mit aus (Lehre aus PROJ-58, siehe Deploy-Vollständigkeits-
|
||||||
@@ -620,3 +624,72 @@ der Fehler tritt nur bei der zweiten+ Inode-Referenz auf.
|
|||||||
|
|
||||||
Re-Test von Punkt 1 (Build) und Punkt 5b (`-force` gegen befüllten Store)
|
Re-Test von Punkt 1 (Build) und Punkt 5b (`-force` gegen befüllten Store)
|
||||||
steht aus.
|
steht aus.
|
||||||
|
|
||||||
|
## QA Re-Test Runde 2 (2026-07-04, Testserver 132)
|
||||||
|
|
||||||
|
**Getestet gegen Commit 55131de (aktueller HEAD, `fix(PROJ-66): Build-Fehler
|
||||||
|
in printHelp() + -force überschreibt keine Hardlinks`).** Nur die beiden zuvor
|
||||||
|
fehlgeschlagenen Punkte erneut geprüft (übrige Punkte waren in Runde 1 bereits
|
||||||
|
PASS und wurden nicht wiederholt). Isolierter Build via `git archive HEAD` nach
|
||||||
|
`/tmp/qa66-build` auf 132, Funktionstest in `/tmp/qa66-test` (synthetischer
|
||||||
|
Store mit PROJ-65-Tenant-Hardlink, `ab/abcdef123.bin` ↔ `tenant_1/abcdef123.bin`,
|
||||||
|
Inode-geteilt, link count 2). Deploy-Repo `/opt/archivmail/_build` NICHT
|
||||||
|
verändert, echte Produktions-/Store-Daten und echte DB nicht berührt.
|
||||||
|
|
||||||
|
| # | Testpunkt | Runde 1 | Runde 2 |
|
||||||
|
|---|-----------|---------|---------|
|
||||||
|
| 1 | `CGO_ENABLED=0 go build -buildvcs=false ./cmd/archivmail/` | FAIL (BUG-1) | **PASS** — Build exit 0, Binary 21 MB erzeugt; `cmd_import.go:310` nutzt jetzt einfache Anführungszeichen statt Backticks |
|
||||||
|
| 5b | `restore -force` gegen befüllten Store mit Hardlinks (Normalfall) | FAIL (BUG-2) | **PASS** — Repro (2× Restore auf dasselbe Ziel, 2. Lauf mit `-force`) läuft jetzt exit 0 durch; Hardlink erhalten (Ziel-Inode identisch für `ab/` und `tenant_1/`, link count 2), Inhalt intakt |
|
||||||
|
|
||||||
|
**BUG-1** verifiziert gefixt: `printHelp()`-Raw-String schließt nicht mehr
|
||||||
|
vorzeitig, gesamtes Backend kompiliert fehlerfrei.
|
||||||
|
|
||||||
|
**BUG-2** verifiziert gefixt: `copyTreePreservingHardlinks()` entfernt vor
|
||||||
|
`os.Link()` eine bereits vorhandene Zieldatei (`os.Remove`, `os.IsNotExist`
|
||||||
|
ignoriert). Vorheriger `os.Link: file exists` tritt nicht mehr auf; `-force`
|
||||||
|
überschreibt einen mit Tenant-Hardlinks befüllten Store verlustfrei.
|
||||||
|
|
||||||
|
**Gesamtergebnis Re-Test: BESTANDEN.** Beide zuvor blockierenden Bugs sind
|
||||||
|
grün. CLI-Kommandos `backup`/`restore` funktional freigegeben. Nicht Teil
|
||||||
|
dieses Re-Tests (unverändert offen laut Implementation Notes): Cron/Timer
|
||||||
|
(AC 1), physische Trennung (AC 3), Backup-Verschlüsselung (AC 4),
|
||||||
|
Restore-Verifikation via echtem `reconcile` gegen realen Bestand (AC 6),
|
||||||
|
Alerting (AC 7), Runbook (AC 8), Bitwarden-Escrow (AC 10), PBS-Restore-Test
|
||||||
|
(AC 11). Empfehlung an devops-deploy: 55131de deployen und Status auf
|
||||||
|
Deployed setzen; die noch offenen ACs bleiben als Folgeschritte bestehen.
|
||||||
|
|
||||||
|
Testartefakte (`/tmp/qa66-build`, `/tmp/qa66-test` auf 132) nach Testende
|
||||||
|
entfernt und Entfernung verifiziert (`ls -d /tmp/qa66*` → NONE).
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
**2026-07-04, devops-deploy, Produktivserver 192.168.1.131**
|
||||||
|
|
||||||
|
Deploy via `bash /opt/archivmail/update.sh` auf 131 (Commit 55131de, bereits
|
||||||
|
auf origin/main). `update.sh` synct Backend-Binary, Frontend-Build,
|
||||||
|
systemd-Units und `/etc/cron.d/archivmail` (Wrapper-Skripte inklusive).
|
||||||
|
|
||||||
|
**Smoke-Test nach Deploy (kein echter Backup/Restore-Lauf gegen
|
||||||
|
Produktivdaten):**
|
||||||
|
|
||||||
|
| Prüfung | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| `archivmail help` zeigt `backup`/`restore` | PASS — beide Einträge korrekt gelistet, kein Syntax-/Parse-Fehler wie in QA-Runde 1 |
|
||||||
|
| `archivmail backup` ohne `-dest` | PASS — sauberer Fehler `backup: -dest is required`, Exit 1, kein Crash |
|
||||||
|
| Backend-Health (`systemctl is-active archivmail`, `GET /api/health`) | PASS — `active`, HTTP 200 |
|
||||||
|
| Frontend-Health (`systemctl is-active archivmail-web`, `GET :3000/`) | PASS — `active`, HTTP 200 |
|
||||||
|
| `/etc/cron.d/archivmail` — PROJ-66-Backup-Zeile | PASS — Zeile bleibt auskommentiert (`# 0 3 * * * root ... archivmail backup ...`), keine aktive Backup-Cron auf 131, wie gefordert |
|
||||||
|
|
||||||
|
Kein `archivmail backup`/`restore` gegen echte Store-/DB-Daten auf 131
|
||||||
|
ausgeführt — nur Hilfe-/Fehlerausgabe geprüft, da noch kein `-dest`-Ziel
|
||||||
|
(getrenntes Mount) und kein Freigabeprozess für einen echten Lauf feststehen.
|
||||||
|
|
||||||
|
**Offen / Folgeschritte (nicht Teil dieses Deploys):**
|
||||||
|
- AC 1: Cron-Zeile aktivieren, sobald `-dest`-Ziel (separates Mount) feststeht
|
||||||
|
- AC 3: physische Trennung des Backup-Ziels
|
||||||
|
- AC 4: Verschlüsselung des Backup-Archivs selbst
|
||||||
|
- AC 6: Restore-Verifikation via `reconcile` gegen realen Bestand
|
||||||
|
- AC 7: Alerting bei fehlgeschlagenem Backup-Lauf
|
||||||
|
- AC 8: Runbook für Restore-Prozess
|
||||||
|
- AC 10: Keyfile-Escrow in Bitwarden
|
||||||
|
- AC 11: Restore-Test gegen echten PBS-Snapshot
|
||||||
|
|||||||
Reference in New Issue
Block a user