Files
archivmail/features/PROJ-66-backup-strategie.md
sysops 4eba165250 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.
2026-07-05 21:11:39 +02:00

696 lines
40 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PROJ-66: Backup-Strategie für archivmail (Produktiv + Teilproduktiv)
**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
## 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_<id>/`)
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/ <ziel>/store/`**`-H` ist
Pflicht**, sonst werden Tenant-Hardlinks aus PROJ-65 zu Vollkopien
aufgelöst
4. `cp /etc/archivmail/keyfile <ziel>/keyfile` (getrennt verschlüsselt/
abgesichert, siehe unten)
5. `cp /etc/archivmail/config.yml <ziel>/config.yml`
6. `cp /var/log/archivmail/audit.log <ziel>/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).
**Umgesetzt (2026-07-04):** `install.sh` gibt das frisch generierte Keyfile
direkt nach der Erzeugung einmalig auf der Konsole aus (`print_keyfile_escrow_warning()`,
beide Installationspfade — Docker- und Native-Modus), mit deutlichem Hinweis,
den Wert sofort in einen Passwort-Safe zu verschieben. Verhindert, dass der
Escrow-Schritt vom Bequemlichkeitsgrund "mach ich später" verschluckt wird —
der Wert steht sonst nirgends mehr sichtbar zur Verfügung, sobald die
Installations-Shell geschlossen ist (Keyfile-Datei selbst ist `chmod 400`,
nicht mehr casual auslesbar). Bereits bestehende Installationen (Keyfile
existiert schon) bekommen die Ausgabe NICHT erneut — dafür manuell
`cat /etc/archivmail/keyfile` einmalig ausführen und in Bitwarden nachtragen.
## 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 <backup>/store/ /var/archivmail/store/`
(Hardlinks bleiben erhalten).
3. Keyfile wiederherstellen: `cp <backup>/keyfile /etc/archivmail/keyfile`,
Rechte prüfen (0600, root:root).
4. Config wiederherstellen: `cp <backup>/config.yml /etc/archivmail/config.yml`.
5. PostgreSQL wiederherstellen: `pg_restore -d archivmail <backup>/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.
**✓ 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
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_<id>/`) 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
- 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).
## Implementation Notes (2026-07-04) — App-eigenes `archivmail backup`/`restore`
Nutzer-Entscheidung: App-eigene Backup-CLI zusätzlich zur PBS-Sicherung
bauen (Phase 2 vorgezogen), statt nur auf Infra-Ebene zu verlassen — gibt
einen von PBS unabhängigen, selektiven Restore-Weg (einzelne Tabellen via
`pg_restore`, ohne ganzen Container zurückspielen zu müssen).
### Neue Dateien
- `cmd/archivmail/cmd_backup.go`: `archivmail backup -dest <dir> [-config ...] [-keep N]`.
Schreibt in `<dest>/<timestamp>/`: `postgres.dump` (`pg_dump -Fc`, shell-out),
`store/` (Hardlink-erhaltende Kopie, siehe unten), `keyfile`, `config.yml`,
`audit.log` (best-effort). Rotation (`-keep`, Default 7) läuft NUR nach
erfolgreichem Durchlauf — ein fehlgeschlagener Lauf lässt den partiellen
Ordner stehen und rotiert nichts weg (Lehre aus PROJ-58: ein Job darf beim
Scheitern nie gute alte Stände zerstören).
- `cmd/archivmail/cmd_restore.go`: `archivmail restore -source <dir> [-force] [-skip-db]`.
Bewusst konservativ: bricht ab, wenn `store_path`/Keyfile bereits Inhalt
haben, außer `-force` ist gesetzt — ein versehentlicher Restore gegen ein
laufendes System soll nicht kommentarlos Produktivdaten überschreiben.
Stoppt/startet den Dienst NICHT selbst (Restore ist für einen frischen oder
bewusst leergeräumten Zielserver gedacht, kein Live-Overlay). Gibt am Ende
die Pflicht-Verifikationsschritte aus dem Runbook aus (`reconcile`,
`reindex`, Stichproben-Entschlüsselung).
### Hardlink-Erhalt ohne externe Tools
`copyTreePreservingHardlinks()` (in `cmd_backup.go`, von `cmd_restore.go`
mitgenutzt) läuft den Store-Baum ab, merkt sich pro Datei die Inode-Nummer
(`syscall.Stat_t.Ino`, Linux) und legt beim zweiten Auftreten derselben Inode
einen Hardlink statt einer Kopie an. Das macht die Backup-CLI unabhängig von
`rsync -H` (kein zusätzliches Tool-Dependency) und funktioniert identisch für
Backup wie Restore — PROJ-65s Tenant-Hardlink-Struktur bleibt dadurch sowohl
im Backup-Ziel als auch nach einem Restore verlustfrei erhalten (kein
Speicherplatz-Mehrverbrauch).
### Bewusst nicht gebaut
- Kein automatischer Cron-Eintrag aktiv — `deploy/cron.d/archivmail` enthält
ihn nur auskommentiert als Vorlage, da `-dest` ein konkretes, vom Host
getrenntes Ziel braucht, das noch nicht feststeht (siehe offene Entscheidung
oben).
- Kein automatisches Stop/Start der Dienste im Restore-Kommando (siehe oben).
- Keine Backup-Verschlüsselung im Tool selbst (Abschnitt "Verschlüsselung des
Backups selbst" bleibt gültig — Transport/Ziel-Absicherung ist
Infrastruktur-Aufgabe, nicht Teil dieses CLI-Kommandos).
- Kein Alerting bei Backup-Alter/-Ausfall im Code selbst (AC 7 bleibt offen,
bräuchte einen zweiten Cron-Job/Health-Check, der die letzte
Backup-Verzeichnis-Zeit prüft — noch nicht gebaut).
### Offen / Handoff
- Kein lokaler `go build` möglich — QA auf Testserver 132 nötig, insbesondere:
Backup+Restore-Roundtrip (Backup ziehen, auf leeren Store restoren,
`reconcile`+Stichprobe), Hardlink-Erhalt verifizieren (Inode-Vergleich wie
bei PROJ-65-QA), Verhalten bei vollem `-dest`-Ziel, `-force`-Schutz wirklich
blockierend bei nicht-leerem Store.
- `-dest`-Ziel für einen produktiven Cron-Eintrag muss noch vom Nutzer
festgelegt werden, bevor die auskommentierte Cron-Zeile aktiviert wird.
## QA Test Results (2026-07-04, Testserver 132)
**Getestet gegen Commit f3a7dea (lokal, nicht gepusht/deployt).** Build und
Funktionstests auf 132 in isoliertem Testbereich (synthetischer Store mit
PROJ-65-Hardlinks unter `/tmp/qa66-*`, eigene Test-DB `archivmail_qa66_test`).
Produktion (60.595 Store-Dateien, DB `archivmail`) nachweislich unberührt —
kein Zugriff auf echte Store-Dateien oder die echte DB, nach dem Test verifiziert.
**Gesamtergebnis: QA NICHT BESTANDEN** — 1 Critical (build-breaking) + 1 High
(-force funktionslos). Nach Fix beider Bugs erneut testen.
| # | Testpunkt | Ergebnis |
|---|-----------|----------|
| 1 | `go build ./cmd/archivmail/` | **FAIL** (BUG-1, build-breaking) |
| 2 | Backup-Lauf gegen Test-Config | PASS (nur mit QA-Workaround-Patch) |
| 3 | postgres.dump/store/keyfile/config.yml vorhanden + Store-Hardlinks (Inode-Gleichheit, link count 2) | PASS |
| 4 | Restore-Roundtrip auf isoliertes Ziel: Store + Hardlinks + pg_restore (DB-Zeilen korrekt), Keyfile 0600 | PASS |
| 5 | `-force`-Schutz: ohne `-force` blockiert (5a) / mit `-force` läuft durch (5b) | 5a PASS / **5b FAIL** (BUG-2) |
| 6 | Rotation `-keep 2` über 3 Läufe → nur 2 Verzeichnisse | PASS |
| 7 | Ungültiges/nicht beschreibbares `-dest` → sauberer Abbruch, keine Rotation guter Backups | PASS |
| 8 | `backup` ohne `-dest` → Fehlerabbruch | PASS |
Zusätzlich verifiziert: pg_dump-Fehlerfall (falsche DB) lässt partiellen
Backup-Ordner stehen und rotiert **keine** guten alten Backups weg (Lehre aus
PROJ-58) — PASS.
### BUG-1 (Severity: CRITICAL, Priorität: sofort) — Binary kompiliert nicht
`cmd/archivmail/cmd_import.go:310` bricht den Build. Die Hilfetext-Zeile für
`restore` wurde im Commit f3a7dea in den mit Backticks begrenzten
Raw-String-Literal von `printHelp()` (`fmt.Printf` mit Backtick-String, geschlossen
mit `` `, AppVersion) ``) eingefügt, enthält aber selbst Backticks um
`` `archivmail backup` ``. Diese beenden das Raw-String-Literal vorzeitig:
```
cmd/archivmail/cmd_import.go:310:30: syntax error: unexpected name archivmail in argument list; possibly missing comma or )
```
Repro: `CGO_ENABLED=0 go build -buildvcs=false -o /tmp/x ./cmd/archivmail/`
→ scheitert. **Das gesamte Backend (nicht nur backup/restore) ist nicht
baubar/deploybar, solange dieser Fehler besteht.** Für die restlichen QA-Punkte
wurde in einem Wegwerf-Build-Verzeichnis die Zeile behelfsweise auf einfache
Anführungszeichen geändert (kein Fix am Repo/Commit — Bug besteht unverändert).
Fix-Vorschlag (Backend Developer): Backticks in Zeile 310 durch einfache
Anführungszeichen ersetzen oder die Zeile ohne Inline-Code-Markup formulieren.
### BUG-2 (Severity: HIGH, Priorität: hoch) — `restore -force` schlägt bei nicht-leerem Store mit Hardlinks fehl
`-force` soll laut Spec/AC einen bereits befüllten Store überschreiben. Tatsächlich
bricht der Restore ab, sobald der Store Hardlinks enthält (PROJ-65-Tenant-Struktur —
der Normalfall):
```
restore: store restore failed: hardlink /…/ab/abcdef123.bin -> /…/tenant_1/abcdef123.bin:
link …: file exists
```
Ursache: `copyTreePreservingHardlinks()` (`cmd_backup.go`) ruft `os.Link()` für die
zweite Inode-Referenz auf, ohne dass das Zielverzeichnis vorher geleert wird. Bei
`-force` überschreibt `copyFile()` reguläre Dateien zwar via `O_TRUNC`, aber `os.Link()`
scheitert an einer bereits existierenden Zieldatei (`EEXIST`). Damit ist `-force`
für genau den Anwendungsfall funktionslos, für den es gedacht ist (Restore über
einen vorhandenen, mit Tenant-Hardlinks befüllten Store). Repro: einmal restoren,
dann erneut mit `-force` auf dasselbe Ziel → Fehler.
Fix-Vorschlag (Backend Developer): bei `-force` das Zielverzeichnis vor dem
Restore leeren, oder in `copyTreePreservingHardlinks()` vor `os.Link()`/`copyFile()`
ein vorhandenes Ziel entfernen (`os.Remove(target)`, `ErrNotExist` ignorieren).
Hinweis: `-force` ohne Hardlinks (nur reguläre Dateien) läuft dank `O_TRUNC` durch —
der Fehler tritt nur bei der zweiten+ Inode-Referenz auf.
### Nicht getestet / Hinweise
- Nur die CLI-Kommandos wurden getestet, nicht die offenen ACs (Cron/Timer AC 1,
physische Trennung AC 3, Verschlüsselung AC 4, Restore-Verifikation via echtem
`reconcile` AC 6, Alerting AC 7, Runbook AC 8, Bitwarden AC 10, PBS AC 11) —
diese sind laut Implementation Notes bewusst noch offen.
- `pg_restore --clean --if-exists` wurde nur gegen eine leere Test-DB geprüft;
Idempotenz über eine bereits befüllte DB nicht separat getestet.
- Testartefakte (`/tmp/qa66-*`, Test-DB `archivmail_qa66_test`, Wegwerf-Build
`/root/archivmail-qa66`) nach Testende entfernt und Entfernung verifiziert.
## Fixes nach QA-Runde 1 (2026-07-04)
- **BUG-1 (Critical, build-breaking):** `cmd/archivmail/cmd_import.go:310`
enthielt Backticks um `` `archivmail backup` `` innerhalb des Raw-String-
Literals von `printHelp()`, was den String vorzeitig schloss
(`syntax error: unexpected name archivmail in argument list`). Fix:
Backticks durch einfache Anführungszeichen ersetzt.
- **BUG-2 (High):** `-force` bei `restore` scheiterte an bereits vorhandenen
Hardlink-Zieldateien (`os.Link: file exists`) — für den Normalfall (Restore
gegen einen PROJ-65-Tenant-Hardlink-Bestand) war `-force` damit
funktionslos. Fix in `copyTreePreservingHardlinks()`: vor `os.Link` wird
eine bereits vorhandene Zieldatei entfernt (`os.Remove`, `os.IsNotExist`
wird ignoriert — im Normalfall ist das Ziel leer und der Remove ist ein
No-Op).
Re-Test von Punkt 1 (Build) und Punkt 5b (`-force` gegen befüllten Store)
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