Files
management/docs/issues/0030-der-restore-ist-nie-geprobt-sicherungen-sind.md
T
Thore CimbalandClaude Opus 4.8 546e444a14 docs(issues): correct the mirror recommendation for the notfallhandbuch
Not mirroring it is deliberate: the handbook maps the infrastructure, backup
locations and where the keys live, so putting it on the internet-facing Gitea
hands an attacker the roadmap once the stack is compromised. My earlier
recommendation only weighed availability and was wrong. Local clone covers the
availability gap. Flagged that this is a standing exception to ADR-0001 and
would warrant its own ADR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 12:00:00 +00:00

193 lines
12 KiB
Markdown
Raw 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.
---
type: issue
id: "0030"
status: in-progress
created: 2026-08-06
milestone: M1
priority: medium
area: security
gitlab_iid: "30"
related: []
---
# Der Restore ist nie geprobt — Sicherungen sind bisher eine Vermutung
> Import aus [management#30](https://git.lab/axion1337.chat/management/-/issues/30) (2026-08-11). Kommentare und Verlauf bleiben dort; kanonisch ist ab jetzt diese Datei (ADR-0012).
Es wird gesichert: jeden Sonntag, alle Dienste, drei Versionen vorgehalten, GitLab auf Overmind mit Datenbank **und** Volumes nach MinIO auf dem DSM. Das ist mehr, als die meisten haben.
**Was fehlt, ist der Beweis, dass sich daraus etwas wiederherstellen lässt.** Es gibt kein dokumentiertes Verfahren und keinen je durchgespielten Versuch. Eine Suche über `docs/` und `verfahren/` findet nur Erwähnungen in `install.md` — keine Anleitung, keine Protokolle.
## Warum das der wichtigste der offenen Punkte ist
Eine Sicherung, die nie zurückgespielt wurde, ist eine **Vermutung**. Die typischen Fehler zeigen sich ausschließlich beim Zurückspielen und nie beim Sichern:
- die Datenbank ist gesichert, aber ohne das Volume mit den Uploads ist sie wertlos
- der Dump ist da, aber der Verschlüsselungsschlüssel lag nur auf dem Host, der weg ist
- es liegen drei Versionen, aber alle drei sind seit Wochen leer, weil ein Pfad umgezogen ist und keiner es gemerkt hat
- niemand weiß, in welcher Reihenfolge die Dienste hochkommen müssen
Der letzte Punkt ist hier besonders relevant: **Der SOPS-age-Schlüssel entschlüsselt alle Secrets im Cluster.** Wenn der nur an einer Stelle liegt, ist die Frage nicht, ob die Sicherung funktioniert, sondern ob sie überhaupt etwas nützt.
## Was zu tun ist
1. **Zuerst das Billigste:** stichprobenartig in die aktuellen Sicherungen hineinschauen. Sind sie plausibel groß? Enthalten sie, was sie sollen? Das findet stille Ausfälle sofort.
2. Ein echtes Wiederherstellungsverfahren schreiben — als Ablauf, nicht als Prosa: welcher Dienst zuerst, woher der SOPS-Schlüssel, woher der kubeconfig.
3. **Einmal wirklich durchspielen**, gegen eine Wegwerf-Umgebung, nicht gegen die Produktion. Was dabei fehlt, ist das Ergebnis.
4. Ergebnis als Verfahren in `verfahren/` ablegen und danach in bekanntem Abstand wiederholen.
⚠️ Bewusst **nicht** vorschlagen: die Sicherung erweitern, bevor die vorhandene geprüft ist. Mehr zu sichern, ohne zu wissen, ob das Vorhandene trägt, verschiebt das Problem nur.
## Grenzen dieses Issues
Ich kenne den Sicherungsaufbau nur aus deiner Beschreibung und einem Screenshot, nicht aus eigener Anschauung. Der erste Schritt ist deshalb Bestandsaufnahme, nicht Bewertung.
*Aufgenommen am 2026-08-06 bei einer Bestandsaufnahme der Sicherheitslage.*
## Schritt 1 erledigt 2026-08-14 — Bestandsaufnahme (Cluster-Seite)
Wie im Issue gefordert zuerst „das Billigste": in die vorhandenen Sicherungen hineingeschaut,
nichts erweitert. Geprüft wurde die **Matrix-Cluster-Seite** (Hetzner/K3s); die Homelab-Seite
(GitLab auf Overmind → MinIO/DSM) ist von hier nicht erreichbar und weiterhin ungeprüft.
**Befund: die Sicherungen selbst sind besser als vermutet.** Drei nächtliche CronJobs, alle
`Complete`, keiner suspendiert, alle **off-host** per Borg auf eine Hetzner Storage Box
(`u641795@…your-storagebox.de:23`) — also genau das Muster, das #0010 für Gitea erst plant:
| Job | Zeit | Ziel-Repo | Volumen (letzter Lauf) | Bewertung |
|---|---|---|---|---|
| `synapse-backup` (matrix) | 03:00 | `/./synapse-backup` | 199,5 MB, 247 Dateien, Dedup-Delta 3,7 MB | plausibel ✅ |
| `authentik-backup` (authentik) | 03:15 | `/./authentik-backup` | ~150 MB gesamt, Delta ~15 MB | plausibel ✅ |
| `wikijs-backup` (matrix) | 03:30 | `/./wikijs-backup` | 223 kB | plausibel ✅ (nur Postgres; die Wiki-**Inhalte** liegen per git-storage in Gitea/git.lab) |
Retention greift nachweislich (`borg prune`, 7 daily / 4 weekly / 6 monthly, „Deleted data"
in den Logs). Kein stiller Ausfall, keine leeren Archive — die Sicherung ist **keine bloße
Vermutung mehr**, zumindest was Existenz und Inhalt angeht.
### ⚠️ Kritischer Befund: Zirkelabhängigkeit beim age-Schlüssel
Genau das vom Issue vorhergesagte Muster („der Dump ist da, aber der Schlüssel lag nur auf dem
Host, der weg ist") liegt real vor:
1. Zum **Lesen** der Borg-Repos braucht man Borg-Passphrase **und** SSH-Key.
2. Beide liegen in `synapse-backup-secret.yaml` / `authentik-backup-secret.yaml`**SOPS-verschlüsselt**.
3. Entschlüsselbar ist das nur mit **einem einzigen** age-Schlüssel
(Empfänger `age14l0hw…`, siehe `.sops.yaml`).
4. Dieser private Schlüssel existiert an **genau zwei Orten**, und beide sind „heiß":
- `sops-age`-Secret in `flux-system`**im Cluster, den das Backup schützen soll**
- `~/.age/keys.txt` auf sorbs Mac — **ein einzelnes Gerät**
**Folge:** Gehen Cluster und Mac zusammen verloren (Totalschaden Hetzner + Laptop weg/defekt),
sind **alle drei Borg-Repos dauerhaft unlesbar**. Die Sicherungen wären technisch einwandfrei
und trotzdem wertlos. Eine Suche über `docs/` und `hosts/` findet **keine** dokumentierte
Auslagerung (Escrow, Offline-Kopie, Passwort-Manager) des Schlüssels.
**Billigste wirksame Gegenmaßnahme (vor jedem Restore-Test):** eine **kalte Kopie** des
age-Schlüssels außerhalb von Cluster und Mac anlegen — Passwort-Manager und/oder Ausdruck an
sicherem Ort — und die Fundstelle in `hosts/` dokumentieren (nur *wo*, nie der Wert). Erst danach
lohnt der eigentliche Restore-Durchlauf, sonst probt man einen Ablauf, dessen Voraussetzung
selbst ungesichert ist.
### Nächste Schritte (unverändert nach Issue-Plan)
2. Restore-Verfahren schreiben (Reihenfolge, Herkunft von age-Key und kubeconfig).
3. Einmal gegen eine Wegwerf-Umgebung durchspielen.
4. Ergebnis nach `verfahren/` und Wiederholungsrhythmus festlegen.
## Update 2026-08-14 — age-Schlüssel ist ausgelagert (Befund entschärft)
sorb bestätigt: der private age-Schlüssel liegt **zusätzlich im Passwort-Vault**, also außerhalb
von Cluster und Mac. Damit ist die oben beschriebene Zirkelabhängigkeit **aufgelöst** — ein
gleichzeitiger Verlust von Hetzner-Host und Laptop macht die Borg-Repos nicht mehr unlesbar.
Der Befund bleibt hier stehen, weil die Kette (Backup lesen → Borg-Passphrase → SOPS → **ein**
age-Schlüssel) beim Schreiben des Restore-Verfahrens explizit auftauchen muss: der Vault ist
Teil des Wiederherstellungswegs, nicht nur Nebensache.
**Zu ergänzen beim Verfahren (Schritt 2):** Fundort des Schlüssels benennen (nur *wo*, nie der
Wert) und im Restore-Ablauf als ersten Schritt führen — ohne ihn ist kein weiterer Schritt
möglich.
## Restaufwand nach heutiger Bestandsaufnahme
Schritt 1 (Bestandsaufnahme) ist erledigt und positiv ausgefallen; Schritt 24 stehen aus:
Restore-Verfahren schreiben, einmal gegen eine Wegwerf-Umgebung durchspielen, Ergebnis nach
`verfahren/` und Wiederholungsrhythmus. Die Homelab-Seite (GitLab auf Overmind → MinIO/DSM)
ist weiterhin ungeprüft — von der Hetzner-Seite aus nicht erreichbar.
## Schritt 2 erledigt 2026-08-14 — Restore-Verfahren geschrieben
[`docs/wiki/deployment/restore.md`](../wiki/deployment/restore.md) — als Ablauf, nicht als Prosa:
Voraussetzungen (age-Key aus dem Vault zuerst), Fundort und Layout der drei Borg-Repos,
Bootstrap-Reihenfolge (Host/K3s → die zwei manuellen Secrets `sops-age` + `flux-system` → Flux →
`infra-apps` → production/authentik/monitoring), Daten-Rückspielung und Verifikation.
**Abweichung vom Issue-Wortlaut:** abgelegt unter `docs/wiki/deployment/` statt `verfahren/`
dort liegt bereits die Deploy-Übergabe, nur `docs/**` wird von `validate.py` geprüft, und die
Seite ist über den Wiki-Index auffindbar. `verfahren/` enthält bislang ausschließlich Skripte.
**Zwei Erkenntnisse beim Schreiben, die vorher nicht dokumentiert waren:**
1. **Flux zieht aus Gitea (rohana), nicht aus git.lab.** Ist rohana beim Ausfall ebenfalls weg,
hängt der Wiederanlauf an einem Host, der gar nicht Teil des Backup-Konzepts ist — dann muss
zuerst eine erreichbare Git-Quelle hergestellt und `gotk-sync.yaml` umgebogen werden. Im
Verfahren als Warnung vermerkt.
2. **Konsumenten müssen vor dem `pg_restore` heruntergefahren werden.** Synapse/MAS/Authentik/
Wiki.js legen beim Start ein leeres Schema an, gegen das ein Restore kollidiert.
Ebenfalls festgehalten: Borg nutzt `repokey-blake2`, die Passphrase allein genügt (keine separate
Schlüsseldatei), und Passphrase/SSH-Key sind **ohne Cluster** per `sops -d` aus einem lokalen
Clone lesbar — der age-Key aus dem Vault ist damit tatsächlich der einzige harte Startpunkt.
**Offen: Schritt 3** (einmal gegen eine Wegwerf-Umgebung durchspielen) und **Schritt 4**
(Wiederholungsrhythmus). Das Verfahren ist bis dahin abgeleitet, aber unerprobt.
## Schritt 3 (teilweise) erledigt 2026-08-14 — Restore-Probe bestanden
Eigenes Repo **`git.lab/axion1337.chat/notfallhandbuch`** angelegt (Wunsch sorb): Einstiegs-
README, das Verfahren (`restore-matrix.md`) und ein menügeführtes Werkzeug (`notfall.sh`).
Das Verfahren wurde aus `docs/wiki/deployment/` dorthin verschoben — hier steht nur noch ein
Zeiger. Begründung: im Ernstfall will man **einen** Clone, nicht drei Repos absuchen.
**Der Beweis, den dieses Issue verlangt, ist für die Datenbanken erbracht.** `notfall.sh`
Stufe 3 spielt die Sicherungen in eine **Wegwerf-Postgres im Pod** zurück — isoliert, die
Produktion bleibt unberührt, beliebig wiederholbar:
| Repo | Datenbank | zurückgespielte Zeilen |
|---|---|---|
| synapse-backup | `synapse` | **31.908** |
| synapse-backup | `matrixauthenticationservice` | **16.085** |
| authentik-backup | `authentik` | **325.149** |
| wikijs-backup | `wiki` | **251** |
Die Sicherungen sind damit **keine Vermutung mehr** — sie wurden tatsächlich zurückgespielt.
**Entwurfsentscheidungen des Werkzeugs (bewusst, nicht beiläufig):**
- Arbeit läuft **im Cluster**, nicht auf dem Laptop: `axion-backup:v2` bringt borg + pg-Tools
mit, die Backup-Secrets verlassen den Cluster nicht, lokal genügt `kubectl`.
- `whiptail` wird genutzt **falls vorhanden**, sonst Textmenü — ein Notfallwerkzeug darf nicht
mit „installier erst ein Paket" beginnen (auf dem Mac fehlt whiptail).
- **Bestehenskriterium ist die Zeilenzahl, nicht der Exitcode.** Die erste Fassung meldete bei
fehlgeschlagenem `pg_restore` fälschlich „OK" (Pipe verschluckte den Code) — genau der
stille Ausfall, den dieses Issue beschreibt, nur im Prüfwerkzeug selbst. Behoben und
gegengetestet.
**Weiterhin offen (Rest von Schritt 3 + Schritt 4):**
- **Phase A/B nie durchgespielt:** Wiederanlauf auf leerem Host (K3s, die zwei Bootstrap-
Secrets, Flux) ist abgeleitet, nicht getestet — das braucht eine Wegwerf-Umgebung.
- **Synapse-Medien** (`media_store` → PVC) nicht erprobt; ohne sie sind Bilder in Räumen tote Links.
- **Wiederholungsrhythmus** festlegen (Stufe 3 ist gefahrlos → z.B. monatlich).
- Homelab-Seite (GitLab/Overmind → MinIO/DSM) weiterhin ungeprüft.
**Keine Spiegelung — bewusste Ausnahme (Entscheidung sorb 2026-08-14):** Anders als alle
übrigen Repos wird das Notfallhandbuch **nicht** nach Gitea gespiegelt. Es beschreibt die
Infrastruktur, den Ablageort der Sicherungen und wo die Schlüssel liegen; auf dem öffentlich
erreichbaren Gitea wäre es bei einer Kompromittierung des Stacks genau die Landkarte, die ein
Angreifer braucht. Vertraulichkeit vor Verfügbarkeit. (Meine ursprüngliche Empfehlung, zu
spiegeln, war damit falsch — sie hatte nur die Verfügbarkeit im Blick.) Die Verfügbarkeitslücke
— git.lab ist nur im Lab erreichbar — wird über einen **lokalen Clone** abgedeckt, nicht über
einen Mirror. Begründung steht im README des Notfallhandbuchs, damit sie nicht erneut
"wegoptimiert" wird.
Das ist eine **dauerhafte Ausnahme von der Spiegel-Topologie** (ADR-0001) — nach dem Framework
(ADR-Pflicht auch für dauerhafte Regel-Ausnahmen) wäre dafür ein eigener ADR fällig; bislang nur
hier und im README festgehalten.