syncova-backup/docs/disaster-recovery.md
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
Enterprise-Backup-, Recovery-, Verification-, Security- und
Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme.

Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als
vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit
nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem
tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes
geht in keine Bewertung als "gut" ein.

Umfang (Phasen 0-23):

- Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll,
  Katalogaufbau allein aus den Manifesten — ohne Datenbank
- Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz
  Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck
- Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell)
- Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive
- Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit
- Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center,
  Ransomware-Heuristik (meldet, handelt nie)
- Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing
- Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository
- Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64

Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup
Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs.

Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die
systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine
wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md
und docs/release-candidate.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 09:10:54 +02:00

203 lines
9.7 KiB
Markdown

# Disaster Recovery (Phase 18)
Vier Szenarien, alle vier real durchgespielt — der Plan (§20) sagt „Implement
**and test**", und ein Notfallverfahren, das nie ausgeführt wurde, ist keines.
## Die eine Entscheidung, die alles trägt
> Eine Konfigurationssicherung, die nur auf dem Control-Server liegt, ist beim
> Verlust des Control-Servers wertlos.
Genau das ist Szenario A. Wer die Konfiguration in ein Verzeichnis neben der
Datenbank schreibt, hat sie im Ernstfall nicht mehr — Server und Datenbank gehen
typischerweise gemeinsam verloren, weil sie auf derselben Maschine stehen.
Der einzige Ort, der den Verlust überlebt, ist das **Repository**: Es liegt
anderswo, es ist selbstbeschreibend, und ohne es wäre ohnehin alles verloren.
Deshalb schreibt `syncova-dr export` die Control-Plane-Konfiguration nach
`metadata/control-plane/` — neben die Backups.
## Was der Sicherungssatz enthält — und was nicht
Enthalten: Repositories, Aufträge samt Quellen, Aufbewahrungsregeln,
Wartungsfenster, Konten mit ihren Rollen, Systemeinstellungen.
**Nicht enthalten, und zwar mit Absicht:**
| Ausgelassen | Warum |
| --- | --- |
| Passwörter und Passwort-Hashes | Ein Argon2id-Hash ist kein Klartext, aber offline angreifbar — und ein Repository liegt naturgemäß außerhalb der Anlage |
| Zweite Faktoren (TOTP) | Dasselbe |
| Zugangsdaten **und Konfiguration** der Benachrichtigungswege | In der Konfiguration steht die Webhook-Adresse, und die trägt bei vielen Diensten das Token im Pfad |
| Sitzungen und Agenten-Tokens | Sie sollen nach einem Ausfall ablaufen |
| Der Verschlüsselungsschlüssel der Anlage | Er gehört in die Umgebung des Dienstes, nicht in eine Datei, die verreist |
Bei den Benachrichtigungswegen wurde die härtere Wahl getroffen: Ein einzelnes
Feld zu schwärzen hieße, bei jedem neuen Kanaltyp erneut daran zu denken — und
einmal denkt niemand daran. Was bleibt, ist ein **Merkzettel**: Es gab einen
Kanal dieses Namens, dieser Art, mit dieser Schwelle. Weniger, als man sich
wünscht, und mehr als nichts.
**Die Liste der Auslassungen steht im Sicherungssatz selbst**, nicht nur hier.
Wer nach einem Totalverlust eine Anlage wiederherstellt, hat die Betriebsanleitung
nicht dabei; die Datei muss die Anleitung sein.
## Nichts läuft von selbst wieder an
Nach dem Einspielen gilt:
- **Aufträge kommen angehalten zurück.** Nach einem Totalverlust weiß niemand,
ob die Quellen noch existieren und ob die wiederhergestellten Daten die
richtigen sind. Ein Zeitplan, der um zwei Uhr nachts von selbst anläuft, könnte
auf ein halb wiederhergestelltes System schreiben — und der Betreiber erführe
davon am nächsten Morgen.
- **Repositories kommen als „nicht erreichbar" zurück.** Ob die Ablage am alten
Pfad noch da ist, steht nicht fest. Eines, das als `active` zurückkäme und
fehlte, ließe den nächsten Lauf ins Leere greifen — und die Übersicht meldete
alles in Ordnung.
- **Konten kommen deaktiviert zurück**, Benachrichtigungswege abgeschaltet.
- **`last_verified_at` und `last_restore_test_at` bleiben leer.** Dass ein Backup
vor dem Ausfall geprüft wurde, sagt nichts über seinen heutigen Zustand.
Das Einspielen läuft in **einer** Transaktion: alles oder nichts. Ein Abbruch auf
halber Strecke hinterließe Aufträge ohne Repositories und Quellen ohne Aufträge —
eine Anlage, die arbeitsfähig aussieht und beim ersten Lauf scheitert. Im
Nachweis ist genau das eingetreten (siehe Funde), und die Datenbank blieb leer.
## Der Weg nach einem Totalverlust
```bash
# 1. Ansehen, was da ist — OHNE Datenbank
syncova-dr inspect --repo /pfad/zum/repository
# 2. Schema anlegen
syncova-migrate up
# 3. Konfiguration und Wiederherstellungspunkte einspielen
syncova-dr restore --repo /pfad/zum/repository --catalog
# 4. Ersten Zugang anlegen
syncova-admin create-admin --username <name>
# 5. Repositorypfade prüfen, auf 'aktiv' setzen, Aufträge freigeben
```
Schritt 1 braucht **keine Datenbank** — das ist der Zweck. Wer nach einem Ausfall
vor einem Repository steht, muss sehen können, was darin ist, bevor er
entscheidet, wie er weitermacht.
`--catalog` baut die Wiederherstellungspunkte **aus den Manifesten** neu auf,
nicht aus dem vorhandenen Katalog: Der könnte veraltet oder beschädigt sein, und
darauf darf man sich im Ernstfall nicht verlassen. Verbindlich sind die
Manifeste.
Die Kennungen entstehen deterministisch (UUIDv5 aus Repository-Kennung, Art und
Backupkennung). Eine zweite Übernahme desselben Repositorys ergibt dieselben
Kennungen; mit Zufallswerten entstünden bei jedem Lauf Dubletten, und niemand
könnte sagen, welcher Eintrag der richtige ist.
## Die vier Szenarien im Nachweis
### A und B — Control-Server und Datenbank verloren
`DROP DATABASE syncova` — null Tabellen. Danach: Schema angelegt, Konfiguration
und Katalog aus dem Repository eingespielt, Administrator neu angelegt,
Repository auf „aktiv" gesetzt.
Dann die Probe: **Quellverzeichnis gelöscht**, Wiederherstellung über die API
angestoßen.
```
Vorabprüfung: durchführbar, 3 Dateien / 3.500.040 Byte, 0 fehlende Blöcke
Ergebnis: succeeded, 5 Objekte, 3.500.040 Byte
Prüfsummen: ✓ alle stimmen überein
Symlink: erhalten (unterordner/verweis.txt -> ../rechnung.txt)
```
### C — Repository-Katalog verloren
`rm -rf indexes/` — der Katalog samt Verzeichnis. Danach `syncova-repo list`:
Der Katalog wird stillschweigend aus den Manifesten neu gebaut und wieder
abgelegt.
### D — Netzunterbrechung
Kein simulierter Netzwerkfehler, sondern der härtere Fall: **SIGKILL mitten in
einer Wiederherstellung** von 1500 Dateien.
```
Abbruch nach 8 s: 496 von 1500 Dateien geschrieben, Prüfpunkt bei 367
Freigabe: SCHEDULER_LOST (die Anlage gibt den verwaisten Lauf selbst frei)
Fortsetzung: succeeded, 1500 Dateien, 0 übersprungen
Prüfsummen: ✓ alle 1500 stimmen — bitgenau nach Abbruch und Fortsetzung
```
Der Prüfpunkt hinkt bewusst hinterher (Schreibintervall 5 s): 496 Dateien lagen
am Platz, der Prüfpunkt stand bei 367. Die Fortsetzung schreibt die Differenz
erneut. Das ist gewollt — die Alternative wäre ein Datenbankschreibvorgang je
Datei.
Ein verwaister Lauf wird **nicht** automatisch fortgesetzt: Ein zweiter Lauf in
ein halb gefülltes Ziel kann Daten beschädigen, die der erste bereits am Platz
hatte. Er wird als gescheitert vermerkt und wartet auf einen Blick.
## Funde
Fünf, alle erst durch das reale Durchspielen sichtbar.
**Die Manifest-Statistik war strukturell null.** Die Backup Engine schrieb ihre
Blöcke über `WriteTransformedChunk` unmittelbar am Repository — und damit an der
Zählung der Schreibsession vorbei. Jedes Manifest trug `logical_bytes: 0`. Alle
Tests blieben grün, weil keiner das Manifest auf seine Kennzahlen ansah.
Aufgefallen ist es erst, als das Repository zum ersten Mal als **alleinige**
Quelle diente: Nach dem Verlust der Datenbank meldete jedes Backup die Größe
null. Ein Repository, das seine eigene Größe nicht kennt, ist nicht
selbstbeschreibend — und das ist die zentrale Zusage des Produkts. Die Engine
schreibt jetzt über die Session; deduplizierte Blöcke werden ebenfalls vermerkt,
denn sonst bliebe die Statistik eines Laufs über unveränderte Daten bei null.
**Der Katalog-Neuaufbau scheiterte, wenn das Verzeichnis fehlte.** „Katalog
verloren" heißt nicht immer „Datei gelöscht"; bei einem Teilausfall des
Dateisystems fehlt das ganze Verzeichnis. Ausgerechnet der Neuaufbau, der den
Verlust beheben soll, brach dann ab — mit einer Meldung über eine temporäre
Datei, die nichts über das eigentliche Problem sagte. Die bestehenden Tests
löschten stets nur die Katalogdatei.
**Eine Fortsetzung meldete einen Teilfehler, obwohl das Ergebnis vollständig
war.** Die Dateien zwischen Prüfpunkt und tatsächlichem Fortschritt lagen bereits
am Platz und wurden übergangen — 85 von 1500. Sie stammen aus dem eigenen
abgebrochenen Lauf und werden jetzt ersetzt. Ein Teilfehler, der keiner ist, ist
genau die Meldung, die beim nächsten Mal niemand mehr liest. (Der Schutz gegen
fremde Daten bleibt: Die Lockerung gilt **ausschließlich** bei einer
Fortsetzung.)
**Die beim Absturz geschriebene Datei blieb liegen.** Unter ihrem temporären
Namen, dauerhaft. Im Nachweis lagen 1501 Dateien in einem Ziel, das 1500
enthalten sollte. Eine Fortsetzung räumt jetzt auf, bevor sie beginnt.
**Ein leeres Musterfeld wurde zu NULL.** Beim ersten Einspielversuch brach die
Wiederherstellung an der häufigsten aller Quellen ab — der ohne Filter. Die
Transaktion rollte sauber zurück; die Datenbank blieb leer, statt halb gefüllt
zu sein. Danach folgte gleich der zweite Fehler: Die Spalten sind `jsonb`, nicht
`text[]`.
## Bekannte Grenzen
- **Die Datenbank selbst wird nicht gesichert.** Szenario B geht davon aus, dass
sie verloren ist und aus dem Repository wiederaufgebaut wird. Ein `pg_dump`
daneben ist trotzdem sinnvoll: Er bringt Läufe, Meldungen, Prüfungen und
Kennzahlen zurück, die der Sicherungssatz bewusst nicht enthält.
- **Der Export läuft nicht selbsttätig.** `syncova-dr export` ist ein Kommando;
es gehört in den Zeitplan des Betriebssystems oder hinter jeden Sicherungslauf.
Ein Sicherungssatz von vor drei Monaten kennt die Aufträge von heute nicht.
- **Konten kommen ohne Passwort zurück.** Das ist gewollt, bedeutet aber: Bei
vielen Konten ist der Wiederaufbau Handarbeit.
- **Ein Repository, das der Sicherungssatz nicht kennt, wird abgelehnt.** Die
Kennung im Repository muss zu der im Satz passen; sonst landeten die
Wiederherstellungspunkte unter einer fremden Kennung, und ein späterer Restore
suchte sie am falschen Ort.
- **Szenario D ist gegen einen Serverabsturz geprüft, nicht gegen eine echte
Netzunterbrechung.** Für ein lokales Repository sind beide Fälle gleich; bei
einem entfernten Ziel käme das Verhalten des Netzwerkstapels hinzu.