diff --git a/features/INDEX.md b/features/INDEX.md index 9581417..b32f619 100644 --- a/features/INDEX.md +++ b/features/INDEX.md @@ -80,8 +80,9 @@ | PROJ-62 | Fix Cross-Tenant IDOR bei POP3-Konto-Löschung/-Import (Sicherheitsbug) | Deployed | [PROJ-62](PROJ-62-fix-pop3-tenant-idor.md) | 2026-06-25 | | PROJ-63 | Defensive Tenant-Scope-Härtung der Tenant-Verwaltungs-Endpunkte (FUND-2) | Deployed | [PROJ-63](PROJ-63-harden-tenant-admin-scope.md) | 2026-06-25 | | PROJ-64 | Session-Invalidation bei Passwort-Change + Datei-Permissions-Härtung (Security-Audit) | Deployed | [PROJ-64](PROJ-64-session-invalidation-file-permissions.md) | 2026-07-03 | -| PROJ-65 | Physische Tenant-Trennung im Storage-Layer | In Review | [PROJ-65](PROJ-65-physische-tenant-trennung.md) | 2026-07-04 | +| PROJ-65 | Physische Tenant-Trennung im Storage-Layer | Deployed | [PROJ-65](PROJ-65-physische-tenant-trennung.md) | 2026-07-04 | +| PROJ-66 | Backup-Strategie für Store, Keyfile, PostgreSQL (Produktiv + Teilproduktiv) | Planned | [PROJ-66](PROJ-66-backup-strategie.md) | 2026-07-04 | -## Next Available ID: PROJ-66 +## Next Available ID: PROJ-67 diff --git a/features/PROJ-65-physische-tenant-trennung.md b/features/PROJ-65-physische-tenant-trennung.md index c24620a..18325b6 100644 --- a/features/PROJ-65-physische-tenant-trennung.md +++ b/features/PROJ-65-physische-tenant-trennung.md @@ -1,7 +1,7 @@ --- id: PROJ-65 title: Physische Tenant-Trennung im Storage-Layer -status: In Review +status: Deployed created: 2026-07-04 --- @@ -247,8 +247,113 @@ PROJ-55/56. - `cmd/archivmail/cmd_status.go` (`checkStoragePermissions`) - `docs/GOBD_DSGVO_CHECKLIST.md` (Punkt 15, siehe separater Commit) -## QA Test Results -_To be added by /qa_ +## QA Test Results (2026-07-04, Testserver 192.168.1.132) + +**Build:** `CGO_ENABLED=0 go build ./cmd/archivmail/` auf 192.168.1.132 erfolgreich +(go1.24.4), 0 Fehler/Warnungen. Getestet mit dem frisch gebauten Binary +(`/tmp/archivmail-proj65`, nach Test entfernt). Hinweis: der laufende Dienst +auf 132 ist noch das alte 0.9.1-Binary ohne PROJ-65 — Produktiv-Deploy + +einmaliger Backfill sind Handoff an devops-deploy (siehe unten). + +**Testdaten-Hygiene:** Backfill wurde als QA-Test auf dem echten Test-Store +gefahren (51304 Mails), danach die erzeugten `store/tenant_/`-Verzeichnisse +und das Test-Binary wieder entfernt, da der laufende Dienst noch das alte Binary +ist (sonst inkonsistenter Zustand: Backfill-Links vorhanden, aber neue Mails ohne +Link). Kanonische Dateien nach Cleanup intakt verifiziert (Linkzähler zurück auf +1, Dateigröße unverändert). Keine echten Passwörter/Configs überschrieben; für +den 0755/0750-Test wurde eine temporäre Kopie der Config mit umgebogenem +`store_path` in einem `mktemp -d` genutzt, danach gelöscht. + +### Acceptance Criteria — Option B + +| # | Kriterium | Ergebnis | +|---|-----------|----------| +| B1 | store/ gehört archivmail-User, Modus 0700 | **PASS** — `/var/archivmail/store` = `700 archivmail:archivmail` | +| B2 | `archivmail status` prüft Storage-Permissions, warnt bei zu offenen Rechten | **PASS** — neuer Check `Storage-Rechte` erscheint in Text- und JSON-Ausgabe; 0700 → OK, 0755 + 0750 → WARN (nie Hard-Fail, Exit 0), 0700 → OK. Alle drei chmod-Fälle live verifiziert. | +| B3 | GoBD-Checkliste Punkt 15 umformuliert | **PASS** — Punkt 15 nennt PROJ-65, Hardlink-Dirs, Backfill, 0700-Überwachung, Defense-in-Depth-Einordnung | +| B4 | Messung Cross-Tenant-Dedup-Quote | **PASS/erledigt** — 8 von 51304 Mails mit >1 Tenant-Zuordnung (~0,016 %), bestätigt die Spec-Annahme "<1%" | + +### Acceptance Criteria — Option A + +| # | Kriterium | Ergebnis | +|---|-----------|----------| +| A1 | Hardlink-Erstellung bei Save (alle Dedup-Zweige) | **PASS (via Backfill verifiziert)** — Backfill legte 50015 Links an (== `COUNT(*) email_refs`), 0 Fehler. Save-Wiring code-reviewed (drei email_refs-INSERT-Stellen). Laufzeit-Verifikation der Save-Pfade steht mit Deploy noch aus (alter Dienst). | +| A2 | Delete berücksichtigt alle referenzierenden Tenants | **PASS (Hardlink-Semantik verifiziert)** — Entfernen des Tenant-Links ließ die kanonische Datei intakt (Linkzähler 1, Größe unverändert); `TenantsForMail()` erfasst Primär-Tenant + email_refs vor DB-Delete (code-reviewed). Voller Delete-API-Durchlauf nicht gefahren (kein Löschen echter archivierter Mails / GoBD). | +| A3 | Kein Speicherplatz-Mehrverbrauch (gleicher Inode) | **PASS** — Einzel-Tenant-Mail: Inode 32623 in Root + Tenant-Dir identisch, Linkzähler 2. Cross-Tenant-Mail (Tenants 1+3): Inode 33626 in Root + tenant_1 + tenant_3 identisch, Linkzähler 3. | +| A4 | Backup-/Restore erhält Hardlinks | **OFFEN** — nicht QA-prüfbar, Handoff an devops-deploy (`rsync -H`/`tar`), bleibt offener AC-Punkt. | +| A5 | Root-Pfad-Operationen unverändert kompatibel | **PASS** — `filePath()`/`Load()`/`Delete()` unverändert am Root-Pfad; Tenant-Links rein additiv, kein bestehender Code liest daraus. | +| A6 | Backfill-Subcommand idempotent | **PASS** — `migrate-tenant-dirs` in main.go registriert; erster Lauf linked=50015/errors=0, zweiter Lauf linked=0/errors=0 (idempotent). | + +### Tenant-Isolation / Sicherheit +- Tenant-Verzeichnisse werden mit **0700** angelegt (`MkdirAll(..., 0o700)`) — + live verifiziert (`drwx------ archivmail:archivmail` für tenant_1/2/3). Kein + Gruppen-/World-Zugriff, konsistent mit dem Defense-in-Depth-Ziel. +- Kein neuer Zugriffspfad in `internal/api/` — Hardlinks sind rein additiv, + DB-gestützte `tenantAccessAllowed()`-Kontrolle bleibt maßgeblich. Keine neue + IDOR-/Cross-Tenant-Angriffsfläche (kein per-ID-HTTP-Endpunkt hinzugekommen). +- `linkTenantDir()` ist best-effort (Warn-Log, kein Fehler-Return) — ein + Filesystem-Fehler kann Save/Import nicht zum Scheitern bringen. Korrekt für + eine additive Defense-in-Depth-Ebene. + +### Regression +Bestehende `archivmail status`-Checks (PostgreSQL, Manticore, Storage, +Encryption, Audit-Log, Retention) weiterhin unverändert funktional; der neue +`Storage-Rechte`-Check ist additiv und beeinflusst den Exit-Code nicht. + +### Bugs +Keine. Keine Blocker, keine Regressionen gefunden. + +### Handoff / offene Punkte (nicht QA-Blocker) +- **devops-deploy:** (1) Produktiv-Deploy des PROJ-65-Binaries, (2) danach + einmalig `archivmail migrate-tenant-dirs` ausführen, (3) Backup-Tool auf + Hardlink-Erhalt prüfen (AC A4, `rsync -H`/`tar`). +- Laufzeit-Verifikation der Save-/Delete-Hardlink-Pfade des *laufenden* Dienstes + erst nach Deploy sinnvoll (aktuell läuft noch das alte Binary). + +**Gesamtergebnis: 9 von 11 AC PASS, 2 OFFEN (A4 Backup-Handoff, A1/A2 +Laufzeit-Verifikation nach Deploy) — 0 Bugs. Empfehlung: freigeben für Deploy.** ## Deployment -_To be added by /deploy_ + +**2026-07-04, devops-deploy, Produktivserver 192.168.1.131** + +- Commit `a15fa37` gepusht nach `origin/main` (war lokaler HEAD, noch nicht auf + Remote). +- `bash /opt/archivmail/update.sh` auf 131 ausgeführt: Quellcode aktualisiert, + Backend gebaut, Frontend gebaut, Cron-Jobs eingespielt, systemd-Units + synchronisiert. Ergebnis: `Backend ✓ läuft`, `Frontend ✓ läuft`. +- **Backfill** `archivmail migrate-tenant-dirs --config /etc/archivmail/config.yml` + ausgeführt: `linked=0, errors=0`. Kein Bug — auf 131 haben alle 151 Bestandsmails + `tenant_id = NULL` und `email_refs` ist leer (0 Zeilen); Produktiv läuft aktuell + ohne aktive Mandanten-Zuordnung, anders als der Testserver 132 (dort 51304 Mails + mit Tenant-Zuordnung, dort lief der Backfill im QA-Test mit `linked=50015`). + Sobald auf 131 Mails einem Tenant zugeordnet werden (`emails.tenant_id` oder + `email_refs`), legt `Save()` die Hardlinks automatisch an; ein erneuter + `migrate-tenant-dirs`-Lauf ist jederzeit gefahrlos wiederholbar. +- **Smoke-Test:** + - `archivmail status` zeigt neuen Check `[OK] Storage-Rechte /var/archivmail/store + Modus 0700 — nur Owner-Zugriff`. + - Backend-Health `GET /api/health` → 200, Frontend `GET /` → 200, beide + systemd-Units `active`. + - Hardlink-Stichprobe (Inode-Vergleich bestehende Mail Root- vs. Tenant-Pfad) + **nicht durchführbar auf 131**, da mangels Tenant-Zuordnung keine + `store/tenant_/`-Verzeichnisse angelegt wurden (erwartetes Verhalten, + kein Fehlschlag). Die Hardlink-Semantik selbst wurde bereits im QA-Lauf auf + 132 mit echten Tenant-Daten verifiziert (Inode-Gleichheit Root/Tenant-Pfad, + Linkzähler korrekt), siehe QA Test Results oben. +- **Backup-Prozess-Check (AC A4):** Auf 131 existiert **kein** dediziertes + Backup-Tool/-Skript für `/var/archivmail/store` — weder in `/etc/cron.d/` + (nur `archivmail`-Cron mit OCR-Pause, Purge, Reindex-Backlog, Reconciliation, + keine Backup-Zeile), noch als systemd-Timer, noch als eigenständiges Skript + unter `/opt/archivmail` oder `/usr/local/bin`. Die Hardlink-Erhalt-Frage + (`rsync -H` vs. `tar` vs. naives `cp -r`) ist damit aktuell **gegenstandslos, + weil es noch keinen Store-Backup-Prozess gibt** — das ist ein eigenständiger, + vom PROJ-65-Deploy unabhängiger offener Punkt und bleibt hier als Hinweis + stehen, nicht selbst behoben (kein Backup-Skript angelegt). +- **Restrisiko/offene Punkte für spätere Sprints:** + 1. Store-Backup-Prozess für 131 fehlt komplett (nicht PROJ-65-Scope, aber im + Zuge dieses Deploys aufgefallen — sollte als eigenes Ticket nachgezogen + werden, inkl. Hardlink-Erhalt-Vorgabe für PROJ-65). + 2. Save-/Delete-Hardlink-Pfade des jetzt laufenden Produktiv-Binaries sind auf + 131 mangels Tenant-Zuordnung noch nicht mit echten Live-Daten beobachtet + worden (nur code-reviewed + auf 132 mit echten Daten verifiziert). diff --git a/features/PROJ-66-backup-strategie.md b/features/PROJ-66-backup-strategie.md new file mode 100644 index 0000000..1f18747 --- /dev/null +++ b/features/PROJ-66-backup-strategie.md @@ -0,0 +1,459 @@ +# PROJ-66: Backup-Strategie für archivmail (Produktiv + Teilproduktiv) + +**Status:** Planned +**Erstellt:** 2026-07-04 + +## Problem / Ausgangslage + +**Nachtrag (2026-07-04, nach Nutzer-Rückmeldung):** Auf Infrastruktur-Ebene +existiert bereits eine Sicherung: Die Container werden per **Snapshot auf +einen zweiten Server synchronisiert** und zusätzlich **per Proxmox Backup +Server (PBS) gezogen**. Das ist von innerhalb des Containers nicht sichtbar +(läuft vom Proxmox-Host aus) — deshalb kam die ursprüngliche Prüfung unten +("kein Backup-Prozess") zu einem unvollständigen Bild. Das entschärft die +zuvor als Blocker geführte Frage nach einem Off-Site-Ziel erheblich: PBS + +Sync auf zweiten Server ist bereits eine physisch getrennte Sicherung. + +Was das ändert und was offen bleibt, siehe Abschnitt "Bewertung der +Infra-Ebene" weiter unten. Der ursprüngliche Befund (App-Ebene: kein +`pg_dump`/rsync/Cron für archivmail selbst) bleibt als Kontext stehen, ist +aber durch die Infra-Sicherung teilweise bereits abgedeckt. + +### Verifizierter Ist-Stand (geprüft am 2026-07-04, beide Hosts) + +Beide Server sind **LXC-Container** (`systemd-detect-virt` → `lxc`), das +zugrunde liegende Storage ist ein ZFS-Dataset auf dem Proxmox-Host +(`zfs/data/subvol-1131-disk-0` bzw. `-1132-disk-0`), aber **aus dem Container +heraus nicht als ZFS ansprechbar** (kein `zfs`/`zpool`-Binary im Container, +kein Zugriff auf Host-Snapshots). Zff-Snapshots wären nur vom Proxmox-Host aus +möglich — das ist eine separate Maßnahme außerhalb der Kontrolle des +archivmail-Deploy-Prozesses und wird hier als Zusatzoption erwähnt, aber nicht +als alleinige Lösung behandelt (siehe Optionen unten). + +Geprüft und **nicht vorhanden**: +- `crontab -l` (root): leer auf beiden Hosts +- `/etc/cron.d/`: nur `archivmail` (App-eigener Cron, PROJ-56c Purge/Reindex/OCR), `e2scrub_all`, `sysstat` — kein Backup-Eintrag +- `/etc/cron.{daily,hourly,weekly}`: nur Standard-Debian-Pakete (apt, dpkg, logrotate, man-db) — kein Backup-Skript +- `systemctl list-timers`: nur Standard-systemd/apt-Timer, kein Backup-Timer +- LVM: kein `lvs`/`vgs` im Container +- Borg/Restic/rsnapshot/Bacula: nicht installiert +- `/var/backups/`: enthält nur dpkg/apt-Housekeeping-Dateien, keine archivmail-Daten + +Vorhanden, aber **ungenutzt/nicht automatisiert**: +- `pg_basebackup`, `pg_backupcluster` (Debian-PostgreSQL-Standardtools) — installiert, aber kein Aufruf irgendwo +- `manticore-backup` — installiert, kein Aufruf irgendwo + +**Fazit:** Bei Totalausfall eines der beiden Container (Hardware, Filesystem- +Korruption, versehentliches `rm`, Ransomware, fehlerhaftes Update) sind Store, +Keyfile und Datenbank **unwiederbringlich verloren**. Das betrifft aktuell +sowohl 131 (Produktiv) als auch 132 (teilproduktiv, ~51.000 Mails). + +### Aktuelle Datenmengen (Referenz für Aufwandsschätzung) + +| Server | Store-Größe | DB | Config | +|---|---|---|---| +| 131 | 3,8 MB (frisch, PROJ-65-Migration) | n/a (Verbindungsproblem bei Check — Peer-Auth) | 23 KB | +| 132 | 6,9 GB (~51.000 Mails) | n/a (nicht separat geprüft) | — | + +Store-Wachstum ist also unterschiedlich weit fortgeschritten; 132 hat den +größeren produktiven Datenbestand trotz "Test"-Label. + +## Bewertung der Infra-Ebene (PBS + Server-Sync) + +Container-/Snapshot-Backup auf Proxmox-Ebene sichert typischerweise das +komplette Root-Filesystem des LXC — d.h. `/var/archivmail/store`, +`/etc/archivmail/keyfile`, `/etc/archivmail/config.yml`, PostgreSQL-Datendir +und `/var/log/archivmail/audit.log` **gemeinsam als ein Snapshot**. Das löst +das in "Warum das kritisch ist" beschriebene Kern-Risiko (Store/Keyfile/DB +müssen zusammen gesichert werden) potenziell bereits vollständig, da alles im +selben Container liegt und in einem Snapshot landet — vorausgesetzt +PostgreSQL läuft ebenfalls im selben Container wie Store/Keyfile (zu +verifizieren, siehe offene Punkte unten). + +**Was dadurch (voraussichtlich) bereits erfüllt ist:** +- Physische Trennung (AC 3) — zweiter Server + PBS sind eigene Infrastruktur. +- Gemeinsame Konsistenz von Store+DB+Keyfile+Config (AC 1) — ein + Filesystem-Snapshot enthält zwangsläufig alle Dateien vom selben Zeitpunkt. +- Hardlink-Erhalt (AC 2) — ein Block-/Filesystem-Snapshot (ZFS/PBS) kopiert + keine Dateien einzeln, sondern sichert die Filesystem-Struktur inklusive + Inode-Referenzen — Hardlinks aus PROJ-65 bleiben dabei erhalten (anders als + bei einem naiven `cp -r`, das dieses Ticket ursprünglich als Risiko für ein + noch zu bauendes App-Backup benannt hatte). + +**Was weiterhin offen bleibt, auch mit PBS+Sync:** +1. **PostgreSQL-Crash-Konsistenz:** Ein Snapshot mitten im Betrieb ist für + PostgreSQL kein sauberes Backup wie `pg_dump`, sondern entspricht einem + Stromausfall-Szenario — PostgreSQL kommt damit über WAL-Replay beim + nächsten Start i.d.R. wieder in einen konsistenten Zustand (das ist der + Normalfall, für den WAL gebaut ist), aber das ist **nicht dasselbe wie + ein verifizierter, anwendungskonsistenter Dump**. Sollte einmal getestet + werden (Restore-Test, siehe unten), nicht nur angenommen. +2. **Restore ist ungetestet** — ein Backup, das nie zurückgespielt wurde, + ist nicht verifiziert (gilt für PBS-Snapshots genauso wie für ein + selbstgebautes rsync-Skript). Das ist unabhängig davon, wer das Backup + zieht, weiterhin ein offener Punkt. +3. **Retention/DSGVO-Abgleich:** Wie lange hält PBS die Snapshots vor, und + ist das mit der DSGVO-Löschkonzeption (PROJ-50) abgestimmt? Muss geklärt + werden, ist aber eine reine Konfigurationsfrage im PBS, kein neuer Code. +4. **Monitoring/Alerting:** Läuft der PBS-Job zuverlässig durch, und gibt es + eine sichtbare Warnung bei Ausfall? (Vermutlich ja, PBS hat eigenes + Job-Monitoring — zu verifizieren, nicht Teil des archivmail-Codes.) +5. **Keyfile-Einzelkopie (Bitwarden):** Zusätzlich zur PBS-Sicherung soll das + Keyfile separat in Bitwarden hinterlegt werden (Nutzer-Entscheidung, + 2026-07-04) — als unabhängiger Wiederherstellungsweg, falls PBS/Snapshot- + Infrastruktur selbst nicht verfügbar ist. Details siehe eigener Abschnitt + unten. + +**Auswirkung auf den Rest dieser Spec:** Die unten stehenden Optionen A/B/C +und der App-eigene rsync+cron-Ansatz sind jetzt **Ergänzung, nicht mehr +alleinige Lösung** — vor allem für den Fall, dass ein anwendungskonsistenter +`pg_dump` zusätzlich zur PBS-Snapshot-Sicherung gewünscht wird (schnellerer, +selektiver Restore einzelner Tabellen, unabhängig von Snapshot-Zeitpunkten). +Die **kritischste verbleibende Lücke ist nicht mehr "kein Backup-Ziel"**, +sondern **"Restore nie getestet"** — das bleibt so oder so zu tun, egal +welche Backup-Quelle (PBS oder App-Skript) im Ernstfall genutzt wird. + +## Warum das kritisch ist + +1. **Datenverlust ohne Backup ist bei einem Mail-Archivsystem ein + Compliance-Vorfall**, nicht nur ein IT-Vorfall — GoBD verlangt + Verfügbarkeit archivierter Mails über die Aufbewahrungsfrist. +2. Store, Keyfile und PostgreSQL-Metadaten sind **nur gemeinsam** nutzbar: + - Store ohne DB-Metadaten: Dateien vorhanden, aber keine Zuordnung zu + Mail/Tenant/Betreff/Datum → praktisch nicht auffindbar. + - Keyfile ohne Store: irrelevant, nichts zu entschlüsseln. + - DB ohne Keyfile: Metadaten vorhanden, aber Mail-Inhalte nicht mehr + entschlüsselbar → GoBD-Anforderung "Lesbarmachung" verletzt. + - Ein Backup, das nur eine dieser drei Komponenten sichert, ist wertlos. +3. **PROJ-65** hat gerade Tenant-Hardlink-Verzeichnisse (`store/tenant_/`) + eingeführt. Ein Backup-Tool, das Hardlinks nicht erhält (z.B. `cp -r`, + naives `scp -r`), würde bei jedem Backup den Speicherbedarf im Backup-Ziel + vervielfachen (pro Tenant eine Vollkopie statt eines Hardlinks) — kein + Datenverlust, aber Speicherplatz-Explosion und potenziell Backup-Jobs, die + wegen vollem Zielverzeichnis fehlschlagen. + +## Backup-Strategie-Optionen + +### Option A — rsync + cron, konsistenter Snapshot via kurzem Stop (empfohlen als erster Schritt) + +Ablauf pro Lauf (nachts, Zeitfenster mit wenig Traffic wählen, ggf. an +PROJ-56-Last-Entzerrung orientieren): + +1. `systemctl stop archivmail` (Backend stoppen — Store/DB werden während + des Kopiervorgangs nicht verändert; Frontend kann laufen bleiben, da es + nur liest, aber optional auch stoppen für saubere Konsistenz) +2. `pg_dump -Fc archivmail > backup.dump` (Custom-Format, komprimiert, + erlaubt selektiven Restore einzelner Tabellen) +3. `rsync -aH --delete /var/archivmail/store/ /store/` — **`-H` ist + Pflicht**, sonst werden Tenant-Hardlinks aus PROJ-65 zu Vollkopien + aufgelöst +4. `cp /etc/archivmail/keyfile /keyfile` (getrennt verschlüsselt/ + abgesichert, siehe unten) +5. `cp /etc/archivmail/config.yml /config.yml` +6. `cp /var/log/archivmail/audit.log /audit.log.$(date +%F)` (Append- + Only-Charakter erhalten — Datei nicht überschreiben, sondern datiert + kopieren, damit historische Audit-Nachweise auch bei Log-Rotation auf dem + Quellsystem nicht verloren gehen) +7. `systemctl start archivmail` +8. Health-Check nach Neustart (Backend ✓ läuft, wie im Standard-Deploy-Workflow) +9. Manticore-Index: **niedrige Priorität**, aus Store+DB per + `archivmail reindex` rekonstruierbar — kein Backup nötig, ggf. optional + `manticore-backup` für schnelleren Recovery-Pfad (Reindex von 51k Mails + kann je nach OCR-Last dauern) + +Vorteil: Downtime ist kurz (Stop → Kopieren der geänderten Blöcke via rsync +inkrementell → Start), einfach zu verstehen, kein neues Tooling nötig außer +rsync/cron (beide bereits vorhanden). Nachteil: Downtime existiert (wenn auch +kurz bei inkrementellem rsync nach dem ersten vollen Lauf). + +### Option B — PITR / kontinuierliche Konsistenz ohne Stop (vollständige Lösung, später) + +- PostgreSQL: WAL-Archiving + Basisbackup (`pg_basebackup`) für + Point-in-Time-Recovery statt Downtime beim Dump. +- Store: Da Store append-only/content-adressiert ist (Dateien werden nach + Schreiben nie verändert, nur neue hinzugefügt oder per GoBD-Purge gelöscht), + ist ein Live-rsync ohne Stop grundsätzlich risikoarm — die einzige + Inkonsistenz-Gefahr ist ein Mail-Datensatz, der in der DB bereits committed + ist, aber dessen Store-Datei noch nicht vollständig geschrieben wurde (Race + zwischen Storage-Write und DB-Commit). Das lässt sich durch Reihenfolge + Store-zuerst-dann-DB im Schreibpfad (falls nicht schon so implementiert — + bei mailarchiv-architect erfragen) und/oder einen Reconciliation-Check nach + dem Restore (PROJ-52 `archivmail reconcile`) abfedern statt durch Downtime. +- Aufwand deutlich höher (WAL-Archiv-Ziel, Retention der WAL-Segmente, mehr + bewegliche Teile) — als Phase 2 vorschlagen, nicht als ersten Schritt. + +### Option C — Proxmox-Host-Snapshots (ZFS) + +Da beide Hosts LXC-Container auf Proxmox mit ZFS-Backing sind, sind +Host-seitige ZFS-Snapshots oder Proxmox-Backup-Server-Jobs eine sinnvolle +**Ergänzung** (schnelle Rollback-Möglichkeit bei OS-/Config-Fehlern), aber +kein Ersatz für anwendungskonsistente Backups: Ein ZFS-Snapshot mitten im +`pg_dump`-losen Normalbetrieb sichert PostgreSQL nicht transaktionskonsistent +(außer Postgres nutzt selbst WAL + fsync korrekt, was bei ZFS-Snapshots auf +Blockebene in der Praxis meist funktioniert, aber nicht ausdrücklich +validiert wurde). Empfehlung: als Zusatzschicht einplanen, aber **erfordert +Zugriff auf den Proxmox-Host**, der nicht Teil dieses Deploy-Agenten-Scopes +ist — separates Ticket/Absprache mit dem Infrastruktur-Verantwortlichen für +den Proxmox-Host nötig. + +### Empfehlung + +**Phase 1 (dieses Ticket, pragmatisch):** Option A — rsync+cron mit kurzem +Stop, tägliches Backup auf ein Off-Container-Ziel (siehe Off-Site unten). +Deckt das eigentliche Risiko (Totalverlust ohne jegliches Backup) sofort ab. + +**Phase 2 (späteres Ticket):** Option B (PITR) wenn Downtime-Fenster zum +Problem wird (z.B. wegen wachsendem Store auf 132), plus Option C als +zusätzliche Sicherheitsschicht in Absprache mit Proxmox-Host-Admin. + +## Backup-Ziel: On-Site vs. Off-Site + +- **On-Site-only ist nicht ausreichend**: Beide Produktivsysteme (131, 132) + laufen vermutlich auf derselben Proxmox-Host-Infrastruktur — ein + Host-Ausfall (Hardware, Ransomware, Fehlkonfiguration) könnte beide Systeme + UND ein On-Site-Backup-Ziel gleichzeitig treffen, wenn es auf demselben + Host/Storage liegt. +- Empfehlung: Backup-Ziel auf physisch getrennter Infrastruktur (zweiter + Host, NAS außerhalb des Proxmox-Clusters, oder Cloud-Storage mit + Verschlüsselung — Cloud nur wenn DSGVO-konform vertraglich abgesichert, + siehe Verschlüsselung unten). +- Mindestens: 3-2-1-Prinzip anstreben (3 Kopien, 2 verschiedene Medien, 1 + Off-Site) — für den pragmatischen ersten Schritt reicht 2 Kopien (Produktiv + + 1 Backup-Ziel off-site) mit klarer Dokumentation, dass das noch nicht + vollständig 3-2-1 ist. + +## Verschlüsselung des Backups selbst + +Das Backup enthält **Keyfile UND verschlüsselte Mails** zusammen — sobald +beide am selben Ort liegen, ist der Verschlüsselungsschutz des Stores +faktisch aufgehoben, wenn das Backup-Ziel kompromittiert wird. Anforderungen: + +1. **Transport:** rsync über SSH (ohnehin Standard) oder explizit + `rsync -e ssh`, niemals unverschlüsseltes Protokoll (FTP, unverschlüsseltes + NFS über öffentliches Netz). +2. **At-Rest am Backup-Ziel:** Wenn das Backup-Ziel als weniger vertrauenswürdig + gilt als der Produktivserver (z.B. NAS mit mehr Admin-Zugriffsberechtigten, + Cloud-Storage), muss das Backup-Ziel selbst verschlüsselt sein — entweder + Ziel-Filesystem-Verschlüsselung (LUKS) oder Backup-Tool mit eingebauter + Verschlüsselung (z.B. `restic` mit Repository-Passwort — als Phase-2-Option + evaluieren, da es außerdem Deduplizierung/Versionierung mitbringt). +3. **Keyfile separat behandeln:** Empfehlung, das Keyfile-Backup zusätzlich + getrennt vom Store-Backup aufzubewahren (anderer Zugriffspfad/anderes + Passwort) — erschwert Angreifern, die nur das Backup-Ziel kompromittieren, + den vollen Zugriff. Mindestens: restriktive Dateirechte (0600, root-only) + auch im Backup. +4. Backup-Skript selbst darf Zugangsdaten (SSH-Key fürs Backup-Ziel) nicht im + Klartext in world-readable Cron-Dateien ablegen (analog zu PROJ-64 + Datei-Permissions-Härtung). + +### Keyfile-Escrow in Bitwarden (Nutzer-Entscheidung, 2026-07-04) + +Zusätzlich zur PBS-/Sync-Sicherung wird das Keyfile als unabhängige Kopie in +Bitwarden hinterlegt — für den Fall, dass die Snapshot-/PBS-Infrastruktur +selbst nicht verfügbar oder kompromittiert ist. + +- **Zugriffsbeschränkung:** Eigene Bitwarden-Collection/Ordner, nur für + Personen mit Restore-Berechtigung sichtbar — nicht die allgemeine + Organisations-Vault. Das Keyfile entschlüsselt das **gesamte Archiv aller + Mandanten**, ist der höchstwertige Secret im System. +- **Format:** Secure Note mit Base64-kodiertem Keyfile-Inhalt (32 Byte roh + → base64 fürs Notizfeld), plus Metadaten (welcher Server/welche + Installation, Erstellungsdatum, wer hat's zuletzt geprüft). +- **Kein Ersatz für Zugriffskontrolle:** Bitwarden-Zugriff muss genauso + ernst genommen werden wie root-Zugriff auf den Produktivserver (2FA + Pflicht, Zugriffsprotokoll wenn Bitwarden das bietet). +- **Rotation/Prüfung dokumentieren:** Wer Zugriff hat und wann zuletzt + verifiziert wurde, dass der Bitwarden-Eintrag noch mit dem aktiven Keyfile + übereinstimmt (relevant falls das Keyfile jemals rotiert wird — aktuell + nicht vorgesehen, aber für die Zukunft dokumentieren). +- Kein Blocker für die restlichen ACs — unabhängig von PBS/App-Backup + umsetzbar, kleiner Zusatzschritt (Bitwarden-Eintrag anlegen, kein Code). + +## Aufbewahrungsdauer / Rotation + +- Vorschlag: Tägliche Backups, Rotation nach Generationen-Prinzip: + - 7 tägliche Backups + - 4 wöchentliche Backups (z.B. jeweils Sonntag) + - 6 monatliche Backups +- Wichtig: Rotation der **Backup-Kopien** ist unabhängig von der + GoBD-Aufbewahrungsfrist der Mails selbst (PROJ-51 Retention-Kategorien) — + eine gelöschte/abgelaufene Mail in Produktion kann in einem älteren Backup + noch enthalten sein. Das ist im Rahmen der GoBD-Löschkonzeption (PROJ-50, + DSGVO-Löschersuchen) zu dokumentieren: Backups mit personenbezogenen Daten, + die in Produktion bereits DSGVO-gelöscht wurden, dürfen nicht unbegrenzt + aufbewahrt werden → Backup-Rotation muss kurz genug sein, dass gelöschte + Daten nach spätestens N Rotationszyklen auch aus alten Backups verschwinden + (Abstimmung mit Datenschutzbeauftragtem/Nutzer nötig, kein rein technisches + Thema). + +## Restore-Prozess (muss dokumentiert UND getestet sein) + +Kein Backup ohne verifizierten Restore-Prozess. Vorschlag Runbook-Struktur +(Stil analog `docs/MIGRATION_RUNBOOK.md`): + +1. Zielserver vorbereiten (frisches Debian oder vorhandener Server im + Wartungsmodus, archivmail-Dienste gestoppt). +2. Store wiederherstellen: `rsync -aH /store/ /var/archivmail/store/` + (Hardlinks bleiben erhalten). +3. Keyfile wiederherstellen: `cp /keyfile /etc/archivmail/keyfile`, + Rechte prüfen (0600, root:root). +4. Config wiederherstellen: `cp /config.yml /etc/archivmail/config.yml`. +5. PostgreSQL wiederherstellen: `pg_restore -d archivmail /backup.dump` + (auf leere, frisch angelegte DB). +6. Audit-Log wiederherstellen (informativ, kein Funktionsbestandteil): + `/var/log/archivmail/audit.log` aus dem letzten Backup-Stand kopieren. +7. Dienste starten, Health-Check (Backend ✓, Frontend ✓ wie im + Standard-Deploy-Workflow). +8. **Verifikation (Pflicht, nicht optional):** + - `archivmail reconcile` laufen lassen (PROJ-52) — Zählvergleich + Store-Dateien vs. DB-Einträge muss aufgehen. + - Stichprobe: 3-5 zufällige Mails über die UI öffnen und lesbar + entschlüsseln lassen (beweist Keyfile+Store+DB passen zusammen). + - `archivmail reindex` ausführen, Suche testen (Manticore wird aus 1+3 + rekonstruiert). +9. Restore-Ergebnis (Mail-Zahl, Zeitstempel, Dauer) dokumentieren — analog zum + Migrations-Runbook-Prinzip "Zählen, Prüfen, Belegen". + +**Regelmäßiger Restore-Test:** Ein Backup, das nie zurückgespielt wurde, ist +nicht verifiziert. Empfehlung: mindestens vierteljährlich einen Restore auf +einen isolierten Test-Container durchführen (nicht auf 131/132!) und Schritt +8 durchlaufen. + +## Monitoring / Alerting für den Backup-Job selbst + +Größtes Risiko bei Cron-Backups: Job schlägt still fehl, niemand merkt es, +bis der Ernstfall eintritt (siehe PROJ-58-Lehre: "Cron-Zeilen fehlten +wochenlang trotz aktivem Code, weil es niemand automatisch prüfte"). + +Anforderungen: +- Backup-Skript muss Exit-Code aller Teilschritte prüfen (rsync, pg_dump, + cp) und bei Fehler **nicht** die Rotation der alten (guten) Backups + anstoßen (sonst wird ein gutes altes Backup durch ein fehlgeschlagenes + überschrieben). +- Log-Datei pro Lauf mit Zeitstempel, Ergebnis, Dauer, Größe. +- Alerting: minimal ein "Backup-Alter"-Check, der z.B. vom bestehenden + Health-Check-Mechanismus oder Dashboard (PROJ-17-Metriken-Anlehnung) + mitgeprüft wird — "letztes erfolgreiches Backup älter als 26h" → + sichtbarer Warnzustand (Dashboard-Badge oder E-Mail/Webhook, je nachdem was + im System bereits an Alerting existiert; falls nichts existiert, ist das + ein Teil dieses Tickets: minimal ein Cron-Check-Skript, das eine + Markerdatei mit Zeitstempel nach erfolgreichem Lauf schreibt und ein + zweiter Cron-Job/Health-Endpoint das Alter dieser Datei prüft). + +## User Stories + +- **Als Betreiber** möchte ich, dass Store, Keyfile und DB-Metadaten täglich + automatisiert und konsistent gesichert werden, damit ein Server-Totalausfall + nicht zu unwiederbringlichem Mail-Verlust führt. +- **Als Betreiber** möchte ich, dass ein fehlgeschlagener Backup-Lauf sichtbar + wird (nicht erst beim Restore-Versuch auffällt), damit ich rechtzeitig + eingreifen kann. +- **Als GoBD-Verantwortlicher** möchte ich, dass ich im Audit-Fall belegen + kann, dass Backups vorhanden, aktuell und restorebar sind. +- **Als Datenschutzbeauftragter** möchte ich, dass DSGVO-Löschungen (PROJ-50) + nicht durch unbegrenzt aufbewahrte Backups unterlaufen werden. + +## Acceptance Criteria + +1. Es existiert ein dokumentiertes, automatisiert laufendes Backup (Cron + oder systemd-Timer) auf 131 UND 132, das Store, Keyfile, PostgreSQL-DB und + config.yml konsistent zueinander sichert. +2. Das Backup-Tool erhält nachweislich Hardlinks im Store (Test: Backup-Größe + nach PROJ-65-Tenant-Hardlinks entspricht nicht der Summe aller + Tenant-Kopien, sondern der tatsächlichen Store-Größe — Stichprobe via + `du --apparent-size` vs. `du` bzw. `stat` Link-Count-Check nach Restore). +3. Backup liegt auf einem vom Produktivsystem physisch getrennten Ziel + (nicht im selben Proxmox-Host/-Storage wie 131/132, sofern das ohne + zusätzliche Host-Beschaffung machbar ist — sonst als bekannte Einschränkung + dokumentieren). +4. Backup-Daten (insbesondere Keyfile-Kopie und Store-Kopie zusammen) sind am + Zielort verschlüsselt oder das Ziel ist gleichwertig abgesichert wie der + Produktivserver. +5. Rotation/Retention ist implementiert und lässt Backups nicht unbegrenzt + wachsen; Retention ist kurz genug, um DSGVO-Löschungen nicht dauerhaft zu + unterlaufen (Zeitraum mit Nutzer/Datenschutzbeauftragtem abgestimmt). +6. Ein vollständiger Restore wurde mindestens einmal auf einem isolierten + Testsystem durchgeführt und mit `archivmail reconcile` + Stichproben- + Entschlüsselung verifiziert; Ergebnis ist dokumentiert. +7. Es gibt einen sichtbaren Alarm-/Warnmechanismus, wenn der letzte + erfolgreiche Backup-Lauf älter als ein definiertes Schwellenwert-Fenster + ist (z.B. 26h bei täglichem Backup). +8. Ein Backup-Runbook (analog `docs/MIGRATION_RUNBOOK.md`) existiert unter + `docs/BACKUP_RESTORE_RUNBOOK.md` mit Schritt-für-Schritt-Anleitung für + Backup, Restore und Verifikation. +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 + Datei nachweislich mit aus (Lehre aus PROJ-58, siehe Deploy-Vollständigkeits- + Check in den devops-deploy-Regeln) — sonst existiert das Skript nur lokal + und geht beim nächsten Server-Neuaufsatz verloren. +10. **Keyfile-Kopie liegt zugriffsbeschränkt in Bitwarden** (Secure Note, + eigene Collection, nur Restore-Berechtigte) — unabhängig von PBS/App-Backup. +11. **PBS-/Sync-Sicherung verifiziert:** Dokumentiert ist, was genau gesichert + wird (welcher Container/welche Pfade), wie lange PBS die Snapshots vorhält, + und dass mindestens ein Restore aus einem PBS-Snapshot erfolgreich getestet + wurde (nicht nur aus einem App-eigenen Backup, falls beide existieren). + +## Edge Cases + +- **Server-Totalausfall (131 oder 132):** Restore auf neuem/wiederhergestelltem + Container muss laut Runbook in angemessener Zeit möglich sein (Ziel-RTO + definieren, z.B. < 4h für 132 mit 51k Mails — abhängig von Reindex-Dauer). +- **Backup-Job schlägt fehl, ohne dass es auffällt:** durch Monitoring/ + Alerting (siehe oben) abgedeckt — Testfall: Backup-Skript künstlich zum + Scheitern bringen (z.B. Zielverzeichnis vollschreiben) und prüfen, ob der + Alarm tatsächlich auslöst. +- **Restore-Test schlägt fehl:** Reconcile zeigt Lücken → Ursache muss vor + Produktivnutzung des Backups geklärt werden (z.B. Backup lief während + aktiven Schreibvorgangs ohne Stop-Schritt → Konsistenzproblem im + Backup-Skript selbst beheben). +- **Backup-Ziel läuft voll:** Rotation muss dies verhindern; zusätzlich + Disk-Space-Check vor jedem Lauf (analog `df -h`-Diagnose-Befehl im + devops-deploy-Skill) mit Abbruch + Alarm statt partiellem/korruptem Backup. +- **PROJ-65-Hardlinks nach Restore:** Nach Restore prüfen, dass + Tenant-Hardlink-Struktur (`store/tenant_/`) wiederhergestellt ist und + nicht zu doppeltem Speicherverbrauch auf dem Zielsystem führt. +- **Gleichzeitiger Schreibzugriff während Backup (Option A mit Stop):** Kurzer + Downtime-Fenster einplanen und in Betriebszeiten kommunizieren (SMTP- + Empfang während Stop puffert sich ggf. beim Absender/Relay, sollte mit + mailarchiv-architect abgeklärt werden, ob eingehende Mails während Backend- + Stop verloren gehen könnten oder ob es einen Retry-Mechanismus gibt). +- **132 ist "teilproduktiv":** Muss von Anfang an mit derselben Priorität wie + 131 behandelt werden, nicht nachrangig als "nur Testserver" — bereits im + Ticket-Titel und in den ACs berücksichtigt (beide Server, nicht nur 131). + +## Aufwandseinschätzung + +Durch die vorhandene PBS+Sync-Infrastruktur verschiebt sich der Schwerpunkt +von "Backup-Ziel schaffen" zu "vorhandenes Backup verifizieren + gezielt +ergänzen": + +- **Phase 0 (klein, zuerst):** Bitwarden-Keyfile-Escrow anlegen (kein Code, + <1h). Dokumentieren, was PBS/Sync konkret sichert und mit welcher + Retention (Abstimmung mit demjenigen, der PBS betreibt — vermutlich kein + archivmail-Code-Zugriff nötig, eher ein Gespräch/Screenshot der + PBS-Job-Konfiguration). Geschätzt **1-2h**. +- **Phase 1 (Restore-Verifikation, wichtigster verbleibender Schritt):** Ein + echter Restore-Test aus einem PBS-Snapshot auf einen isolierten + Test-Container, durchlaufen mit `archivmail reconcile` + Stichproben- + Entschlüsselung (siehe Restore-Prozess oben). Deckt auf, ob die + PostgreSQL-Crash-Konsistenz in der Praxis reicht oder ob doch ein + zusätzlicher `pg_dump` nötig ist. Geschätzt **2-4h** (abhängig davon, wie + schnell ein Testsystem für den Restore verfügbar ist). +- **Phase 2 (nur falls Phase 1 Lücken zeigt):** App-eigenes rsync+cron+pg_dump + (Option A) als Ergänzung, falls PBS-Snapshots allein nicht ausreichen + (z.B. weil Retention zu kurz für Compliance ist, oder PITR-Granularität + gebraucht wird). Aufwand wie ursprünglich geschätzt, **4-6h**. +- **Phase 3 (optional, später):** PITR/WAL-Archiving, restic/borg-Migration — + nur falls Phase 1/2 einen konkreten Bedarf dafür aufzeigen. + +**Offene Entscheidung vor Implementierung von Phase 1:** Steht ein isolierter +Test-Container für den Restore-Test zur Verfügung (nicht 131/132 selbst)? + +## Nicht Teil dieses Tickets + +- Implementierung selbst (dieses Ticket liefert nur die Spec, Status + "Planned" — Implementierung erst nach Freigabe). +- Konfiguration/Änderung der PBS-Jobs selbst (liegt außerhalb des + archivmail-Deploy-Scopes, eigener Verantwortungsbereich). +- Proxmox-Host-seitige ZFS-Snapshot-Konfiguration (separates Thema, anderer + Verantwortungsbereich/Zugriff).