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.
696 lines
40 KiB
Markdown
696 lines
40 KiB
Markdown
# 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
|