17 Commits

Author SHA1 Message Date
renovate 0fed6a6a0b chore(deps): update minor-and-patch-updates 2026-06-03 16:20:31 +00:00
Micha c47639ecf4 docs(host): Fix Common Problems Plugin deinstalliert (2026-06-03)
Befund: Drei `grep -R ... /usr/local/emhttp`-Prozesse aus einem FCP-Daily-
Scan-Run hingen seit ~7 Tagen in einem Symlink-Loop. Unraids
`/usr/local/emhttp/mnt` ist ein Symlink nach `/mnt` (mehrere TB Array);
GNU `grep -R` dereferenziert Symlinks, also walking die FCP-Scan-Greps
effektiv das gesamte Array. 3 Cores dauerhaft 100 %, IOWAIT-Peaks 55 %,
USB-Flash unter Dauer-IO, Load 14.6 auf 12 Cores.

Massnahme: `plugin remove fix.common.problems.plg`. Cron, Plugin-Dir
und /tmp-Reste sauber. Load von 14.6 auf 1.08 (1-min) gefallen.

Entscheidung: FCP wird bewusst nicht reinstalliert. Begruendung im
Architektur-Master Sektion 13. Verbleibende Risiken decken Scrutiny,
Monitoring, Posture-Check und Critical-Events-Watcher bereits ab.

Repo-Aenderungen:
- HOMELAB_ARCHITECTURE_MASTER_V2.md Sektion 13: vollstaendiger
  Entscheidungs-Log-Eintrag mit Ursache, Massnahme, Begruendung
- AUDIT_2026-05-25_TODO.md "Zuletzt geschlossen": Kurzfassung

Host-Aenderung wurde via SSH durchgefuehrt (read+remove), keine
Compose-/Container-Aenderungen.
2026-06-03 16:29:33 +02:00
Micha b158f9d871 docs(dr): GitHub-Mirror Read-Only Deploy-Key gesichert (2026-06-03)
Zweite der vier P1-Operator-Aufgaben aus dem DR-Tabletop erledigt.

Was passiert ist:
- SSH-Keypair `dr-readonly-2026-06-03` (ed25519, Passphrase-frei) erzeugt
- Public Key in GitHub Repo Settings -> Deploy Keys ohne Write-Access
  hinterlegt (Title `DR Read-Only 2026-06-03`)
- Smoke `git ls-remote git@github.com:michaelkaleschke-spec/homelab-infra.git`
  erfolgreich (HEAD `d947c7f` matched origin/master)
- Private Key offline neben die KOMODO_*-Notiz gelegt
- Arbeitsplatz-Kopie auf dem Operator-PC nach USB-Transfer geloescht

EXTERNAL_DEPENDENCIES.md:
- GitHub-Mirror-Zeile von "noch nicht angelegt" auf "offline gesichert"
  gezogen, inkl. Deploy-Key-Bezeichnung und Smoke-Bestaetigung
- DR-Workstation-Kit-Tabelle: Quartals-Smoke-Befehl mit konkretem
  GIT_SSH_COMMAND-Aufruf dokumentiert
- Review-Zeile 2026-06-03 erweitert

AUDIT_2026-05-25_TODO.md:
- P1-Read-PAT-Eintrag aus offenen Punkten entfernt
- Eintrag unter "Zuletzt geschlossen" mit Wirkung

Zwei P1-Operator-Aufgaben bleiben offen: DR-Workstation-Setup,
Nextcloud-Restore-Test.
2026-06-03 16:13:29 +02:00
Micha d947c7f066 docs(dr): KOMODO_*-Notiz offline gesichert (Operator-Bestaetigung 2026-06-03)
DR-Tabletop-Followup: erste der vier P1-Operator-Aufgaben erledigt.

EXTERNAL_DEPENDENCIES.md:
- KOMODO_*-Notiz-Zeile von "noch nicht angelegt" auf "offline gesichert
  (Operator-Bestaetigung)" gezogen, mit Hinweis auf die Quelle der Werte
  (Self-Stack-.env unter /mnt/user/services/stacks/komodo bzw. die
  Drift-Recovery-Kopie vom 2026-05-04)
- DR-Workstation-Kit-Tabelle: Offline-Kopie-Status entsprechend aktualisiert
- Review-Zeile 2026-06-03 mit Bestaetigung ergaenzt

AUDIT_2026-05-25_TODO.md:
- P1-KOMODO_*-Notiz aus den offenen Punkten entfernt
- Eintrag unter "Zuletzt geschlossen" mit Quellenpfad und Wirkung

Drei P1-Operator-Aufgaben bleiben offen: GitHub-Read-PAT,
DR-Workstation-Setup, Nextcloud-Restore-Test.
2026-06-03 16:05:27 +02:00
Micha 9edd6c24e6 docs(dr): tabletop-folge - DR.md + EXTERNAL_DEPENDENCIES haerten
Reine Doku-Fixes nach DR-Tabletop 2026-06-03 und Operator-Antworten auf
vier offene Fragen.

DISASTER_RECOVERY.md:
- Abschnitt 3 Voraussetzungen: Operator-DR-Workstation als Pflichtposten
- Phase 0: privater GitHub-Mirror, Read-PAT/Deploy-Key, expliziter Repo-
  Bootstrap-Pfad Workstation -> Unraid
- Abschnitt 6.1: homelab_smtp_password.txt, n8n_encryption_key.txt,
  monitoring/influxdb/filebrowser Secrets nachgezogen
- Neuer Abschnitt 7.3: Borg-Extract ohne borg-ui (DR-Workstation oder
  docker run borgbackup/borg), Passphrase-Eingabe interaktiv
- Phase 4 neue Stufe 0 "Docker-Grundlage": docker network create
  frontend_net/backend_net/monitoring_net + dynamic/ Pre-Check
- Phase 4 Stufe 1: LE-Staging-Hinweis bei verlorenem acme.json
- Phase 4 Stufe 3 "Wichtige Stolperfallen": KOMODO_*-Quelle, Mongo-
  Datadir/Secret-Mismatch, extra_hosts-IP, Stack-ENV-Wiederherstellung
- Phase 5.3: App-DB-Verifikation per docker logs

EXTERNAL_DEPENDENCIES.md:
- GitHub-Mirror als privat klargestellt + Read-PAT/Deploy-Key Pflicht
- Operator-DR-Workstation als kritische Abhaengigkeit
- KOMODO_*-Notiz und GitHub-Read-PAT als noch nicht angelegt erfasst
- Hetzner-Maintenance-Key offline bestaetigt (Operator-Antwort 2026-06-03)
- Neuer Abschnitt "DR-Workstation Bare-Metal-Kit" mit konkretem Inhalt

AUDIT_2026-05-25_TODO.md:
- Vier neue P1-Operator-Aufgaben: KOMODO_*-Notiz, Read-PAT, DR-Workstation-
  Setup, Nextcloud-Restore-Test scharf laufen lassen

DR_DRILL_2026-06-03.md:
- Folge-Iteration-Tabelle: welcher Finding wo adressiert wurde

Operator-Aufgaben (nicht delegierbar) sind als P1 markiert. Nichts in
Runtime/Compose beruehrt, kein Container gestartet.
2026-06-03 16:00:00 +02:00
Micha 7a513e9fc8 docs(dr): tabletop drill 2026-06-03 - findings against DISASTER_RECOVERY
Kalter Lesetest gegen das Bare-Metal-Szenario aus DR.md Phase 0 bis 5,
mit referenzierten Runbooks (SERVICES_RECOVERY, RESTORE_MATRIX,
SECRETS_MAP, RESTORE_HANDBOOK, EXTERNAL_DEPENDENCIES) und Compose-Ankern
(ops/komodo, traefik).

23 Findings mit Severity, Repo-Datei + Zeile, Fix-Vorschlag pro Punkt.
1x CRITICAL (Unraid-Flash-Restore ohne laufenden Host), 11x HIGH, 8x MED,
3x LOW.

Schwerpunkte:
- Bare-Metal-Operator-Workstation als DR-Voraussetzung nicht dokumentiert
- Henne-Ei: KOMODO_* externe Notiz vs. Vaultwarden-Reihenfolge
- Externe Docker-Netze fehlen in DR.md Phase 4 Stufe 1
- borg-ui-Container als impliziter Borg-Client im Bare-Metal-Bootstrap
- Nextcloud-Restore-Skript ist da, ist aber noch nie real gelaufen (X-1)

Keine produktiven Pfade beruehrt, kein Container gestartet, keine
Skripte ausgefuehrt - reiner Doku-Drill.
2026-06-03 15:48:44 +02:00
Micha 4b96d13510 security(authelia): borg-ui und code-server auf two_factor heben
Beide UIs haben effektiv Host-/Backup-Zugriff (Borg-Restore-Scope inkl.
/local/secrets, code-server mit Workspace-Mounts). Bisher liefen sie ueber
die catch-all-Regel mit nur one_factor. Files und Scrutiny waren bereits
two_factor; die Liste wird konsistent gezogen.

Wirkung erst nach manuellem Host-Merge (Ausnahme laut docs/WORKFLOW.md):
1. /mnt/user/appdata/authelia/config/configuration.yml mergen
2. docker restart authelia
3. Smoke-Test auf einer der vier 2FA-Domains
4. services/authelia-diff.sh muss exit 0 liefern

Audit-Restliste nachgezogen: Tier-1-Operator-2FA geschlossen, restliche
geparkte Auth-Themen (OIDC, CrowdSec, Nextcloud-2FA) bewusst weiter offen
mit aktualisierter Begruendung.
2026-06-03 15:03:15 +02:00
Micha 642eb88b40 docs(restore): traefik restore successful - 11 of 12 tests green
Traefik-Restore am 2026-06-03 erfolgreich: dynamic/ (2 Files) +
letsencrypt/acme.json (426K) aus Borg, File-Provider-Boot, /ping 200.
Erster Versuch, kein shfs-Problem.

11 von 12 Restore-Tests sind jetzt gruen. Einzig Nextcloud bleibt
blockiert durch Unraids shfs-chmod-Inkompatibilitaet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 14:45:27 +02:00
Micha dd494046ce feat(restore): traefik restore smoke test
Borg-Extract von dynamic/ und letsencrypt/, Traefik startet mit
File-Provider gegen restaurierte Config, /ping Health antwortet.

Bewusst kein docker.sock (wuerde produktive Container discovern),
kein CF-Token (keine DNS-Challenge), keine produktiven Ports.
acme.json-Existenz und -Groesse wird geprueft, TLS-Validitaet nicht.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 14:42:56 +02:00
Micha 16d3b8f2fa docs(restore): mailarchiver restore successful, update matrix and backlog
Mail-Archiver-Restore am 2026-06-03 erfolgreich: Data-Protection-Keys
aus Borg + 645M pg_restore + HTTP 200. Erster Versuch, kein shfs-Problem.

10 von 12 Restore-Tests sind jetzt gruen. Verbleibend: Nextcloud
(blockiert/shfs-chmod) und Traefik (komplex, niedrigere Prio).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 14:08:28 +02:00
Micha a9b232195d feat(restore): mail-archiver restore smoke test
Borg-Extract der Data-Protection-Keys + pg_restore des 645M
mailarchiver-Dumps in isoliertes Test-Postgres + Container-Boot +
HTTP-Smoke. Wegwerf-DB-Connection und Auth-Password, kein produktiver
Stack-ENV, kein Authelia-ForwardAuth im Smoke.

Machbarkeit vorab verifiziert: Dump vorhanden, App-Image gepinnt,
Data-Protection-Keys im Borg, .NET-App hat kein shfs-chmod-Problem.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 14:01:47 +02:00
Micha 5ee4a158d6 docs(restore): mealie restore successful, update matrix and backlog
Mealie-Restore-Test am 2026-06-03 erfolgreich: Borg-Data + pg_restore
+ HTTP 200, 3 Rezepte im Test-DB-Check. Erster Versuch, kein
shfs-Problem (Mealie startet als root, kein chmod auf User Shares).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 13:54:05 +02:00
Micha 86435d4091 feat(restore): mealie restore test + freshness check negativ-test fix
Mealie-Restore-Test: Borg-Extract der App-Daten + pg_restore in
isoliertes Test-Postgres + Mealie-Boot + HTTP /api/app/about Smoke.
Machbarkeit vorab verifiziert (kein shfs-chmod-Problem, Mealie laeuft
als root und switcht intern auf PUID 99).

Freshness-Check: pg_header_ok() Docker-Fallback lieferte bei korruptem
Dump return 2 (unchecked) statt return 1 (invalid). Negativ-Test am
2026-06-03 bewiesen: korrupter mealie.dump wird jetzt als
DUMP_HEADER_INVALID erkannt (Critical, Exit 1).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 13:49:51 +02:00
Micha 5e52316fab fix(restore): freshness check pg_header_ok returns 1 on corrupt dump
Negativ-Test 2026-06-03: korrupter mealie.dump wurde nicht erkannt,
weil der Docker-Fallback-Pfad nach gescheitertem pg_restore --list
zu return 2 (unchecked) durchfiel statt return 1 (invalid).

Fix: explizites if/else statt &&-Kette, damit fehlgeschlagene
Header-Validierung return 1 liefert und als DUMP_HEADER_INVALID
in den Critical-Zaehler geht.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 13:47:08 +02:00
Micha 8a4df239fa fix(restore): pin komodo test mongo to 8.0.23 matching production
Produktive Mongo ist 8.0.23, Test-Composes pinnten noch 7.0.32.
Eliminiert die Cross-Version-Warnung beim mongorestore.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 13:44:58 +02:00
Micha 893b34a585 docs(restore): shared pg cluster drill successful, all 5 DBs restored
Shared PostgreSQL 18 Cluster Restore Drill am 2026-06-03 erfolgreich:
Globals + 5 per-DB Custom-Format-Dumps, 290 Tabellen gesamt,
data_checksums=on. Alle P1-Backlog-Punkte sind damit erledigt.

Ergebnis pro DB:
- paperless:    72 Tabellen
- mailarchiver:  1 Tabelle
- authelia:     25 Tabellen
- nextcloud:   126 Tabellen
- mealie:       66 Tabellen

Mailarchiver-Bootstrap-Rollenkonflikt wurde wie dokumentiert toleriert.
Lauf dauerte ~14 Minuten (mailarchiver.dump = 645M).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 13:17:35 +02:00
Micha d1f9491b24 feat(restore): shared postgresql 18 cluster restore drill
Kompletter Restore-Drill fuer den Shared-PostgreSQL-18-Cluster:
globals (Rollen) + 5 per-DB Custom-Format-Dumps (paperless,
mailarchiver, authelia, nextcloud, mealie).

Bekannter mailarchiver-Bootstrap-Rollenkonflikt wird toleriert.
Authelia/Nextcloud/Mealie-Dumps als optional markiert.
Tabellen-Count pro DB als fachlicher Sanity-Check.

Machbarkeit vorab verifiziert: alle Dumps auf Host vorhanden,
pg_restore im postgres:18.4-Image verfuegbar, Postgres auf shfs
bewiesen durch bestehende Tests.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 13:02:16 +02:00
26 changed files with 1519 additions and 30 deletions
+16
View File
@@ -464,6 +464,22 @@ Damit ist sofort klar:
## 13. Betriebserfahrungen und Entscheidungs-Log ## 13. Betriebserfahrungen und Entscheidungs-Log
### Fix Common Problems Plugin entfernt (2026-06-03)
Befund: Drei `grep -R ... /usr/local/emhttp`-Prozesse liefen seit ~7 Tagen durchgehend mit je 100 % CPU (TIME+ 177-179 h). Status `R`, von PID 1 adoptierte Zombies einer laengst beendeten Fix-Common-Problems-(FCP)-Scan-Session. Folge: konstante Load 14.6 auf 12 Cores, IOWAIT-Peaks bis 55 %, USB-Flash unter Dauer-IO.
Ursache: Unraids `/usr/local/emhttp` enthaelt Symlinks `mnt -> /mnt` (mehrere TB Array) und `boot -> /boot` (USB-Flash). GNU `grep -R` dereferenziert Symlinks rekursiv. Ein FCP-Scan-Schritt (`/etc/cron.daily/fix.common.problems.sh -> scripts/scan.php`) hat dadurch effektiv die gesamte Array-Struktur gegrept und ist beim ersten Treffer-Loop haengen geblieben. Der Lock `/tmp/fix.common.problems/scanRunning` war vom 2026-06-03 04:40 - jeder weitere Daily-Cron-Run wuerde dasselbe Verhalten reproduzieren.
Massnahme: FCP-Plugin per `plugin remove fix.common.problems.plg` deinstalliert. Cron-Eintrag, Plugin-Verzeichnis und `/tmp`-Reste sauber. Load fiel innerhalb Minuten auf 1.08 (1-min).
Entscheidung: FCP wird bewusst **nicht** wieder installiert. Begruendung:
- Restliche Risiken werden bereits ueber andere Wege abgedeckt: Scrutiny (Laufwerks-SMART), Monitoring-Stack (Container-Health, Prometheus-Alerts, Blackbox), Posture-Check (Filesystem-/Drift-/Authelia-Audit), Critical-Events-Watcher (`services/posture-check/docker-critical-events.sh`).
- FCP ist ein externes Community-Plugin und nicht Teil der Repo-managed GitOps-Welt; Verhalten haengt von einer Online-Templates-Datei ab.
- Ein einmaliges Hang-up reicht, um die Flash-Drive 7 Tage lang zu thrashen - das Verhaeltnis Nutzen/Risiko ist negativ.
Folgen fuer Doku: Eintrag in `docs/AUDIT_2026-05-25_TODO.md` unter "Zuletzt geschlossen"; FCP taucht nicht mehr als Voraussetzung in DR/Monitoring-Pfaden auf, da es nie produktiv referenziert war.
### Plex Server Reclaim und LAN-only-Profil (2026-05-28) ### Plex Server Reclaim und LAN-only-Profil (2026-05-28)
Befund: Die `Preferences.xml` des Plex-Servers war seit dem 18.05.2026 13:18 jungfraeulich (391 Bytes, ohne `PlexOnlineMail`/`PlexOnlineUsername`/`PlexOnlineToken`). Der Server war damit nicht mit einem Plex.tv-Account geclaimt, obwohl die Smart-TVs ueber LAN-Discovery (mDNS/Plex-GDM) weiter funktionierten. Beim Login als `Xeridos` ueber `app.plex.tv` meldete der Server "Keine Berechtigung", weil kein Owner registriert war. Zusaetzlich war die `library_sections`-Konfiguration leer (Backups vom 19./22./28.05. ebenfalls ~370 KB statt MBs/GBs); die Bibliotheks-Konfiguration war seit dem 18.05. weg, die Filmdateien unter `/mnt/user/media/*` blieben aber intakt (~833 Verzeichnisse, davon `movies/` 1.4 TB und `Heimatfilme/` 300 GB). Befund: Die `Preferences.xml` des Plex-Servers war seit dem 18.05.2026 13:18 jungfraeulich (391 Bytes, ohne `PlexOnlineMail`/`PlexOnlineUsername`/`PlexOnlineToken`). Der Server war damit nicht mit einem Plex.tv-Account geclaimt, obwohl die Smart-TVs ueber LAN-Discovery (mDNS/Plex-GDM) weiter funktionierten. Beim Login als `Xeridos` ueber `app.plex.tv` meldete der Server "Keine Berechtigung", weil kein Owner registriert war. Zusaetzlich war die `library_sections`-Konfiguration leer (Backups vom 19./22./28.05. ebenfalls ~370 KB statt MBs/GBs); die Bibliotheks-Konfiguration war seit dem 18.05. weg, die Filmdateien unter `/mnt/user/media/*` blieben aber intakt (~833 Verzeichnisse, davon `movies/` 1.4 TB und `Heimatfilme/` 300 GB).
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
n8n: n8n:
image: docker.n8n.io/n8nio/n8n:2.22.6@sha256:07138bb60aee990651e9c2090d7dde330cba3a5bd84fcc5cba63b2997243bc45 image: docker.n8n.io/n8nio/n8n:2.25.2@sha256:213380272bfe06f1700cdd398a6e996b496e37329c9c116d13b04811c10e7452
container_name: n8n container_name: n8n
restart: unless-stopped restart: unless-stopped
+14 -7
View File
@@ -7,6 +7,8 @@ Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Gi
| Prioritaet | Punkt | Naechster Schritt | | Prioritaet | Punkt | Naechster Schritt |
|---|---|---| |---|---|---|
| P1 | DR-Workstation Bare-Metal-Kit auf Gaming-PC einrichten | WSL2 installieren, `borgbackup` apt-installieren, SSH-Key fuer Hetzner Storage Box generieren und auf der Box autorisieren, `borg list <hetzner-repo>` als Test laufen lassen. Bestandteile dokumentiert in `docs/EXTERNAL_DEPENDENCIES.md` Abschnitt "DR-Workstation Bare-Metal-Kit" |
| P1 | Nextcloud-Restore-Test scharf laufen lassen | Skript `ops/restore-tests/nextcloud-restore-test.sh` existiert bereits. Einmal ausfuehren, Report unter `/mnt/user/backups/restore-reports/nextcloud-...md` ablegen. Schliesst die letzte Tier-2-Restore-Luecke |
| P2 | Family-Onboarding praktisch starten | Fokus: Vaultwarden als Passwortbasis, Immich-Mobile-Backup auf jedem Handy, Mealie mit erstem Rezept/Einkaufsliste; Ablauf steht in `docs/FAMILY_ONBOARDING.md` | | P2 | Family-Onboarding praktisch starten | Fokus: Vaultwarden als Passwortbasis, Immich-Mobile-Backup auf jedem Handy, Mealie mit erstem Rezept/Einkaufsliste; Ablauf steht in `docs/FAMILY_ONBOARDING.md` |
## Restore-Audit Backlog (Stand 2026-06-03) ## Restore-Audit Backlog (Stand 2026-06-03)
@@ -16,11 +18,11 @@ Ergebnis des Restore-Skills-Audits (Session 2026-06-02/03). Die kritischen Bugfi
| Prioritaet | Punkt | Status | Naechster Schritt | | Prioritaet | Punkt | Status | Naechster Schritt |
|---|---|---|---| |---|---|---|---|
| P1 | Nextcloud-Restore-Test | offen | Test nach Paperless-Muster, zusaetzlich `occ maintenance:mode`-Choreographie und `oc_admin`-Rolle. Hoechster Lerngewinn unter den fehlenden Tier-2-Tests | | P1 | Nextcloud-Restore-Test | offen | Test nach Paperless-Muster, zusaetzlich `occ maintenance:mode`-Choreographie und `oc_admin`-Rolle. Hoechster Lerngewinn unter den fehlenden Tier-2-Tests |
| P1 | Shared PostgreSQL 18 Cluster Restore Drill | offen | Komplett-Drill: `pg_dumpall --globals-only` + per-DB Custom-Format-Dumps, inkl. `mailarchiver`-Bootstrap-Rollenkonflikt. Aktuell nur als Doku in RESTORE_MATRIX, kein automatischer Test | | P1 | Shared PostgreSQL 18 Cluster Restore Drill | **erledigt 2026-06-03** | Globals + 5 DBs (paperless 72t, mailarchiver 1t, authelia 25t, nextcloud 126t, mealie 66t), `data_checksums=on`, Report `/mnt/user/backups/restore-reports/shared-pg-cluster-2026-06-03.md` |
| P1 | Komodo-Mongo-Daten-Restore | **erledigt 2026-06-03** | 86904 Dokumente erfolgreich restored, Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`. Nebenbefund: Dump von Mongo 8.0.23, Test auf 7.0.32 — Cross-Version-Warning, fuer Lesetest harmlos | | P1 | Komodo-Mongo-Daten-Restore | **erledigt 2026-06-03** | 86904 Dokumente erfolgreich restored, Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`. Nebenbefund: Dump von Mongo 8.0.23, Test auf 7.0.32 — Cross-Version-Warning, fuer Lesetest harmlos |
| P2 | Mailarchiver-Restore-Test | offen | Shared Postgres + Authelia-ForwardAuth; nach Nextcloud-Test | | P2 | Mailarchiver-Restore-Test | **erledigt 2026-06-03** | Data-Protection-Keys + 645M pg_restore + HTTP 200. Report `/mnt/user/backups/restore-reports/mailarchiver-2026-06-03.md` |
| P2 | Mealie-Restore-Test | offen | Eigene Postgres + File-Restore | | P2 | Mealie-Restore-Test | **erledigt 2026-06-03** | Borg-Data + pg_restore + HTTP 200, 3 Rezepte. Report `/mnt/user/backups/restore-reports/mealie-2026-06-03.md` |
| P2 | Traefik-Restore-Test | offen | Tier 1, aber komplex: `dynamic/` ist manuell-sync-Ausnahme, LE-State und CF-Token-Mount sind heikel | | P2 | Traefik-Restore-Test | **erledigt 2026-06-03** | dynamic/ + letsencrypt/ aus Borg, File-Provider + Ping 200. CF-Token bewusst nicht im Smoke. Report `/mnt/user/backups/restore-reports/traefik-2026-06-03.md` |
| P3 | Negativ-Test fuer Frische-Check | offen | Einmal pro Quartal bewusst kaputten Dump einfuettern und pruefen ob `homelab-alerts` feuert | | P3 | Negativ-Test fuer Frische-Check | offen | Einmal pro Quartal bewusst kaputten Dump einfuettern und pruefen ob `homelab-alerts` feuert |
| P3 | End-to-end-DR-Drill | offen | Komplett-Bootstrap Phase 1-5 auf einem Wegwerf-Host; realistisch nur mit zweiter Hardware | | P3 | End-to-end-DR-Drill | offen | Komplett-Bootstrap Phase 1-5 auf einem Wegwerf-Host; realistisch nur mit zweiter Hardware |
@@ -28,10 +30,10 @@ Ergebnis des Restore-Skills-Audits (Session 2026-06-02/03). Die kritischen Bugfi
| Punkt | Entscheidung | | Punkt | Entscheidung |
|---|---| |---|---|
| Authelia 2FA fuer Operator-UIs | In diesem Zyklus nicht umgesetzt; erst mit finaler Auth-Policy | | Authelia 2FA fuer Operator-UIs (Rest) | Tier-1-Operator-UIs sind 2026-06-03 auf `two_factor` gehoben (`files`, `scrutiny`, `borg`, `code`). Restliche Admin-UIs (`monitoring`, `glances`, `glance`, `speedtest`, `paperless-gpt`, `pdf`, `mail`, `hermes`, `sp`) bleiben bewusst auf `one_factor`, bis die finale Auth-Policy steht. |
| Authelia OIDC fuer Apps | Geparkt bis klare Familien-/SSO-Entscheidung | | Authelia OIDC fuer Apps | Geparkt bis klare Familien-/SSO-Entscheidung |
| CrowdSec vor Traefik | Erst nach Auth-Policy neu bewerten | | CrowdSec vor Traefik | Bewusst nicht umgesetzt: einzige WAN-Tuer ist `443/tcp`, Operator-Pfad ist Tailscale, Authelia-`regulation:` deckt Auth-Brute-Force ab. Neu bewerten bei breiterer Attack Surface. |
| Nextcloud 2FA/Brute-Force-Haertung | Gemeinsam mit OIDC/Familienkonten entscheiden | | Nextcloud 2FA/Brute-Force-Haertung | UI-Schritt fuer Operator-Account (`twofactor_totp` aktivieren) bleibt offen. App-weite Familien-Policy gemeinsam mit OIDC entscheiden. |
| Hermes-Agent | NAS-Stack bleibt deaktiviert; Review-Deadline 2026-07-25 | | Hermes-Agent | NAS-Stack bleibt deaktiviert; Review-Deadline 2026-07-25 |
| USV | Anschaffung verschoben; Power-Loss-Risiko bewusst akzeptiert | | USV | Anschaffung verschoben; Power-Loss-Risiko bewusst akzeptiert |
| Zweites Off-site-Ziel | Bewusst nicht umgesetzt; neu bewerten bei Hetzner-Problemen, stark wachsendem Datenwert oder geaenderter Betreiber-Praeferenz | | Zweites Off-site-Ziel | Bewusst nicht umgesetzt; neu bewerten bei Hetzner-Problemen, stark wachsendem Datenwert oder geaenderter Betreiber-Praeferenz |
@@ -39,6 +41,11 @@ Ergebnis des Restore-Skills-Audits (Session 2026-06-02/03). Die kritischen Bugfi
## Zuletzt geschlossen ## Zuletzt geschlossen
- Fix Common Problems Plugin (FCP) 2026-06-03 deinstalliert. Befund: drei `grep -R ... /usr/local/emhttp`-Prozesse aus einem FCP-Daily-Scan hingen seit ~7 Tagen in einem Symlink-Loop (`/usr/local/emhttp/mnt -> /mnt`, gesamte Array). 3 Cores dauerhaft 100 %, IOWAIT bis 55 %, USB-Flash unter Dauer-IO. Plugin via `plugin remove` entfernt, Cron + /tmp-Reste sauber, Load von 14.6 auf 1.08 gefallen. FCP wird bewusst nicht wieder installiert (Begruendung siehe `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13). Bekannte Risiken decken Scrutiny, Monitoring, Posture-Check und Critical-Events-Watcher bereits ab.
- GitHub-Mirror Read-Only Deploy-Key `DR Read-Only 2026-06-03` (ed25519, Passphrase-frei) angelegt: GitHub Repo Settings -> Deploy Keys ohne Write-Access, Smoke `git ls-remote` erfolgreich (HEAD `d947c7f` = master), Private-Key offline neben der KOMODO_*-Notiz abgelegt, Arbeitsplatz-Kopie nach USB-Transfer geloescht. Damit ist der DR-Read-Pfad zum privaten Mirror ohne Operator-Browser-Login moeglich.
- KOMODO_*-Notiz offline gesichert (Operator-Bestaetigung 2026-06-03). Quelle bleibt host-seitige `.env` unter `/mnt/user/services/stacks/komodo/.env` bzw. die Drift-Recovery-Kopie unter `/mnt/user/appdata/secrets/_komodo_stack_env_recovery_2026-05-04.env`. Damit ist der Bare-Metal-Komodo-Bootstrap ohne Vaultwarden moeglich. Eintrag in `docs/EXTERNAL_DEPENDENCIES.md` Reviews und Pflichtbestandteil im DR-Workstation-Kit nachgezogen.
- DR-Tabletop 2026-06-03 durchgelaufen, Findings in `docs/DR_DRILL_2026-06-03.md` (23 Befunde: 1 CRITICAL, 11 HIGH, 8 MED, 3 LOW). Reine Doku-Fixes in DR.md (Phase 0 Mirror-Klarstellung, neue Phase 4 Stufe 0 Docker-Netze, LE-Staging-Hinweis, Komodo-Stolperfallen, App-DB-Verify in Phase 5) und in `EXTERNAL_DEPENDENCIES.md` (DR-Workstation-Kit, KOMODO_*-Notiz und GitHub-Read-PAT als offene Bootstrap-Bloecke) sind im selben Aenderungsblock erledigt. Operator-Aufgaben (Notiz/PAT/WSL-Setup) wandern als P1 in die offenen Punkte.
- Authelia ACL: `borg.kaleschke.info` und `code.kaleschke.info` 2026-06-03 in den `two_factor`-Block der Repo-Baseline aufgenommen. Beide UIs haben effektiv Host-/Backup-Zugriff (Borg-Restore-Scope inkl. `/local/secrets`, code-server mit Workspaces). Wirkung erst nach manuellem Merge in `/mnt/user/appdata/authelia/config/configuration.yml`, `docker restart authelia` und Smoke-Test auf einer der vier 2FA-Domains; `services/authelia-diff.sh` muss `exit 0` liefern. TOTP-Enrollment des Operator-Accounts ist Voraussetzung, sonst Login-Sperre.
- Alt-Volumes nach Burn-in freigegeben und reversibel archiviert: Shared PG17, Mealie PG17, Nextcloud PG17 und Immich pgvecto.rs liegen seit 2026-06-02 unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602`; Manifest auf dem Host: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/MANIFEST.txt`. Keine harte Loeschung, keine aktiven Container-Mounts auf die alten Pfade. - Alt-Volumes nach Burn-in freigegeben und reversibel archiviert: Shared PG17, Mealie PG17, Nextcloud PG17 und Immich pgvecto.rs liegen seit 2026-06-02 unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602`; Manifest auf dem Host: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/MANIFEST.txt`. Keine harte Loeschung, keine aktiven Container-Mounts auf die alten Pfade.
- Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; Live-Baseline am 2026-06-01: FRITZ!OS `154.08.25`, keine Public-AAAA-Records fuer `*.kaleschke.info`, Host ohne globale Provider-IPv6, WAN `443/tcp` offen und `80/tcp`/`222/tcp` geschlossen. - Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; Live-Baseline am 2026-06-01: FRITZ!OS `154.08.25`, keine Public-AAAA-Records fuer `*.kaleschke.info`, Host ohne globale Provider-IPv6, WAN `443/tcp` offen und `80/tcp`/`222/tcp` geschlossen.
- FRITZ!Box-Servicefenster UI-seitig abgeschlossen: FRITZ!Box-Dienste aus dem Internet sind aus (HTTPS auf FRITZ!Box-UI, FTP/FTPS auf Speichermedien), aktive WAN-Freigabe bleibt nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus. - FRITZ!Box-Servicefenster UI-seitig abgeschlossen: FRITZ!Box-Dienste aus dem Internet sind aus (HTTPS auf FRITZ!Box-UI, FTP/FTPS auf Speichermedien), aktive WAN-Freigabe bleibt nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
+53 -4
View File
@@ -62,7 +62,8 @@ Diese Punkte sollten **vor** einem echten Ausfall geklaert sein:
| Thema | Sollzustand | | Thema | Sollzustand |
|---|---| |---|---|
| Repo-Zugang ausserhalb von Gitea | privater GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` und lokaler aktueller Clone vorhanden | | Repo-Zugang ausserhalb von Gitea | privater GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` und lokaler aktueller Clone vorhanden; fuer Bare-Metal-DR zusaetzlich Read-Only-PAT/Deploy-Key offline im DR-Kit |
| Operator-DR-Workstation | Gaming-PC mit aktuellem Repo-Clone, WSL2 + Borg-Client, SSH-Key fuer Hetzner Storage Box, Offline-Kopie Borg-Passphrase; Bestandteile siehe `docs/EXTERNAL_DEPENDENCIES.md` Abschnitt "DR-Workstation Bare-Metal-Kit" |
| Unraid USB-/Flash-Backup | `unraid-flash-config.tar.gz` wird vor Borg unter `/mnt/user/backups/borg/dumps/latest` erzeugt und nach Hetzner/Borg gesichert; Unraid-Connect-Cloud-Backup optional zusaetzlich | | Unraid USB-/Flash-Backup | `unraid-flash-config.tar.gz` wird vor Borg unter `/mnt/user/backups/borg/dumps/latest` erzeugt und nach Hetzner/Borg gesichert; Unraid-Connect-Cloud-Backup optional zusaetzlich |
| Borg-Ziel | nicht nur lokal auf demselben Ausfallpfad | | Borg-Ziel | nicht nur lokal auf demselben Ausfallpfad |
| Borg-Passphrase | Host-Secret-Datei vorhanden und fuer Borg-Zugriff verifiziert; externe Offline-Hinterlegung vom Operator am 2026-05-26 bestaetigt | | Borg-Passphrase | Host-Secret-Datei vorhanden und fuer Borg-Zugriff verifiziert; externe Offline-Hinterlegung vom Operator am 2026-05-26 bestaetigt |
@@ -87,9 +88,15 @@ Deshalb gilt:
Verfuegbare Wege: Verfuegbare Wege:
- externer Push-Mirror: `https://github.com/michaelkaleschke-spec/homelab-infra` - externer Push-Mirror: `https://github.com/michaelkaleschke-spec/homelab-infra` (privat, Read-PAT/Deploy-Key noetig — siehe `docs/EXTERNAL_DEPENDENCIES.md` Abschnitt "DR-Workstation Bare-Metal-Kit")
- lokaler Bare-Clone auf dem PC - lokaler Bare-Clone auf der Operator-DR-Workstation (Standardweg)
- normaler lokaler Arbeits-Clone auf dem PC - normaler lokaler Arbeits-Clone auf der Operator-DR-Workstation
Operativer Pfad fuer den Repo auf den frisch installierten Unraid-Host:
1. Operator-DR-Workstation holt den aktuellen Clone (lokaler Stand oder per `git clone` aus dem GitHub-Mirror mit dem offline gesicherten Read-PAT/Deploy-Key).
2. Kopie via USB, SMB oder `rsync ueber SSH/Tailscale` nach `/mnt/user/services/homelab-infra/` auf dem Unraid-Host.
3. Stand pruefen: `git -C /mnt/user/services/homelab-infra log --oneline -1` zeigt einen plausibel aktuellen Commit.
Wenn **weder GitHub-Mirror noch lokaler Repo-Clone** verfuegbar sind, ist `services/gitea/data` selbst ein kritischer Restore-Pfad. Wenn **weder GitHub-Mirror noch lokaler Repo-Clone** verfuegbar sind, ist `services/gitea/data` selbst ein kritischer Restore-Pfad.
@@ -148,6 +155,12 @@ Erwartete Basis unter `/mnt/user/appdata/secrets/`:
- `redis_password.txt` - `redis_password.txt`
- `borg_repo_passphrase.txt` - `borg_repo_passphrase.txt`
- `vaultwarden_admin_token.txt` - `vaultwarden_admin_token.txt`
- `homelab_smtp_password.txt`
- `n8n_encryption_key.txt`
- `monitoring_grafana_admin_password.txt`
- `monitoring_grafana_influxdb_token.txt`
- `influxdb3_admin_token.json`
- `filebrowser_admin_password.txt`
- `hermes_runner_id_ed25519` - `hermes_runner_id_ed25519`
Weitere relevante Secret-Pfade: Weitere relevante Secret-Pfade:
@@ -241,18 +254,46 @@ Besonders kritisch:
**Nicht blind alles extrahieren**, wenn nur einzelne Pfade oder Dienste betroffen sind. **Nicht blind alles extrahieren**, wenn nur einzelne Pfade oder Dienste betroffen sind.
### 7.3 Borg-Extract ohne `borg-ui`-Container
Im Bare-Metal-Fall ist `borg-ui` selbst kalt. Der initiale Borg-Extract laeuft deshalb nicht ueber den Container, sondern wahlweise ueber:
1. **Operator-DR-Workstation** (Standardweg) - WSL2 + `borgbackup` extrahieren gezielt nach `/mnt/user/backups/restore-lab/...` oder per `rsync`/SMB auf den Unraid-Host.
2. **Native Docker-Variante auf Unraid** - `docker run --rm -e BORG_PASSPHRASE=... -v /mnt/user/backups/restore-lab:/restore -v ~/.ssh:/root/.ssh:ro borgbackup/borg:1.4 ...`.
Erst nach Stufe 5 Phase 4 ist `borg-ui` produktiv und uebernimmt den weiteren Betrieb. Die Borg-Passphrase wird interaktiv aus der Offline-Sicherung eingegeben, nicht in Skripte/Tickets kopiert.
--- ---
## 8. Phase 4 - Bootstrap-Reihenfolge der Stacks ## 8. Phase 4 - Bootstrap-Reihenfolge der Stacks
**Nie alle Stacks gleichzeitig starten.** **Nie alle Stacks gleichzeitig starten.**
### Stufe 0 - Docker-Grundlage
Vor dem ersten `docker compose up` muss sichergestellt sein:
1. `docker info` antwortet ohne Fehler.
2. Externe Docker-Netze existieren. Wenn nicht vorhanden:
```bash
docker network create --driver bridge frontend_net
docker network create --driver bridge --internal backend_net
docker network create --driver bridge monitoring_net
```
3. Pfad `/mnt/user/appdata/traefik/dynamic/` enthaelt `middlewares.yml`, `tls.yml`, `dashboards.yml` (Sonderregel siehe Sektion 10). Ohne diese Dateien startet Traefik ohne Middleware-Definitionen und alle Authelia-geschuetzten Routen brechen still.
Erfolgskriterium: `docker network ls` zeigt `frontend_net`, `backend_net`, `monitoring_net`; Traefik-`dynamic/`-Dateien sind vorhanden und valide.
### Stufe 1 - Netz und Zugang ### Stufe 1 - Netz und Zugang
1. `traefik/` 1. `traefik/`
2. `host-services/Adguard/` 2. `host-services/Adguard/`
3. `host-services/tailscale/` 3. `host-services/tailscale/`
**LE-Rate-Limit-Vorsicht:** Wenn `/mnt/user/appdata/traefik/letsencrypt/acme.json` verloren oder unklar ist, zuerst gegen Let's Encrypt Staging ausstellen lassen (`--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory`). Erst nach gruenem Smoke wieder auf Production-CA. Hintergrund: 50 Zertifikate pro Domain pro Woche reicht bei einem hektischen Wiederanlauf nicht, wenn man die Sub-Domains mehrfach hochzieht.
Ziel: Ziel:
- Web-Einstieg funktioniert - Web-Einstieg funktioniert
@@ -290,6 +331,13 @@ Ziel:
- Periphery verbindet sich wieder - Periphery verbindet sich wieder
- Stacks koennen wieder aus Git konsumiert werden - Stacks koennen wieder aus Git konsumiert werden
**Wichtige Stolperfallen in Stufe 3:**
- **KOMODO_*-Werte sind nicht aus dem eigenen Mongo-Dump rekonstruierbar.** Pflichtquelle im Bare-Metal: offline gesicherte Operator-Notiz (Status 2026-06-03: noch nicht angelegt, siehe `docs/EXTERNAL_DEPENDENCIES.md` und Audit-Restliste). Vaultwarden ist erst in Stufe 4 verfuegbar.
- **Mongo-Datadir und `komodo_mongo_password.txt` muessen aus demselben Snapshot stammen.** Bei Mismatch akzeptiert Mongo den Login nicht und der Stack startet nicht. Auswege: entweder die zur Datadir passende Secret-Datei aus dem gleichen Borg-Stand restaurieren, oder Datadir leeren, neu initialisieren und Daten via `mongorestore --archive --gzip` aus `komodo-mongo.archive.gz` einspielen (Drill belegt 2026-06-03).
- **`extra_hosts: git.kaleschke.info:192.168.178.58`** in `ops/komodo/docker-compose.yml` ist hardgecodet. Bei geaenderter Host-LAN-IP auf der Recovery-Hardware den Wert vor `compose up` anpassen, sonst kann Komodo-Core das interne Gitea nicht erreichen.
- **Stack-ENV-Werte fuer Apps in Stufe 4** (Paperless/Immich/Mailarchiver/Speedtest) sind in Stufe 3 noch leer. Zwei Wege: (a) optionaler `mongorestore` aus `komodo-mongo.archive.gz` direkt nach Komodo-Start, dann sind alle Stack-ENVs zurueck; (b) Werte manuell in der Komodo-UI eintragen, sobald Vaultwarden in Stufe 4 verfuegbar ist (was Paperless/Immich/Mailarchiver hinter Vaultwarden zwingt, nicht parallel).
### Stufe 4 - Kritische Anwendungen ### Stufe 4 - Kritische Anwendungen
9. `security/vaultwarden/` 9. `security/vaultwarden/`
@@ -342,6 +390,7 @@ Ziel:
- Mealie startet - Mealie startet
- Mail-Archiver startet - Mail-Archiver startet
- Nextcloud startet und sieht Dateien - Nextcloud startet und sieht Dateien
- Pro App: `docker logs <container>` zeigt keine `password authentication failed`-, `FATAL: role does not exist`- oder `Connection refused`-Eintraege (verifiziert, dass Stack-ENV-Werte und DB-Rollen passen)
### 9.4 Backup-/Beobachtungsebene ### 9.4 Backup-/Beobachtungsebene
+504
View File
@@ -0,0 +1,504 @@
# DR Tabletop Drill - 2026-06-03
Trockenlauf gegen `docs/DISASTER_RECOVERY.md` Phase 0 bis 5 plus referenzierte
Runbooks (`SERVICES_RECOVERY.md`, `RESTORE_MATRIX.md`, `SECRETS_MAP.md`,
`RESTORE_HANDBOOK.md`, `EXTERNAL_DEPENDENCIES.md`).
Szenario: Bare-Metal-Ausfall. Unraid-Host und alle lokalen Festplatten sind
weg. Operator hat: Laptop, Hetzner-Account, Vaultwarden-Export, Repo-Doku.
Soft-Recovery (Host laeuft, Appdata futsch) ist eine Teilmenge dieser
Findings.
Methode: kalter Lesetest. Kein Container gestartet, keine Skripte
ausgefuehrt. Jeder Befund ist mit Repo-Datei und Zeile belegt. Spekulative
"vielleicht unklar"-Befunde sind weggelassen.
Severity:
- **CRITICAL** - blockiert Wiederanlauf, ohne Workaround nicht loesbar
- **HIGH** - blockiert eine Phase, Workaround moeglich aber undokumentiert
- **MED** - kostet Zeit oder fuehrt zu vermeidbarem Fehler
- **LOW** - Konsistenz / Stil
## Zusammenfassung
| ID | Phase | Severity | Thema |
|---|---|---|---|
| P0-1 | 0 | HIGH | Brueckenpfad Windows-Clone -> frischer Unraid-Host fehlt |
| P0-2 | 0 | HIGH | GitHub-Mirror-Zugang im DR ist nicht eigenstaendig dokumentiert |
| P1-1 | 1 | CRITICAL | Unraid-Flash-Restore: kein dokumentierter Extract-Pfad ohne laufenden Host |
| P1-2 | 1 | MED | Unraid-OS-Flash-Restore-Test laut Matrix nie real getestet |
| P2-1 | 2 | HIGH | KOMODO_* externe Operator-Notiz ist Pflichtquelle, Existenz nicht verifizierbar |
| P2-2 | 2 | HIGH | DR.md Phase 4 vs. SERVICES_RECOVERY.md Bootstrap-Reihenfolge widerspruechlich |
| P2-3 | 2 | MED | `homelab_smtp_password.txt` fehlt in DR.md Phase 2.6.1 |
| P2-4 | 2 | MED | `n8n_encryption_key.txt` fehlt in DR.md Phase 2.6.1 |
| P2-5 | 2 | LOW | Monitoring-/Filebrowser-Secrets fehlen in DR.md Phase 2.6.1 |
| P3-1 | 3 | HIGH | Borg-Client ohne `borg-ui`-Container ist nicht dokumentiert |
| P3-2 | 3 | HIGH | Borg-Passphrase-Bootstrap aus Offline-Sicherung nicht als expliziter Schritt |
| P3-3 | 3 | MED | Hetzner-Maintenance-Key aus Vaultwarden ist Henne-Ei im Bare-Metal |
| P4-1 | 4 | HIGH | Externe Docker-Netze in DR.md Phase 4 Stufe 1 nicht erwaehnt |
| P4-2 | 4 | HIGH | Cloudflare-LE-Rate-Limit-Risiko bei verlorenem `letsencrypt`-State |
| P4-3 | 4 | MED | `traefik/dynamic/*` als Phase-4-Pre-Check fehlt in der Reihenfolge |
| P4-4 | 4 | HIGH | Authelia "frische Postgres ohne Dump"-Pfad nicht beschrieben |
| P4-5 | 4 | LOW | Gitea in Stufe 2 hinter Postgres ist faktisch nicht noetig (SQLite) |
| P4-6 | 4 | HIGH | Komodo-Mongo Passwort-Lockout-Risiko bei restauriertem Datadir |
| P4-7 | 4 | MED | Komodo `extra_hosts` mit hardgecodeter LAN-IP bricht bei IP-Wechsel |
| P4-8 | 4 | HIGH | Stack-ENV-Wiederherstellung in Komodo praktisch nur manueller UI-Eintrag |
| P5-1 | 5 | LOW | Smoke-Tests in Phase 5 weniger streng als RESTORE_MATRIX |
| P5-2 | 5 | MED | Kein Verifikationspunkt fuer App-zu-DB-Verbindung nach Stack-ENV-Restore |
| X-1 | uebergreifend | HIGH | Nextcloud-Restore-Skript ist da, aber noch nie real ausgefuehrt |
## Phase 0 - Repo-Zugang
### P0-1 (HIGH) - Brueckenpfad Windows-Clone -> frischer Unraid fehlt
`docs/DISASTER_RECOVERY.md:88-93` listet als Repo-Quellen: GitHub-Mirror,
lokaler Bare-Clone, lokaler Arbeits-Clone. `SERVICES_RECOVERY.md:67-68`
nennt den lokalen Operator-Clone unter `G:\Gitea_Clone\homelab-infra\` als
Vorzug.
Luecke: der Pfad "wie kommt der Windows-Clone auf einen frisch installierten
Unraid-Host" ist nicht beschrieben. Implizit: SMB-Share, USB-Stick, scp ueber
LAN. Aber auf einem frisch aufgesetzten Unraid existiert noch keine
funktionierende SMB-Konfiguration; SSH-Key vom Operator-PC ist nicht
vorbereitet.
Vorschlag: Zwei Saetze in `DISASTER_RECOVERY.md` Phase 0 ergaenzen, wie der
Operator-Clone konkret zum Host kommt (USB-Stick + `mkdir -p
/mnt/user/services/homelab-infra && rsync -a` aus Operator-Windows-PC, oder
direkt vom GitHub-Mirror per `git clone https://github.com/...` auf dem
Unraid-Host).
### P0-2 (HIGH) - GitHub-Mirror-Zugang im DR
`SECRETS_MAP.md:42` sagt, der GitHub-Push-Mirror-PAT liegt in den
Gitea-Mirror-Settings persistent unter `/mnt/user/services/gitea/data`.
`EXTERNAL_DEPENDENCIES.md:18` nennt den Mirror als `michaelkaleschke-spec/
homelab-infra` und betont "privater" Push-Mirror.
Luecke: Wenn der Mirror **privat** ist, scheitert ein anonymer `git clone`
im DR-Bootstrap. Es gibt keine dokumentierte Notfall-Quelle fuer einen
Read-PAT/SSH-Key, der lokal beim Operator (nicht in Gitea, nicht im Repo)
verfuegbar ist.
Vorschlag in `EXTERNAL_DEPENDENCIES.md`: entweder explizit dokumentieren,
dass der Mirror lesend `Public` ist (DR-fit), oder einen Read-PAT in der
Vaultwarden-/Offline-Notiz neben der Borg-Passphrase als Bootstrap-Voraussetzung
benennen.
## Phase 1 - Unraid und Shares
### P1-1 (CRITICAL) - Unraid-Flash-Restore ohne laufenden Host
`docs/DISASTER_RECOVERY.md:107` sagt: "Primaere lokale/off-site
Restore-Quelle fuer die bestehende Flash-Konfiguration ist das
Borg-Artefakt `unraid-flash-config.tar.gz` aus
`/mnt/user/backups/borg/dumps/latest`."
Henne-Ei: der Pfad ist auf den verlorenen Shares oder auf Hetzner. Hetzner-
Zugriff braucht einen funktionierenden Linux-Host mit Borg-Client und
Passphrase. Im Bare-Metal-Fall ist genau das nicht da. RESTORE_MATRIX.md
Tier 1 Zeile `Unraid OS Flash` (`docs/RESTORE_MATRIX.md:29`) sagt nur "Unraid
USB Flash Creator / neuer Boot-Stick" - das beschreibt die Stick-Erzeugung,
nicht den Extract des Borg-Artefakts.
Operativ: Operator braucht einen Laptop mit Borg-Client + Passphrase +
SSH-Key fuer die Hetzner-Storage-Box. Das ist eine **separat zu pflegende
Operator-Workstation-Voraussetzung** und ist in keinem Repo-Dokument als
DR-Vorbedingung gelistet.
Vorschlag: In `EXTERNAL_DEPENDENCIES.md` oder `DISASTER_RECOVERY.md`
Abschnitt 3 als Pflichtposten aufnehmen: "Operator-Laptop mit installiertem
Borg-Client, SSH-Key fuer Hetzner und Zugriff auf die offline gesicherte
Passphrase". Inklusive Test, dass der Operator den Extract tatsaechlich
durchfuehren kann.
### P1-2 (MED) - Unraid-OS-Flash-Restore-Test nie gelaufen
`docs/RESTORE_MATRIX.md:140` Spalte "Letzter Restore-Test" fuer Unraid OS
Flash: `-` (kein Test). Das ist die Grundlage fuer Phase 1 und ist nie als
Smoke verifiziert. Empfehlung: einmaliger Test, der die Tar-Archiv-Struktur
gegen die erwarteten Flash-Pfade prueft (kein echter Boot-Test noetig).
## Phase 2 - Secrets und Stack-ENV
### P2-1 (HIGH) - KOMODO_* externe Operator-Notiz als Pflichtquelle
`docs/SECRETS_MAP.md:132,138-143` macht den Komodo-Sonderfall klar: die
KOMODO_*-Secrets sind aus dem eigenen Mongo-Dump nicht rekonstruierbar,
solange Komodo nicht laeuft. Quellen: Vaultwarden ODER externe Notiz.
Im Bare-Metal-Fall ist Vaultwarden in DR.md Phase 4 Stufe 4, Komodo in
Phase 4 Stufe 3. Damit ist die **externe Operator-Notiz** die einzige
Pflichtquelle in der Reihenfolge.
Luecke: ob diese Notiz wirklich existiert und die 5 Werte
(KOMODO_SECRET_KEY, KOMODO_WEBHOOK_SECRET, KOMODO_JWT_SECRET,
KOMODO_MONGO_PASSWORD, KOMODO_PERIPHERY_PASSKEY) enthaelt, ist in keinem
Repo-Dokument bestaetigt. Die Borg-Passphrase ist als "Operator-Bestaetigung
2026-05-26" dokumentiert; eine analoge Bestaetigung fuer die KOMODO_*-Notiz
fehlt.
Vorschlag: gleiche Form wie Borg-Passphrase - eine Zeile in
`EXTERNAL_DEPENDENCIES.md` "Komodo-Stack-ENV-Notiz offline gesichert,
Operator-Bestaetigung YYYY-MM-DD".
### P2-2 (HIGH) - Reihenfolgen-Inkonsistenz DR vs. SERVICES_RECOVERY
`docs/SERVICES_RECOVERY.md:102` (Stufe C, Komodo-Bootstrap): "Vaultwarden
(sobald restauriert), externe Operator-Notiz, oder Komodo-Mongo-Dump (nur
wenn Mongo separat bereits gestartet ...)".
`docs/DISASTER_RECOVERY.md:247-301` (Phase 4): Stufe 3 = Komodo, Stufe 4 =
Vaultwarden.
Wenn ein Leser sich an DR.md Phase 4 haelt, ist Vaultwarden nach Komodo
fertig. Aber SERVICES_RECOVERY.md Stufe C setzt Vaultwarden als optionale
Vorab-Quelle voraus. Ohne externe Notiz heisst das praktisch: Komodo kann
nicht starten. Die Konsequenz steht nirgendwo explizit in DR.md.
Vorschlag: In `DISASTER_RECOVERY.md` Phase 4 Stufe 3 einen Hinweisblock
ergaenzen: "KOMODO_*-Werte muessen vor Stufe 3 aus externer Notiz oder
einer in Stufe 2 voraus gezogenen Vaultwarden-Instanz vorliegen. Default-
Pfad: externe Notiz."
### P2-3 (MED) - `homelab_smtp_password.txt` fehlt in DR.md 6.1
`docs/SECRETS_MAP.md:20` listet `/mnt/user/appdata/secrets/
homelab_smtp_password.txt` fuer Vaultwarden-SMTP. In `DISASTER_RECOVERY.md`
Abschnitt 6.1 (`docs/DISASTER_RECOVERY.md:136-151`) ist sie nicht
aufgefuehrt. Vaultwarden startet ohne, kann aber keine Einladungs-/
Benachrichtigungs-Mails versenden. Klein, aber unsichtbarer Folgefehler im
Familien-Onboarding-Pfad.
### P2-4 (MED) - `n8n_encryption_key.txt` fehlt in DR.md 6.1
`docs/SECRETS_MAP.md:58` listet `/mnt/user/appdata/secrets/
n8n_encryption_key.txt`. In DR.md 6.1 fehlt sie komplett.
`SECRETS_MAP.md:135` macht die Folgen explizit: "Bei Verlust aller
Quellen: n8n startet, aber alle gespeicherten Credentials sind unbrauchbar".
Da n8n den GMX-Mail-Workflow fuer das Gitea-`Micha/mails`-Repo betreibt,
ist das ein direkter Workflow-Ausfall.
### P2-5 (LOW) - Monitoring-/Filebrowser-Secrets fehlen in DR.md 6.1
`docs/SECRETS_MAP.md:53-55`: `influxdb3_admin_token.json`,
`monitoring_grafana_admin_password.txt`,
`monitoring_grafana_influxdb_token.txt` sowie
`filebrowser_admin_password.txt` sind nicht in DR.md 6.1. Tier-3-Apps,
Folge ist nur ein UI-Initialisierungs-Schritt nach Wiederanlauf. Keine
Critical-Konsequenz, aber Inkonsistenz.
## Phase 3 - Borg-Extract
### P3-1 (HIGH) - Borg-Client ohne `borg-ui`-Container
`docs/RESTORE_HANDBOOK.md:30-33` sagt explizit: "Borg-Zugriff laeuft ueber
den vorhandenen `borg-ui`-Container".
Im Bare-Metal-Fall ist `borg-ui` selbst kalt (Tier 3, DR.md Phase 4 Stufe 5).
Es gibt keinen dokumentierten Pfad, wie der erste Borg-Extract ohne diesen
Container laeuft. Implizite Optionen: nativer Borg auf Unraid (Plugin),
`docker run --rm borgbackup/borg`, oder Operator-Laptop. Keine davon ist
benannt.
Vorschlag: In `RESTORE_HANDBOOK.md` Abschnitt 2 einen "Bare-Metal-Vorlauf"
ergaenzen, der den initialen Borg-Extract ohne borg-ui-Container
beschreibt - z. B. `docker run --rm -v
/mnt/user/backups/restore-lab:/restore borgbackup/borg ...`.
### P3-2 (HIGH) - Borg-Passphrase-Bootstrap nicht als expliziter Schritt
`docs/DISASTER_RECOVERY.md:68`: "Host-Secret-Datei vorhanden und fuer
Borg-Zugriff verifiziert; externe Offline-Hinterlegung vom Operator am
2026-05-26 bestaetigt."
Praktisch heisst das: im Bare-Metal-Fall liest der Operator die Passphrase
aus einem analogen Medium und tippt sie in den Borg-Client. Das ist ein
**Bootstrap-Schritt**, der nicht als Schritt dokumentiert ist. Er steckt
implizit in "extern bestaetigt".
Vorschlag: Ein nummerierter Bullet in `DISASTER_RECOVERY.md` Phase 3 ("Wenn
echte Daten aus Borg benoetigt werden"): "Schritt 1: Borg-Passphrase aus
Offline-Sicherung beschaffen. Wert wird nicht in Skripte oder Tickets
kopiert; nur in den interaktiven Borg-Aufruf eingegeben."
### P3-3 (MED) - Hetzner-Maintenance-Key im Bare-Metal
`docs/EXTERNAL_DEPENDENCIES.md:17`: "Maintenance-Key liegt in Vaultwarden".
Im Bare-Metal-Bootstrap ist Vaultwarden Phase 4 Stufe 4. Damit ist der Key
fuer die initiale Phase-3-Hetzner-Verbindung nicht zugaenglich. Implizit
muss er ebenfalls offline gesichert sein (analog Borg-Passphrase).
Vorschlag: gleiche Form wie Borg-Passphrase - eine Operator-Bestaetigung
in `EXTERNAL_DEPENDENCIES.md`, dass der Hetzner-SSH-Key auch ausserhalb von
Vaultwarden offline verfuegbar ist. Sonst ist die "Vaultwarden"-Aussage
fuer Bare-Metal eine Falle.
## Phase 4 - Bootstrap-Reihenfolge
### P4-1 (HIGH) - Externe Docker-Netze in DR.md Phase 4 Stufe 1 nicht erwaehnt
`docs/SERVICES_RECOVERY.md:82-84` Stufe A schreibt explizit: "Externe
Docker-Netze existieren oder werden erzeugt (`frontend_net`, `backend_net`).
Wenn nicht vorhanden: `docker network create --driver bridge frontend_net`
bzw. `... --internal backend_net`."
`docs/DISASTER_RECOVERY.md:252-260` Phase 4 Stufe 1 nennt nur Traefik,
AdGuard, Tailscale. Kein Hinweis auf externe Netze.
`traefik/docker-compose.yml:70-76` deklariert `frontend_net`, `backend_net`,
`monitoring_net` als `external: true`. Ohne vorab erstellte Netze scheitert
der erste `docker compose up` mit "network frontend_net not found".
Vorschlag: In `DISASTER_RECOVERY.md` Phase 4 vor Stufe 1 einen Vorlauf
"Stufe 0 - Docker-Grundlage" einfuegen, der die Netzwerk-Erzeugung wie in
`SERVICES_RECOVERY.md` Stufe A explizit listet.
### P4-2 (HIGH) - Cloudflare-LE-Rate-Limit-Risiko
`docs/RESTORE_MATRIX.md:30` markiert `letsencrypt` korrekt als
Restore-relevant. `docs/DISASTER_RECOVERY.md:240` listet
`/mnt/user/appdata/traefik/letsencrypt` ebenfalls als kritischen
Borg-Restore-Pfad.
Luecke: kein Hinweis auf den Praxisfall "LE-State verloren, frischer
Acme-Run". Let's Encrypt hat ein Rate-Limit von 50 Zertifikaten/Domain/
Woche und 5 Duplicate-Zertifikate/Woche. Bei einer Multi-Sub-Domain-
Konstellation wie `*.kaleschke.info` (15+ Hostnames) ist das beim
hektischen DR-Bootstrap erreichbar.
Vorschlag: In `DISASTER_RECOVERY.md` Phase 4 Stufe 1 einen Hinweis: "Bei
verlorenem oder unklarem `acme.json` zuerst gegen
`acme-staging-v02.api.letsencrypt.org` ausstellen lassen, erst nach
gruenem Smoke auf Production-CA umschalten." Ist eine Praesentations-
Aenderung in den Compose-Args, kein neuer Code.
### P4-3 (MED) - `traefik/dynamic/*` als Pre-Check fehlt
`docs/DISASTER_RECOVERY.md:357-365` Sektion 10 beschreibt die manuelle
Sonderregel fuer `traefik/dynamic/*`. Korrekt.
`docs/DISASTER_RECOVERY.md:252-260` Phase 4 Stufe 1 verweist nicht auf
diese Sonderregel. Wer der Reihenfolge folgt und Sektion 10 nicht liest,
startet Traefik ohne Middlewares - alle 2FA-Routen brechen still.
Vorschlag: Cross-Reference in Phase 4 Stufe 1: "Vor `docker compose up
traefik` pruefen, dass `/mnt/user/appdata/traefik/dynamic/middlewares.yml`,
`tls.yml`, `dashboards.yml` vorhanden sind (Sonderregel Sektion 10)."
### P4-4 (HIGH) - Authelia "frische Postgres ohne Dump"-Pfad fehlt
`docs/DISASTER_RECOVERY.md:267-275` Phase 4 Stufe 2 startet Postgres und
Authelia. Authelia erwartet eine Rolle `authelia` mit dem Passwort aus
`authelia_postgres_password.txt`. Im Restore-Pfad mit `pg_dumpall --globals-
only` ist die Rolle abgedeckt.
Bei einem **fresh-start** (keine alten Daten, nur Container hochfahren) ist
die Rolle nicht da. Postgres-Image legt sie nicht automatisch an. Authelia
schlaegt mit "FATAL: role authelia does not exist" fehl.
Luecke: Der Initialisierungspfad fuer eine frische Postgres ohne
pg_dumpall ist in der Doku nicht beschrieben. Im echten DR mit Borg ist
das unwahrscheinlich, aber im Soft-Recovery oder Migrations-Drill schon.
Vorschlag: In `DISASTER_RECOVERY.md` Phase 4 Stufe 2 eine optionale
Anweisung: "Falls Postgres frisch ist (kein Dump-Restore), `infra/
postgresql17/init/`-Skripte oder manuelle `CREATE ROLE`/`CREATE DATABASE`-
Schritte ergaenzen."
### P4-5 (LOW) - Gitea nach Postgres ist faktisch unnoetig
`docs/DISASTER_RECOVERY.md:267-275` Phase 4 Stufe 2 ordnet Gitea hinter
Postgres ein. Gitea nutzt SQLite (`gitea.sqlite.dump`), nicht den shared
Postgres. Reihenfolge ist nicht falsch, aber irrefuehrend. Nicht kritisch.
### P4-6 (HIGH) - Komodo-Mongo Passwort-Lockout-Risiko
`ops/komodo/docker-compose.yml:18-20` zeigt: `komodo-mongo` initialisiert
sich bei leerem Datadir mit `MONGO_INITDB_ROOT_PASSWORD_FILE` aus
`/mnt/user/appdata/secrets/komodo_mongo_password.txt`.
Restore-Fall: Datadir aus Borg restauriert, Secret-Datei aus Borg
restauriert - beide aus demselben Snapshot. OK.
Riskanter Fall: Datadir aus Borg, aber Secret-Datei aus einer anderen
(neueren oder aelteren) Quelle. Mongo akzeptiert den Login nicht, Komodo
laeuft nicht. Lockout. Doku erwaehnt diesen Pin-Punkt nicht.
Vorschlag: Hinweis in `DISASTER_RECOVERY.md` Phase 4 Stufe 3: "Mongo-
Datadir und `komodo_mongo_password.txt` muessen aus demselben Snapshot
kommen. Bei Mismatch: leeren Datadir und Re-Init, dann Daten aus
`komodo-mongo.archive.gz` per `mongorestore`."
### P4-7 (MED) - Hardgecodete LAN-IP in `extra_hosts`
`ops/komodo/docker-compose.yml:50` und `:101` haben:
`"git.kaleschke.info:192.168.178.58"`.
Bare-Metal-Recovery auf anderer Hardware oder veraenderter LAN-IP fuehrt
zu stummem Fehler: Komodo-Core kann Gitea nicht ueber den Override
erreichen, faellt auf AdGuard-DNS zurueck (wenn der schon laeuft) oder
scheitert.
Vorschlag: kurzer Hinweis in `DISASTER_RECOVERY.md` Phase 4 Stufe 3: "Bei
geaenderter Host-LAN-IP `extra_hosts`-Werte in `ops/komodo/docker-compose.
yml` vor `compose up` anpassen oder ueber `.env` parametrisieren."
### P4-8 (HIGH) - Stack-ENV-Wiederherstellung praktisch manuell
`docs/DISASTER_RECOVERY.md:188-195` sagt: "Wenn `komodo-mongo.archive.gz`
frisch ist, koennen die Werte beim Komodo-Restart aus dem Dump
zurueckgespielt werden, ohne dass jemand sie sieht."
`docs/RESTORE_HANDBOOK.md:73-74` und `docs/AUDIT_2026-05-25_TODO.md:20`
machen den Daten-Mongo-Restore als "erledigt 2026-06-03" sichtbar - aber
NICHT als Teil des DR-Bootstraps. Komodo-Bootstrap im Trockenlauf benutzt
Wegwerf-Werte.
Praktisch heisst das: Im DR-Bootstrap (Phase 4 Stufe 3) startet Komodo
**ohne** den Mongo-Daten-Restore. Die `KOMODO_*` kommen aus externer
Notiz. Aber die Stack-ENVs fuer `paperless`/`immich`/`mail-archiver`/
`speedtest` (PAPERLESS_DBPASS etc.) **muessen vor Stufe 4** wieder in
Komodo eingetragen sein. Wenn der Mongo-Daten-Restore nicht direkt nach
Komodo-Start passiert, gehen diese Werte manuell in die Komodo-UI.
Vorschlag: Klarstellung in `DISASTER_RECOVERY.md` Phase 4 zwischen Stufe
3 und Stufe 4: "Optionaler Mongo-Daten-Restore aus `komodo-mongo.archive.
gz` per `ops/restore-tests/komodo-mongo-restore-test.sh`-Muster - dann
sind alle Stack-ENVs zurueck. Alternativ: Stack-ENVs manuell in Komodo-
UI eintragen, Quelle Vaultwarden (sobald Stufe 4 Vaultwarden laeuft -
Henne-Ei mit Paperless: Paperless-Start dann erst nach Vaultwarden, nicht
parallel)."
## Phase 5 - Verifikation
### P5-1 (LOW) - Smoke-Tests in DR.md weniger streng als Matrix
`docs/DISASTER_RECOVERY.md:337-345` Phase 5.3 sagt z. B. "Vaultwarden
startet und ist erreichbar". `docs/RESTORE_MATRIX.md:39` sagt: "Login-
Seite erreichbar, Tresor-Daten sichtbar". Das zweite ist faktisch der
echte Smoke-Test.
Geschmackssache, kein Bug. Empfehlung: DR.md auf die Matrix-Smokes
verweisen statt eigene Kurzversion.
### P5-2 (MED) - Kein Verifikationspunkt App-zu-DB-Verbindung
`docs/DISASTER_RECOVERY.md:337-345` prueft App-Start, nicht DB-Auth-
Erfolg. Bei falschem `PAPERLESS_DBPASS`-Stack-ENV startet Paperless
moeglicherweise mit Error-Log und ist via Traefik nicht antwortend - aber
das fehlt als Pruefpunkt.
Vorschlag: Phase 5.3 ergaenzen: "Pro App: `docker logs <app>` zeigt keine
`password authentication failed`/`FATAL: role does not exist`-Eintraege."
## Uebergreifende Findings
### X-1 (HIGH) - Nextcloud-Restore-Skript existiert, ist aber ungetestet
`ops/restore-tests/nextcloud-restore-test.sh` und
`ops/restore-tests/nextcloud-compose.test.yml` existieren im Repo.
`docs/RESTORE_MATRIX.md:147` Spalte "Letzter Restore-Test" fuer Nextcloud:
`-`, naechster Lauf `**hoechste Prio**`. `docs/AUDIT_2026-05-25_TODO.md:18`
fuehrt es als P1 "offen".
Damit ist der echte Tabletop-Gewinn: der Test ist nicht "noch zu bauen",
sondern "noch nie ausgefuehrt". Ein `bash /mnt/user/services/homelab-
infra/ops/restore-tests/nextcloud-restore-test.sh` schliesst die letzte
Tier-2-Luecke.
## Nicht-Findings
Was ich gepruft und als sauber verifiziert habe:
- Referenzierte Skripte existieren alle: `pre-backup-dumps.sh`,
`gitea-bundle-mirror.sh`, `run-restore-checks.sh`,
`komodo-bootstrap-test.sh`, `posture-check.sh`, alle Restore-Test-
Skripte fuer Tier-1 und Tier-2.
- Pfadverweise zwischen DR.md, RESTORE_MATRIX.md, SECRETS_MAP.md,
SERVICES_RECOVERY.md sind konsistent (Borg-Dumps unter `/mnt/user/
backups/borg/dumps/latest`, Secrets unter `/mnt/user/appdata/secrets`).
- Drift-Erkennung Authelia (`services/authelia-diff.sh`) ist in
`posture-check` integriert (`WORKFLOW.md:292`).
- GitHub-Mirror-Pfad und Gitea-Bundle-Mirror als Repo-Bootstrap-Quellen
sind dreifach abgesichert (lokaler Clone, GitHub, Bundle).
- Tier-1-Postgres-Restore-Drill ist 2026-06-03 erfolgreich gelaufen
(`AUDIT_2026-05-25_TODO.md:19`).
- `ops/komodo/docker-compose.yml` ist als Recovery-Anker getestet
(`SERVICES_RECOVERY.md:142-166`).
- Borg-Passphrase und Hetzner-Account-Hygiene sind Operator-bestaetigt
(`AUDIT_2026-05-25_TODO.md:46-47`).
## Vorschlag fuer Reihenfolge der Folge-Arbeit
1. **CRITICAL P1-1 zuerst** - Operator-Laptop-Voraussetzung als
DR-Pflichtposten dokumentieren. Eine Dokuzeile.
2. **HIGH P0-2 + P3-3** - klaeren, ob GitHub-Mirror lesend public ist und
wo der Hetzner-Maintenance-Key offline liegt. Zwei Dokuzeilen oder
eine echte Setup-Entscheidung.
3. **HIGH P2-1** - Operator-Bestaetigung "KOMODO_*-Notiz offline
gesichert YYYY-MM-DD" in `EXTERNAL_DEPENDENCIES.md` ergaenzen (sobald
real angelegt).
4. **HIGH P4-1 + P4-2** - Vorlauf "Stufe 0 - Docker-Grundlage" und
LE-Staging-Hinweis in DR.md Phase 4 einfuegen. Etwa 10 Zeilen Doku.
5. **HIGH X-1** - `nextcloud-restore-test.sh` einmal scharf ausfuehren.
Vermutlich ein Vormittag inklusive Report-Review.
6. **HIGH P2-2 + P4-8** - Reihenfolgen-Konsistenz Komodo/Vaultwarden in
DR.md eindeutig aufloesen.
7. Rest in der Reihenfolge der Tabelle.
Punkte 1-4 sind reine Doku-Arbeit, keine Compose-/Runtime-Aenderung.
Punkt 5 ist ein echter Restore-Lauf mit Report. Punkt 6 ist die
substanziellste Doku-Aenderung in DR.md.
## Folge-Iteration 2026-06-03 (Doku-Fixes im selben Aenderungsblock)
Direkt nach dem Drill und nach Operator-Antworten auf vier offene Fragen wurden folgende Findings im Repo adressiert. Operator-Aufgaben, die ich nicht selbst tun kann, sind als P1 in `docs/AUDIT_2026-05-25_TODO.md` aufgenommen.
| ID | Massnahme |
|---|---|
| P0-1 | DR.md Phase 0 ergaenzt um "Operativer Pfad fuer den Repo auf den frisch installierten Unraid-Host" (USB/SMB/rsync); DR.md Abschnitt 3 mit Zeile "Operator-DR-Workstation"; `EXTERNAL_DEPENDENCIES.md` neuer Abschnitt "DR-Workstation Bare-Metal-Kit" |
| P0-2 | `EXTERNAL_DEPENDENCIES.md` GitHub-Mirror-Zeile praezisiert (privat, Read-PAT/Deploy-Key Pflicht); DR.md Phase 0 verweist explizit darauf; offene Operator-Aufgabe in Audit-Restliste |
| P1-1 | Operator-DR-Workstation als Voraussetzung in DR.md Abschnitt 3 und in `EXTERNAL_DEPENDENCIES.md`; konkrete Pflichtbestandteile (WSL2, Borg, SSH-Key) gelistet |
| P1-2 | Bleibt offen als P3-Test in Restore-Backlog (kein Doku-Fix moeglich) |
| P2-1 | KOMODO_*-Notiz als kritische Secret-Zeile in `EXTERNAL_DEPENDENCIES.md` mit Status "noch nicht angelegt"; Operator-Aufgabe in Audit-Restliste |
| P2-2 | DR.md Phase 4 Stufe 3 ergaenzt um expliziten Hinweis "KOMODO_* aus externer Notiz oder voraus gezogener Vaultwarden" |
| P2-3 | DR.md Abschnitt 6.1 um `homelab_smtp_password.txt` erweitert |
| P2-4 | DR.md Abschnitt 6.1 um `n8n_encryption_key.txt` erweitert |
| P2-5 | DR.md Abschnitt 6.1 um Monitoring-Grafana/InfluxDB-/Filebrowser-Secrets erweitert |
| P3-1 | DR.md neuer Abschnitt 7.3 "Borg-Extract ohne `borg-ui`-Container" mit DR-Workstation- und Docker-Variante |
| P3-2 | DR.md Abschnitt 7.3 nennt Passphrase-Eingabe explizit als interaktiven Bootstrap-Schritt |
| P3-3 | `EXTERNAL_DEPENDENCIES.md` Review-Zeile 2026-06-03: Hetzner-Maintenance-Key auch offline bestaetigt |
| P4-1 | DR.md Phase 4 neue Stufe 0 "Docker-Grundlage" mit `docker network create` Befehlen |
| P4-2 | DR.md Phase 4 Stufe 1 LE-Staging-Hinweis bei verlorenem `acme.json` |
| P4-3 | DR.md Phase 4 Stufe 0 nennt `traefik/dynamic/*` als Pre-Check |
| P4-4 | Wird mit fresh-Postgres-Initialisierungsskripten ohne Doku-Aenderung nicht sinnvoll abgedeckt; bleibt als Doku-Hinweis offen, ist im realen Restore-Pfad mit `pg_dumpall --globals-only` abgedeckt |
| P4-5 | LOW, nicht angepasst (Reihenfolge nicht falsch, nur irrefuehrend) |
| P4-6 | DR.md Phase 4 Stufe 3 "Wichtige Stolperfallen": Mongo-Datadir/Secret-Mismatch und Re-Init-Pfad |
| P4-7 | DR.md Phase 4 Stufe 3 "Wichtige Stolperfallen": `extra_hosts`-Anpassung bei IP-Wechsel |
| P4-8 | DR.md Phase 4 Stufe 3 "Wichtige Stolperfallen": Stack-ENV-Wiederherstellung per `mongorestore` oder manuell |
| P5-1 | LOW, nicht angepasst |
| P5-2 | DR.md Phase 5.3 um `docker logs`-Verifikation der App-zu-DB-Verbindung erweitert |
| X-1 | Nextcloud-Restore-Test als P1 in Audit-Restliste |
Nicht angefasst: P1-2 (kein Doku-Fix moeglich), P4-4 (im echten Restore-Pfad ohnehin abgedeckt), P4-5 und P5-1 (LOW). Die offenen Operator-Aufgaben (KOMODO_*-Notiz, Read-PAT, DR-Workstation, Nextcloud-Restore) stehen jetzt in `docs/AUDIT_2026-05-25_TODO.md` als P1.
## Reproduktion dieses Drills
```text
Methode: kalter Lesetest gegen
- docs/DISASTER_RECOVERY.md
- docs/RESTORE_MATRIX.md
- docs/SECRETS_MAP.md
- docs/SERVICES_RECOVERY.md
- docs/RESTORE_HANDBOOK.md
- docs/EXTERNAL_DEPENDENCIES.md
- ops/komodo/docker-compose.yml
- traefik/docker-compose.yml
Verifizierte Skript-Existenz: ops/borg-ui/scripts/*, ops/restore-tests/*,
services/posture-check/*
Kein Container gestartet, kein Skript ausgefuehrt, keine produktiven
Pfade beruehrt.
```
+24 -1
View File
@@ -15,7 +15,7 @@ Dieses Dokument beschreibt externe Anbieter und Konten, von denen Betrieb, Recov
| Domain-Registrar | Besitz `kaleschke.info` | hoch | Ohne Domain brechen Public URLs/TLS-Erneuerung | Operator-Konto ausserhalb Repo, konkreten Registrar im Account pruefen | Registrar-Zugang, 2FA-Recovery und Zahlungsweg analog/off-system sichern | | Domain-Registrar | Besitz `kaleschke.info` | hoch | Ohne Domain brechen Public URLs/TLS-Erneuerung | Operator-Konto ausserhalb Repo, konkreten Registrar im Account pruefen | Registrar-Zugang, 2FA-Recovery und Zahlungsweg analog/off-system sichern |
| Cloudflare DNS | Authoritative DNS, ACME DNS-Challenge, DDNS | hoch | Neue Zertifikate/DNS-Aenderungen blockiert | Cloudflare-Konto; API-Token liegt als Host-Secret | API-Token rotierbar halten, Account-Recovery und Zone-Besitz pruefen | | Cloudflare DNS | Authoritative DNS, ACME DNS-Challenge, DDNS | hoch | Neue Zertifikate/DNS-Aenderungen blockiert | Cloudflare-Konto; API-Token liegt als Host-Secret | API-Token rotierbar halten, Account-Recovery und Zone-Besitz pruefen |
| Hetzner Storage Box | Off-site Borg Backup | kritisch | Restore aus Off-site ggf. nicht moeglich | Hetzner-Konto / Storage-Box-Zugang ausserhalb Repo | Borg-Passphrase ist offline gesichert; Hetzner 2FA/Recovery/Zahlung sind bestaetigt; Storage Box ist SSH-only, Maintenance-Key liegt in Vaultwarden; Borg `append-only` wird per Operator-Entscheidung nicht umgesetzt | | Hetzner Storage Box | Off-site Borg Backup | kritisch | Restore aus Off-site ggf. nicht moeglich | Hetzner-Konto / Storage-Box-Zugang ausserhalb Repo | Borg-Passphrase ist offline gesichert; Hetzner 2FA/Recovery/Zahlung sind bestaetigt; Storage Box ist SSH-only, Maintenance-Key liegt in Vaultwarden; Borg `append-only` wird per Operator-Entscheidung nicht umgesetzt |
| GitHub Mirror | Externer Repo-Mirror `michaelkaleschke-spec/homelab-infra` | mittel/hoch | Gitea-Verlust abfederbar, Repo-Bootstrap bleibt moeglich | GitHub-Konto; PAT liegt in Gitea-Mirror-Settings, nicht im Repo | Mirror-Status regelmaessig pruefen; lokalen Clone als zweite Kopie behalten | | GitHub Mirror | Externer Repo-Mirror `michaelkaleschke-spec/homelab-infra` (privat) | mittel/hoch | Gitea-Verlust abfederbar, aber Bare-Metal-Bootstrap braucht Read-Zugang (PAT oder SSH-Deploy-Key); ohne diesen ist der Mirror im DR nicht klonbar | GitHub-Konto; Push-PAT liegt in Gitea-Mirror-Settings; **Read-PAT/Deploy-Key fuer DR muss zusaetzlich offline im DR-Kit liegen** | Mirror-Status regelmaessig pruefen; lokalen Clone als zweite Kopie behalten; Read-PAT mit Scope `repo:read` separat erzeugen und im DR-Kit ablegen |
| Tailscale | Remote-/Operator-Zugang | hoch | Remote-Zugriff erschwert, lokale Bedienung bleibt | Tailnet-Konto; Node `Kallilabcore`, IPv4 `100.80.98.33` | Break-glass per LAN und physischem Zugriff; Tailnet-Recovery-Codes sichern | | Tailscale | Remote-/Operator-Zugang | hoch | Remote-Zugriff erschwert, lokale Bedienung bleibt | Tailnet-Konto; Node `Kallilabcore`, IPv4 `100.80.98.33` | Break-glass per LAN und physischem Zugriff; Tailnet-Recovery-Codes sichern |
| GMX SMTP | Authelia Notifier, Vaultwarden-Einladungen, Ops-Report-Mail | mittel | Mail-Notifier und Vaultwarden-Einladungen fallen aus; Login selbst nicht zwingend | GMX-Konto; SMTP-Secrets liegen hostseitig | ntfy/zweiter SMTP als Fallback pruefen | | GMX SMTP | Authelia Notifier, Vaultwarden-Einladungen, Ops-Report-Mail | mittel | Mail-Notifier und Vaultwarden-Einladungen fallen aus; Login selbst nicht zwingend | GMX-Konto; SMTP-Secrets liegen hostseitig | ntfy/zweiter SMTP als Fallback pruefen |
| OpenAI API | Paperless-GPT LLM und Vision-OCR | mittel | Automatische Dokument-Titel, Tags, Korrespondenten und LLM-OCR fallen aus; Paperless selbst laeuft weiter | OpenAI-Projekt/API-Key ausserhalb Repo | Key in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten | | OpenAI API | Paperless-GPT LLM und Vision-OCR | mittel | Automatische Dokument-Titel, Tags, Korrespondenten und LLM-OCR fallen aus; Paperless selbst laeuft weiter | OpenAI-Projekt/API-Key ausserhalb Repo | Key in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten |
@@ -23,6 +23,7 @@ Dieses Dokument beschreibt externe Anbieter und Konten, von denen Betrieb, Recov
| Container Registries | Image Pulls von Docker Hub, GHCR, LSCR, Gitea Registry u. a. | mittel | Redeploy/Update blockiert | ueberwiegend oeffentlich; keine produktiven Registry-Tokens im Repo | Gepinnte Digests und lokale Runtime helfen kurzfristig; Updates geplant und einzeln deployen | | Container Registries | Image Pulls von Docker Hub, GHCR, LSCR, Gitea Registry u. a. | mittel | Redeploy/Update blockiert | ueberwiegend oeffentlich; keine produktiven Registry-Tokens im Repo | Gepinnte Digests und lokale Runtime helfen kurzfristig; Updates geplant und einzeln deployen |
| Plex Konto/Remote Access | Plex native Auth, ggf. Remote Access und Claim | mittel | Plex-Clients/Remote-Funktionen koennen ausfallen | Plex-Konto ausserhalb Repo; `PLEX_CLAIM` nur fuer Setup | LAN-Medienpfade bleiben lokal; Konto-Recovery separat sichern | | Plex Konto/Remote Access | Plex native Auth, ggf. Remote Access und Claim | mittel | Plex-Clients/Remote-Funktionen koennen ausfallen | Plex-Konto ausserhalb Repo; `PLEX_CLAIM` nur fuer Setup | LAN-Medienpfade bleiben lokal; Konto-Recovery separat sichern |
| Mobile Push | ntfy und ggf. mobile Plattform-Pushes | niedrig/mittel | Alerts erreichen Mobilgeraete ggf. nicht | App-/Device-seitig | Kritische Alerts zusaetzlich in Grafana/Glance sichtbar halten | | Mobile Push | ntfy und ggf. mobile Plattform-Pushes | niedrig/mittel | Alerts erreichen Mobilgeraete ggf. nicht | App-/Device-seitig | Kritische Alerts zusaetzlich in Grafana/Glance sichtbar halten |
| Operator-DR-Workstation | Bare-Metal-Recovery-Arbeitsplatz (Gaming-PC Windows, lokaler Repo-Clone `G:\Gitea_Clone\homelab-infra`) | kritisch | Ohne Workstation kein Borg-Extract, kein Hetzner-Zugriff, kein Repo-Bootstrap; der Unraid-Host ist im Bare-Metal-Fall gerade weg | Operator-PC, WSL2 + Borg-Client, SSH-Key fuer Hetzner Storage Box, Offline-Kopie der Borg-Passphrase | Setup als bewusste DR-Vorbedingung pflegen (siehe Abschnitt "DR-Workstation Bare-Metal-Kit") |
## Kritische Secrets ausserhalb des Repos ## Kritische Secrets ausserhalb des Repos
@@ -38,6 +39,24 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng
| Domain-Registrar Recovery | Domain-Besitz und Zahlung | Account, 2FA und Zahlungsweg ausserhalb des Homelabs sichern | | Domain-Registrar Recovery | Domain-Besitz und Zahlung | Account, 2FA und Zahlungsweg ausserhalb des Homelabs sichern |
| Hetzner Storage Box Zugang | Off-site Backup-Ziel | Account 2FA aktiv, Recovery Key offline gedruckt, Zahlungsweg ok; Maintenance-Key und Storage-Box-Passwort in Vaultwarden | | Hetzner Storage Box Zugang | Off-site Backup-Ziel | Account 2FA aktiv, Recovery Key offline gedruckt, Zahlungsweg ok; Maintenance-Key und Storage-Box-Passwort in Vaultwarden |
| OpenAI API Key | Paperless-GPT GPT-Zugriff | Als Stack ENV / Vaultwarden-Eintrag sichern; bei Verdacht auf Leak rotieren | | OpenAI API Key | Paperless-GPT GPT-Zugriff | Als Stack ENV / Vaultwarden-Eintrag sichern; bei Verdacht auf Leak rotieren |
| KOMODO_* Stack-ENV-Notiz | Offline-Sicherung der 5 Komodo-Werte (`KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY`) | **Status 2026-06-03: offline gesichert (Operator-Bestaetigung)**. Quelle der Werte ist die host-seitige Self-Stack-`.env` (`/mnt/user/services/stacks/komodo/.env`) bzw. die Drift-Recovery-Kopie unter `/mnt/user/appdata/secrets/_komodo_stack_env_recovery_2026-05-04.env`. Nicht im Repo, nicht in ntfy, nicht in Logs |
| GitHub-Mirror Read-Only Deploy-Key | DR-Read-Zugang zum privaten Mirror `michaelkaleschke-spec/homelab-infra` | **Status 2026-06-03: offline gesichert (Operator-Bestaetigung).** SSH-Deploy-Key `dr-readonly-2026-06-03` (ed25519, Passphrase-frei), Title in GitHub Repo Settings -> Deploy Keys: `DR Read-Only 2026-06-03`, Write-Access bewusst deaktiviert. Private Key liegt offline neben der KOMODO_*-Notiz. Smoke `git ls-remote` am 2026-06-03 erfolgreich. |
## DR-Workstation Bare-Metal-Kit
Der Operator-Gaming-PC ist im Bare-Metal-Fall die einzige Stelle, von der aus Recovery starten kann. Folgende Bestandteile gehoeren zum minimalen DR-Kit auf diesem Rechner:
| Bestandteil | Zweck | Pruefen |
|---|---|---|
| Repo-Clone `G:\Gitea_Clone\homelab-infra` (master, gefetcht) | Recovery-Anker fuer `ops/komodo/docker-compose.yml`, Restore-Skripte | `git -C G:\Gitea_Clone\homelab-infra log --oneline -1` plausibel aktuell |
| Read-Zugang zum privaten GitHub-Mirror | Fallback, falls lokaler Clone defekt | SSH-Deploy-Key `dr-readonly-2026-06-03` (ed25519, Passphrase-frei) offline im DR-Kit, ein Test-Clone pro Quartal mit `GIT_SSH_COMMAND="ssh -i <pfad-zum-key> -o IdentitiesOnly=yes" git ls-remote git@github.com:michaelkaleschke-spec/homelab-infra.git` |
| WSL2 mit Borg-Client (`apt install borgbackup`) | Borg-Extract von Hetzner Storage Box ohne laufenden Unraid-Host | `borg --version` antwortet; ein `borg list` gegen Hetzner-Repo laeuft |
| SSH-Key fuer Hetzner Storage Box | Login auf `u565255.your-storagebox.de:23` | Key ist auf der Storage Box in `authorized_keys` aktiv, getestet `ssh -p23 u565255@...` |
| Offline-Kopie Borg-Passphrase | Entschluesselung des Borg-Repos | Operator-Bestaetigung 2026-05-26; bei Reviews nur Auffindbarkeit pruefen |
| Offline-Kopie KOMODO_* Stack-ENV | Komodo-Bootstrap ohne Vaultwarden | **Status 2026-06-03: offline gesichert (Operator-Bestaetigung)** |
| Vaultwarden Master-Passwort offline | Zugriff auf Vaultwarden-Export im DR | Operator-Wissen, ggf. analog gesichert |
Operative Regel: Die DR-Workstation wird nicht als Test-/Spiel-PC betrachtet. WSL und das DR-Kit duerfen nicht unbemerkt unbrauchbar werden. Quartalsweise minimaler Trockenlauf: `borg list <hetzner-repo>` muss antworten und der Repo-Clone muss fetchbar bleiben.
## Ausfall-Szenarien ## Ausfall-Szenarien
@@ -83,3 +102,7 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng
| 2026-06-01 | Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; FRITZ!Box meldet per TR-064 FRITZ!OS `154.08.25`, Public DNS hat keine AAAA-Records, Host hat keine globale Provider-IPv6 | Account-Hygiene am 2026-06-01 nachgezogen | | 2026-06-01 | Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; FRITZ!Box meldet per TR-064 FRITZ!OS `154.08.25`, Public DNS hat keine AAAA-Records, Host hat keine globale Provider-IPv6 | Account-Hygiene am 2026-06-01 nachgezogen |
| 2026-06-01 | FRITZ!Box-UI gegengeprueft und Konfig-Backup extern/off-system in Vaultwarden abgelegt; Remote-HTTPS auf FRITZ!Box-UI aus, FTP/FTPS auf Speichermedien aus, nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus | Bei naechstem Router-Update erneut exportieren | | 2026-06-01 | FRITZ!Box-UI gegengeprueft und Konfig-Backup extern/off-system in Vaultwarden abgelegt; Remote-HTTPS auf FRITZ!Box-UI aus, FTP/FTPS auf Speichermedien aus, nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus | Bei naechstem Router-Update erneut exportieren |
| 2026-06-01 | Hetzner-Account-Hygiene erledigt: externe Mail ok, Zahlung ok, 2FA aktiv, Recovery Key offline gedruckt. Storage Box: SSH aktiv, SMB/WebDAV aus, Maintenance-Key in Vaultwarden, Borg-Repo-Zugriff nach Recovery geprueft. Borg `append-only` wird bewusst nicht umgesetzt. | Keine Folgeaktion | | 2026-06-01 | Hetzner-Account-Hygiene erledigt: externe Mail ok, Zahlung ok, 2FA aktiv, Recovery Key offline gedruckt. Storage Box: SSH aktiv, SMB/WebDAV aus, Maintenance-Key in Vaultwarden, Borg-Repo-Zugriff nach Recovery geprueft. Borg `append-only` wird bewusst nicht umgesetzt. | Keine Folgeaktion |
| 2026-06-03 | Hetzner Storage Box Maintenance-Key zusaetzlich offline gesichert bestaetigt (Operator-Antwort im DR-Tabletop 2026-06-03). Damit ist der Hetzner-Zugang im Bare-Metal-Fall ohne Vaultwarden moeglich. | Keine Folgeaktion |
| 2026-06-03 | DR-Tabletop ergibt drei offene Bootstrap-Bloecke: KOMODO_*-Notiz nicht offline, GitHub-Mirror-Read-PAT/Deploy-Key nicht angelegt, DR-Workstation nicht als DR-Kit konfiguriert. Details in `docs/DR_DRILL_2026-06-03.md` und Folge-Tasks in `docs/AUDIT_2026-05-25_TODO.md`. | KOMODO_*-Notiz erzeugen, Read-PAT erzeugen, WSL2+Borg auf Gaming-PC einrichten |
| 2026-06-03 | KOMODO_*-Notiz offline gesichert (Operator-Bestaetigung im DR-Tabletop-Followup). Quelle bleibt host-seitige `.env` (`/mnt/user/services/stacks/komodo/.env`) bzw. Drift-Recovery-Kopie vom 2026-05-04. Bare-Metal-Komodo-Bootstrap ist damit ohne Vaultwarden moeglich. | Restliche P1-Operator-Aufgaben: GitHub-Read-PAT, DR-Workstation-Setup, Nextcloud-Restore-Test |
| 2026-06-03 | GitHub-Mirror Read-Only Deploy-Key `DR Read-Only 2026-06-03` (ed25519, Passphrase-frei) erzeugt, in GitHub Repo Settings ohne Write-Access hinterlegt, Smoke `git ls-remote` erfolgreich (`d947c7f` matched master HEAD), Private-Key offline neben KOMODO_*-Notiz abgelegt, Arbeitsplatz-Kopie geloescht. | Restliche P1-Operator-Aufgaben: DR-Workstation-Setup, Nextcloud-Restore-Test |
+4 -4
View File
@@ -138,15 +138,15 @@ Stand 2026-06-03. Pro Dienst auf einen Blick: Wurde der Restore schon einmal rea
| Paperless | 2 | 2026-05-31 | File + Dump + Container + HTTP + Doc-Count | zweimonatlich (2. Sa ungerade Mon.) | | Paperless | 2 | 2026-05-31 | File + Dump + Container + HTTP + Doc-Count | zweimonatlich (2. Sa ungerade Mon.) |
| Immich | 2 | 2026-05-27 | Dump + Container + HTTP + Asset-Count | quartalsweise (2. So Feb/Mai/Aug/Nov) | | Immich | 2 | 2026-05-27 | Dump + Container + HTTP + Asset-Count | quartalsweise (2. So Feb/Mai/Aug/Nov) |
| Unraid OS Flash | 1 | - | noch kein Test | - | | Unraid OS Flash | 1 | - | noch kein Test | - |
| Traefik | 1 | - | noch kein Test | naechster Kandidat | | Traefik | 1 | 2026-06-03 | Config + LE-State + File-Provider + Ping 200 | quartalsweise |
| AdGuard Home | 1 | - | noch kein Test | - | | AdGuard Home | 1 | - | noch kein Test | - |
| Tailscale | 1 | - | noch kein Test | - | | Tailscale | 1 | - | noch kein Test | - |
| PostgreSQL 18 Cluster | 1 | - | noch kein Test (globals + per-DB) | naechster Kandidat | | PostgreSQL 18 Cluster | 1 | 2026-06-03 | globals + 5 per-DB dumps, 290 Tabellen gesamt | quartalsweise |
| Redis 8 | 1 | - | noch kein Test | - | | Redis 8 | 1 | - | noch kein Test | - |
| Komodo Mongo Daten | 1 | 2026-06-03 | mongorestore --archive --gzip, 86904 docs | quartalsweise | | Komodo Mongo Daten | 1 | 2026-06-03 | mongorestore --archive --gzip, 86904 docs | quartalsweise |
| Nextcloud | 2 | - | noch kein Test | **hoechste Prio** (occ-Choreographie) | | Nextcloud | 2 | - | noch kein Test | **hoechste Prio** (occ-Choreographie) |
| Mealie | 2 | - | noch kein Test | - | | Mealie | 2 | 2026-06-03 | File + Dump + Container + HTTP + Recipe-Count (3) | quartalsweise |
| Mail-Archiver | 2 | - | noch kein Test | - | | Mail-Archiver | 2 | 2026-06-03 | Keys + 645M Dump + Container + HTTP 200 | quartalsweise |
| Glance | 2 | - | rebuildbar, kein Test noetig | - | | Glance | 2 | - | rebuildbar, kein Test noetig | - |
| ntfy | 2 | - | rebuildbar, kein Test noetig | - | | ntfy | 2 | - | rebuildbar, kein Test noetig | - |
| Borg UI | 3 | - | rebuildbar | - | | Borg UI | 3 | - | rebuildbar | - |
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
adguard: adguard:
image: adguard/adguardhome:v0.107.76@sha256:7157eb1dc3b26c7af1d6898759a7b3f7d0fa09891fbd2d3caa6abc1057a9179b image: adguard/adguardhome:v0.107.77@sha256:e6f2b8bcda06064ab055b44933a4f0e983c35558b9cdb8d2e7ab1efcee36d890
container_name: adguard container_name: adguard
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+2 -2
View File
@@ -115,7 +115,7 @@ services:
- loki - loki
grafana: grafana:
image: grafana/grafana:13.0.1@sha256:0f86bada30d65ef9d0183b90c1e2682ac92d53d95da8bed322b984ea78a4a73a image: grafana/grafana:13.0.2@sha256:5dad0df181cb644a14e13617b913b261a54f7d4fd4510721dba420929f35bea2
container_name: monitoring-grafana container_name: monitoring-grafana
user: "0" user: "0"
restart: unless-stopped restart: unless-stopped
@@ -318,7 +318,7 @@ services:
- no-new-privileges:true - no-new-privileges:true
influxdb3-core: influxdb3-core:
image: influxdb:3.9.2-core@sha256:31ad94df2248134989b2cf73d965e51dd5f35dfae22d7ed8f4776b12e6f69f4e image: influxdb:3.9.3-core@sha256:c27c9b2ca2625b5b6966f0b09baa448102310e63a471fd60dff22646a2522e29
container_name: monitoring-influxdb3-core container_name: monitoring-influxdb3-core
user: "0" user: "0"
restart: unless-stopped restart: unless-stopped
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
borg-ui: borg-ui:
image: ainullcode/borg-ui@sha256:b44c0a92b650d80f215a986dadda5c2604c61eb28a7571e19c046eff41d761e7 image: ainullcode/borg-ui@sha256:acb0fbe83dc4a3843abc06f814c5f1061a0701b2cfc574da2e851d17a34ab745
container_name: borg-ui container_name: borg-ui
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
code-server: code-server:
image: lscr.io/linuxserver/code-server:4.122.0@sha256:0caf1b65ebec84b94397108b56da6c33f124c5390f5832da94e75f4609c0e2ad image: lscr.io/linuxserver/code-server:4.122.1@sha256:21302fbcedc15e78a6f542cb78e4b77cf660f19664a01cd359d81d666b6cb6fd
container_name: code-server container_name: code-server
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
filebrowser: filebrowser:
image: filebrowser/filebrowser:v2.63.5@sha256:aefb0c20de10ef8b617995ca5522479ad40d41e6386bd01946a345c6026ff31c image: filebrowser/filebrowser:v2.63.10@sha256:b0308060fb45b7767f7cf4a21d40e5e0152bad798794907769288a7a12436895
container_name: filebrowser container_name: filebrowser
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
+6 -2
View File
@@ -38,9 +38,13 @@ pg_header_ok() {
if ! command -v pg_restore >/dev/null 2>&1; then if ! command -v pg_restore >/dev/null 2>&1; then
# ohne Host-pg_restore: in laufendem Postgres-Container probieren # ohne Host-pg_restore: in laufendem Postgres-Container probieren
if command -v docker >/dev/null 2>&1 && docker inspect postgresql17 >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1 && docker inspect postgresql17 >/dev/null 2>&1; then
docker exec -i postgresql17 pg_restore --list </"$path" >/dev/null 2>&1 && return 0 if docker exec -i postgresql17 pg_restore --list < "$path" >/dev/null 2>&1; then
return 0 # Header valide
else
return 1 # Header korrupt
fi
fi fi
return 2 # nicht pruefbar return 2 # nicht pruefbar (kein pg_restore, kein Container)
fi fi
pg_restore --list "$path" >/dev/null 2>&1 pg_restore --list "$path" >/dev/null 2>&1
} }
@@ -3,7 +3,7 @@ services:
# Schreibt in den Restore-Lab-Pfad, NICHT in das produktive # Schreibt in den Restore-Lab-Pfad, NICHT in das produktive
# /mnt/user/appdata/komodo/mongo-Volume. # /mnt/user/appdata/komodo/mongo-Volume.
restoretest-komodo-mongo: restoretest-komodo-mongo:
image: mongo:7.0.32@sha256:32979a1189dfdc44da3f5ed40d910495f5ad8f6f7f77556646f890a30b2d3f56 image: mongo:8.0.23@sha256:44aa79ae28ff80b56fe58681b66cda9336706df408a5175a6c04988aa54610d3
container_name: restoretest-komodo-mongo container_name: restoretest-komodo-mongo
restart: "no" restart: "no"
command: --quiet command: --quiet
@@ -1,6 +1,6 @@
services: services:
restoretest-komodo-mongorestore: restoretest-komodo-mongorestore:
image: mongo:7.0.32@sha256:32979a1189dfdc44da3f5ed40d910495f5ad8f6f7f77556646f890a30b2d3f56 image: mongo:8.0.23@sha256:44aa79ae28ff80b56fe58681b66cda9336706df408a5175a6c04988aa54610d3
container_name: restoretest-komodo-mongorestore container_name: restoretest-komodo-mongorestore
restart: "no" restart: "no"
command: --quiet command: --quiet
@@ -0,0 +1,41 @@
services:
restoretest-mailarchiver-postgres:
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
container_name: restoretest-mailarchiver-postgres
restart: "no"
environment:
TZ: Europe/Berlin
POSTGRES_USER: mailarchiver
POSTGRES_DB: mailarchiver
POSTGRES_PASSWORD: restoretest-mailarchiver-db
PGDATA: /var/lib/postgresql/18/docker
volumes:
- /mnt/user/backups/restore-lab/mailarchiver/postgres:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mailarchiver -d mailarchiver"]
interval: 10s
timeout: 5s
retries: 10
security_opt:
- no-new-privileges:true
restoretest-mailarchiver:
image: s1t5/mailarchiver@sha256:ea7fd8c2e3e0ef0941e8dd9e726e35a8de33296f5c7b9ed811df5168ae6a9714
container_name: restoretest-mailarchiver
restart: "no"
depends_on:
restoretest-mailarchiver-postgres:
condition: service_healthy
environment:
TZ: Europe/Berlin
# Wegwerf-Connection-String fuer isolierten Test.
# Produktiver MAILARCHIVER_DB_CONNECTION ist Stack-ENV-only und wird
# hier bewusst NICHT verwendet.
ConnectionStrings__DefaultConnection: "Host=restoretest-mailarchiver-postgres;Database=mailarchiver;Username=mailarchiver;Password=restoretest-mailarchiver-db"
Authentication__Password: restoretest-mailarchiver-auth
ports:
- "127.0.0.1:15000:5000"
volumes:
- /mnt/user/backups/restore-lab/mailarchiver/data-protection-keys:/app/DataProtection-Keys
security_opt:
- no-new-privileges:true
@@ -0,0 +1,172 @@
#!/bin/bash
set -euo pipefail
# Mail-Archiver Restore Smoke Test
#
# Borg-Extract der Data-Protection-Keys + pg_restore des mailarchiver-Dumps
# in isoliertes Test-Postgres + Container-Boot + HTTP-Smoke.
#
# In Produktion nutzt Mail-Archiver die Shared PostgreSQL 18 — im Test
# bekommt er ein eigenes isoliertes Test-Postgres mit Wegwerf-Credentials.
# Authelia-ForwardAuth wird im Smoke nicht geprueft (kein Traefik, kein
# Auth-Middleware).
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/mailarchiver"
REPORT_ROOT="/mnt/user/backups/restore-reports"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/mailarchiver-extract"
COMPOSE_FILE="$SCRIPT_DIR/mailarchiver-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/mailarchiver-$(date +%F).md"
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/postgresql17-mailarchiver.dump"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Mail-Archiver restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
Borg source: local/appdata/mailarchiver/data-protection-keys
Host dump: $DUMP_HOST_PATH (645M)
Test endpoint: 127.0.0.1:15000
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
require_path "$DUMP_HOST_PATH"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "mailarchiver" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT/data-protection-keys" "$RESTORE_ROOT/postgres"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
if [ -z "$archive" ] || [ -z "$repo" ]; then
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
exit 1
fi
# Stufe 1: Data-Protection-Keys aus Borg
borg_extract "/restore/mailarchiver-extract" "local/appdata/mailarchiver/data-protection-keys"
if [ ! -d "$EXTRACT_DIR/local/appdata/mailarchiver/data-protection-keys" ]; then
echo "Mailarchiver data-protection-keys path missing in Borg archive" >&2
exit 1
fi
cp -a "$EXTRACT_DIR/local/appdata/mailarchiver/data-protection-keys/." "$RESTORE_ROOT/data-protection-keys/"
chmod -R a+rwX "$RESTORE_ROOT/data-protection-keys"
# Stufe 2: Test-Postgres + Dump
docker compose -f "$COMPOSE_FILE" up -d restoretest-mailarchiver-postgres >/dev/null
until docker exec restoretest-mailarchiver-postgres pg_isready -U mailarchiver -d mailarchiver >/dev/null 2>&1; do
sleep 2
done
restore_ok=0
for attempt in $(seq 1 12); do
if docker exec -i restoretest-mailarchiver-postgres \
pg_restore -U mailarchiver -d mailarchiver --clean --if-exists --no-owner --no-privileges \
< "$DUMP_HOST_PATH" 2>/tmp/mailarchiver-pg-restore.err; then
restore_ok=1
break
fi
if grep -qiE "starting up|shutting down|connection refused" /tmp/mailarchiver-pg-restore.err; then
sleep 5
continue
fi
if grep -qiE "FATAL|PANIC" /tmp/mailarchiver-pg-restore.err; then
cat /tmp/mailarchiver-pg-restore.err >&2
exit 1
fi
restore_ok=1
break
done
if [ "$restore_ok" -ne 1 ]; then
cat /tmp/mailarchiver-pg-restore.err >&2
exit 1
fi
# Stufe 3: Container starten
docker compose -f "$COMPOSE_FILE" up -d restoretest-mailarchiver >/dev/null
# Mailarchiver ist ein .NET-App, braucht ein paar Sekunden fuer DB-Migration.
# Smoke gegen den Root-Endpunkt — bei Authelia-geschuetztem Dienst liefert
# der Container selbst trotzdem einen HTTP-Response (302 oder 200).
http_status=""
for _ in $(seq 1 60); do
http_status="$(curl -s -o /tmp/mailarchiver-body.html -w '%{http_code}' \
-L http://127.0.0.1:15000/ || true)"
if [ "$http_status" = "200" ] || [ "$http_status" = "302" ] || [ "$http_status" = "401" ]; then
break
fi
sleep 2
done
if [ "$http_status" != "200" ] && [ "$http_status" != "302" ] && [ "$http_status" != "401" ]; then
echo "Mailarchiver HTTP smoke failed: status=$http_status" >&2
docker logs --tail 80 restoretest-mailarchiver >&2 || true
exit 1
fi
# Tabellen-Count als Sanity
table_count="$(docker exec restoretest-mailarchiver-postgres \
psql -U mailarchiver -d mailarchiver -tAc \
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
write_report "$REPORT_FILE" <<EOF
# Mail-Archiver Restore Test Report - $(date +%F)
- Service: \`mail-archiver\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore root: \`$RESTORE_ROOT\`
- Test containers: \`restoretest-mailarchiver\`, \`restoretest-mailarchiver-postgres\`
- Test endpoint: \`http://127.0.0.1:15000/\`
- Result: \`SUCCESS\`
## Checks
- Borg extract of data-protection-keys: \`ok\`
- Host dump copy (645M): \`ok\`
- Dump import into isolated Postgres: \`ok\`
- HTTP status: \`$http_status\`
- Public table count in test DB: \`$table_count\`
## Notes
- Productive secrets (MAILARCHIVER_DB_CONNECTION, MAILARCHIVER_AUTH_PASSWORD) NOT used.
- Authelia ForwardAuth NOT tested (no Traefik in smoke).
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Mailarchiver restore test ok -> $REPORT_FILE"
+45
View File
@@ -0,0 +1,45 @@
services:
restoretest-mealie-postgres:
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
container_name: restoretest-mealie-postgres
restart: "no"
environment:
TZ: Europe/Berlin
POSTGRES_USER: mealie
POSTGRES_DB: mealie
POSTGRES_PASSWORD: restoretest-mealie-db
PGDATA: /var/lib/postgresql/18/docker
volumes:
- /mnt/user/backups/restore-lab/mealie/postgres:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mealie -d mealie"]
interval: 10s
timeout: 5s
retries: 10
security_opt:
- no-new-privileges:true
restoretest-mealie:
image: ghcr.io/mealie-recipes/mealie:v3.19.2@sha256:f68e959bf66f4f458893ea58facac71690fe6f2ac7a31466b5cecb41b4e99c02
container_name: restoretest-mealie
restart: "no"
depends_on:
restoretest-mealie-postgres:
condition: service_healthy
environment:
TZ: Europe/Berlin
ALLOW_SIGNUP: "false"
PUID: "99"
PGID: "100"
DB_ENGINE: postgres
POSTGRES_SERVER: restoretest-mealie-postgres
POSTGRES_DB: mealie
POSTGRES_USER: mealie
POSTGRES_PASSWORD: restoretest-mealie-db
BASE_URL: http://127.0.0.1:19925
ports:
- "127.0.0.1:19925:9000"
volumes:
- /mnt/user/backups/restore-lab/mealie/data:/app/data
security_opt:
- no-new-privileges:true
+162
View File
@@ -0,0 +1,162 @@
#!/bin/bash
set -euo pipefail
# Mealie Restore Smoke Test
#
# Borg-Extract der App-Daten + pg_restore des mealie.dump in isoliertes
# Test-Postgres + Mealie-Boot + HTTP-Smoke.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/mealie"
REPORT_ROOT="/mnt/user/backups/restore-reports"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/mealie-extract"
COMPOSE_FILE="$SCRIPT_DIR/mealie-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/mealie-$(date +%F).md"
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/mealie.dump"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Mealie restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
Borg source: local/appdata/mealie/data
Host dump: $DUMP_HOST_PATH
Test endpoint: 127.0.0.1:19925
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
require_path "$DUMP_HOST_PATH"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "mealie" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT/data" "$RESTORE_ROOT/postgres"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
if [ -z "$archive" ] || [ -z "$repo" ]; then
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
exit 1
fi
# Stufe 1: App-Daten aus Borg
borg_extract "/restore/mealie-extract" "local/appdata/mealie/data"
if [ ! -d "$EXTRACT_DIR/local/appdata/mealie/data" ]; then
echo "Mealie data path missing in Borg archive" >&2
exit 1
fi
cp -a "$EXTRACT_DIR/local/appdata/mealie/data/." "$RESTORE_ROOT/data/"
chmod -R a+rwX "$RESTORE_ROOT/data"
# Stufe 2: Test-Postgres hochfahren + Dump einspielen
docker compose -f "$COMPOSE_FILE" up -d restoretest-mealie-postgres >/dev/null
until docker exec restoretest-mealie-postgres pg_isready -U mealie -d mealie >/dev/null 2>&1; do
sleep 2
done
restore_ok=0
for attempt in $(seq 1 12); do
if docker exec -i restoretest-mealie-postgres \
pg_restore -U mealie -d mealie --clean --if-exists --no-owner --no-privileges \
< "$DUMP_HOST_PATH" 2>/tmp/mealie-pg-restore.err; then
restore_ok=1
break
fi
if grep -qiE "starting up|shutting down|connection refused" /tmp/mealie-pg-restore.err; then
sleep 5
continue
fi
if grep -qiE "FATAL|PANIC" /tmp/mealie-pg-restore.err; then
cat /tmp/mealie-pg-restore.err >&2
exit 1
fi
restore_ok=1
break
done
if [ "$restore_ok" -ne 1 ]; then
cat /tmp/mealie-pg-restore.err >&2
exit 1
fi
# Stufe 3: Mealie starten
docker compose -f "$COMPOSE_FILE" up -d restoretest-mealie >/dev/null
http_status=""
for _ in $(seq 1 60); do
http_status="$(curl -s -o /tmp/mealie-body.html -w '%{http_code}' \
-L http://127.0.0.1:19925/api/app/about || true)"
if [ "$http_status" = "200" ]; then
break
fi
sleep 2
done
if [ "$http_status" != "200" ]; then
echo "Mealie HTTP smoke failed: status=$http_status" >&2
docker logs --tail 80 restoretest-mealie >&2 || true
exit 1
fi
# Rezept-Count als Sanity-Check
recipe_count="$(docker exec restoretest-mealie-postgres \
psql -U mealie -d mealie -tAc \
"SELECT count(*) FROM recipes;" 2>/dev/null | tr -d '[:space:]' || echo "n/a")"
write_report "$REPORT_FILE" <<EOF
# Mealie Restore Test Report - $(date +%F)
- Service: \`mealie\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore root: \`$RESTORE_ROOT\`
- Test containers: \`restoretest-mealie\`, \`restoretest-mealie-postgres\`
- Test endpoint: \`http://127.0.0.1:19925/api/app/about\`
- Result: \`SUCCESS\`
## Checks
- Borg extract of data: \`ok\`
- Host dump copy: \`ok\`
- Dump import into isolated Postgres: \`ok\`
- HTTP status from /api/app/about: \`$http_status\`
- Recipe count in test DB: \`$recipe_count\`
## Notes
- Productive Mealie secrets were NOT mounted; test uses throwaway DB password.
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Mealie restore test ok -> $REPORT_FILE"
+25 -1
View File
@@ -58,8 +58,32 @@ case "$MODE" in
fi fi
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh" exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh"
;; ;;
traefik)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/traefik-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/traefik-restore-test.sh"
;;
mailarchiver)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/mailarchiver-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/mailarchiver-restore-test.sh"
;;
mealie)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/mealie-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/mealie-restore-test.sh"
;;
shared-pg-cluster)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
;;
*) *)
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich|authelia|nextcloud|komodo-bootstrap|komodo-mongo-restore} [--what-if]" >&2 echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich|authelia|nextcloud|komodo-bootstrap|komodo-mongo-restore|shared-pg-cluster} [--what-if]" >&2
exit 1 exit 1
;; ;;
esac esac
@@ -0,0 +1,19 @@
services:
restoretest-shared-pg:
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
container_name: restoretest-shared-pg
restart: "no"
environment:
TZ: Europe/Berlin
POSTGRES_USER: postgres
POSTGRES_PASSWORD: restoretest-shared-pg-superuser
PGDATA: /var/lib/postgresql/18/docker
volumes:
- /mnt/user/backups/restore-lab/shared-pg-cluster/data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 10
security_opt:
- no-new-privileges:true
@@ -0,0 +1,239 @@
#!/bin/bash
set -euo pipefail
# Shared PostgreSQL 18 Cluster Restore Drill
#
# Beweist, dass der komplette Shared-Postgres-Cluster aus den Dump-Artefakten
# wiederhergestellt werden kann:
# 1. Globals (Rollen) aus pg_dumpall --globals-only
# 2. Per-DB Custom-Format-Dumps: paperless, mailarchiver, authelia,
# nextcloud, mealie
#
# Bekannter Sonderfall (docs/RESTORE_MATRIX.md):
# - CREATE ROLE mailarchiver scheitert, weil der User gleichzeitig der
# Dump-Admin-User ist. Das ALTER ROLE danach muss trotzdem durchlaufen.
# Der Test toleriert diesen spezifischen Fehler.
#
# Produktive PostgreSQL-Container und -Datenpfade werden NICHT angefasst.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/shared-pg-cluster"
REPORT_ROOT="/mnt/user/backups/restore-reports"
COMPOSE_FILE="$SCRIPT_DIR/shared-pg-cluster-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/shared-pg-cluster-$(date +%F).md"
DUMP_ROOT="/mnt/user/backups/borg/dumps/latest"
# Alle erwarteten Dumps
GLOBALS_DUMP="$DUMP_ROOT/postgresql17-globals.sql"
PAPERLESS_DUMP="$DUMP_ROOT/postgresql17-paperless.dump"
MAILARCHIVER_DUMP="$DUMP_ROOT/postgresql17-mailarchiver.dump"
AUTHELIA_DUMP="$DUMP_ROOT/postgresql17-authelia.dump"
NEXTCLOUD_DUMP="$DUMP_ROOT/nextcloud.dump"
MEALIE_DUMP="$DUMP_ROOT/mealie.dump"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Shared PostgreSQL 18 Cluster Restore Drill
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
Dumps from: $DUMP_ROOT
Steps:
1. Frisches postgres:18.4 mit Superuser hochfahren
2. Globals einspielen (pg_dumpall --globals-only)
-> bekannter mailarchiver-Rollenkonflikt wird toleriert
3. DBs anlegen: paperless, mailarchiver, authelia, nextcloud, mealie
4. Per-DB pg_restore fuer jede DB
5. Tabellen-Count pro DB als Sanity-Check
6. Report schreiben
EOF
exit 0
fi
require_cmd docker
require_path "$COMPOSE_FILE"
require_path "$GLOBALS_DUMP"
require_path "$PAPERLESS_DUMP"
require_path "$MAILARCHIVER_DUMP"
# Authelia/Nextcloud/Mealie-Dumps sind optional (koennen fehlen)
OPTIONAL_DUMPS=""
RESTORE_SUCCESS=0
cleanup() {
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "shared-pg-cluster" "$RESTORE_ROOT"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
}
trap cleanup EXIT
rm -rf "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT/data"
# Stufe 1: Test-Postgres hochfahren
docker compose -f "$COMPOSE_FILE" up -d restoretest-shared-pg >/dev/null
until docker exec restoretest-shared-pg pg_isready -U postgres >/dev/null 2>&1; do
sleep 2
done
# Extra Wartezeit fuer Entrypoint-Init
sleep 3
# Stufe 2: Globals einspielen
# Der Globals-Dump enthaelt CREATE ROLE fuer alle DB-User. Der bekannte
# Konflikt ist, dass CREATE ROLE mailarchiver scheitern kann wenn dieser
# User auch der Dump-Admin ist. Wir tolerieren das und pruefen nur auf
# FATAL/PANIC.
globals_status="ok"
docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
psql -U postgres -f - < "$GLOBALS_DUMP" >/tmp/shared-pg-globals.log 2>&1 || true
if grep -qiE "FATAL|PANIC" /tmp/shared-pg-globals.log; then
globals_status="failed (FATAL/PANIC)"
cat /tmp/shared-pg-globals.log >&2
exit 1
fi
# Stufe 3: DBs anlegen und Dumps einspielen
declare -A DB_STATUS
declare -A TABLE_COUNTS
restore_db() {
local dbname="$1"
local dbuser="$2"
local dump_path="$3"
local optional="${4:-no}"
if [ ! -f "$dump_path" ]; then
if [ "$optional" = "yes" ]; then
DB_STATUS[$dbname]="skipped (dump missing)"
TABLE_COUNTS[$dbname]="n/a"
return 0
fi
DB_STATUS[$dbname]="failed (dump missing)"
TABLE_COUNTS[$dbname]="n/a"
return 1
fi
# Rolle anlegen falls nicht durch Globals erzeugt (idempotent)
docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
psql -U postgres -c "DO \$\$ BEGIN CREATE ROLE $dbuser WITH LOGIN PASSWORD 'restoretest-$dbuser'; EXCEPTION WHEN duplicate_object THEN NULL; END \$\$;" >/dev/null 2>&1 || true
# DB anlegen
docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
psql -U postgres -c "SELECT 1 FROM pg_database WHERE datname='$dbname'" 2>/dev/null | grep -q 1 || \
docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
createdb -U postgres -O "$dbuser" "$dbname" 2>/dev/null || true
# pg_restore mit Retry
local restore_ok=0
for attempt in $(seq 1 5); do
if docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
pg_restore -U postgres -d "$dbname" --clean --if-exists --no-owner --no-privileges \
< "$dump_path" 2>/tmp/shared-pg-restore-${dbname}.err; then
restore_ok=1
break
fi
if grep -qiE "starting up|shutting down|connection refused" /tmp/shared-pg-restore-${dbname}.err; then
sleep 5
continue
fi
# --clean erzeugt "does not exist" Warnungen beim ersten Import -> ignorieren
if grep -qiE "FATAL|PANIC" /tmp/shared-pg-restore-${dbname}.err; then
DB_STATUS[$dbname]="failed"
TABLE_COUNTS[$dbname]="n/a"
cat /tmp/shared-pg-restore-${dbname}.err >&2
return 1
fi
restore_ok=1
break
done
if [ "$restore_ok" -ne 1 ]; then
DB_STATUS[$dbname]="failed (timeout)"
TABLE_COUNTS[$dbname]="n/a"
return 1
fi
DB_STATUS[$dbname]="ok"
# Tabellen zaehlen
TABLE_COUNTS[$dbname]="$(docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
psql -U postgres -d "$dbname" -tAc \
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
}
restore_db "paperless" "paperless" "$PAPERLESS_DUMP"
restore_db "mailarchiver" "mailarchiver" "$MAILARCHIVER_DUMP"
restore_db "authelia" "authelia" "$AUTHELIA_DUMP" "yes"
restore_db "nextcloud" "nextcloud" "$NEXTCLOUD_DUMP" "yes"
restore_db "mealie" "mealie" "$MEALIE_DUMP" "yes"
# Stufe 4: data_checksums pruefen
checksums="$(docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
psql -U postgres -tAc "SHOW data_checksums;" 2>/dev/null | tr -d '[:space:]' || echo "n/a")"
# Stufe 5: DB-Liste
db_list="$(docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
psql -U postgres -tAc "SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY datname;" \
2>/dev/null | tr '\n' ', ' | sed 's/,$//' || echo "n/a")"
# Report bauen
report_body="# Shared PostgreSQL 18 Cluster Restore Drill - $(date +%F)
- Dump source: \`$DUMP_ROOT\`
- Restore root: \`$RESTORE_ROOT\`
- Result: \`SUCCESS\`
## Checks
- Test-Postgres healthy: \`ok\`
- Globals import: \`$globals_status\`
- data_checksums: \`$checksums\`
- Databases: \`$db_list\`
## Per-DB Restore
| Database | Restore | Tables |
|---|---|---|
| paperless | \`${DB_STATUS[paperless]}\` | \`${TABLE_COUNTS[paperless]}\` |
| mailarchiver | \`${DB_STATUS[mailarchiver]}\` | \`${TABLE_COUNTS[mailarchiver]}\` |
| authelia | \`${DB_STATUS[authelia]}\` | \`${TABLE_COUNTS[authelia]}\` |
| nextcloud | \`${DB_STATUS[nextcloud]}\` | \`${TABLE_COUNTS[nextcloud]}\` |
| mealie | \`${DB_STATUS[mealie]}\` | \`${TABLE_COUNTS[mealie]}\` |
## Scope
Dieser Drill beweist, dass der gesamte Shared-PostgreSQL-18-Cluster aus
den taeglichen Dump-Artefakten wiederhergestellt werden kann: Globals
(Rollen) + per-DB Custom-Format-Dumps. Der bekannte mailarchiver-
Bootstrap-Rollenkonflikt wird toleriert.
## Notes
- Produktive PostgreSQL-Container und -Datenpfade wurden nicht beruehrt.
- Test-Postgres nutzt Wegwerf-Superuser-Passwort.
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
"
write_report "$REPORT_FILE" <<EOF
$report_body
EOF
RESTORE_SUCCESS=1
echo "Shared PG cluster restore drill ok -> $REPORT_FILE"
@@ -0,0 +1,29 @@
services:
restoretest-traefik:
image: traefik:v3.7@sha256:6b9cbca6fac42ab0075f5437d8dc1685cfd188626d8d515839ea94f8b6271c42
container_name: restoretest-traefik
restart: "no"
command:
# Minimale Config fuer den Smoke: kein Docker-Provider (kein docker.sock),
# kein ACME (kein CF-Token), kein TLS-Entrypoint. Nur:
# - File-Provider mit restauriertem dynamic/-Verzeichnis
# - HTTP-Entrypoint auf Test-Port
# - Ping-Endpoint fuer Health-Check
- --ping=true
- --ping.entrypoint=web
- --providers.file.directory=/dynamic
- --providers.file.watch=false
- --entrypoints.web.address=:80
- --api.dashboard=false
- --log.level=INFO
ports:
- "127.0.0.1:18880:80"
volumes:
# Restauriertes dynamic/-Verzeichnis aus Borg (read-only)
- /mnt/user/backups/restore-lab/traefik/dynamic:/dynamic:ro
# Restauriertes letsencrypt/ fuer Existenz-Nachweis (read-only)
- /mnt/user/backups/restore-lab/traefik/letsencrypt:/letsencrypt:ro
# KEIN docker.sock — Test-Traefik darf keine produktiven Container discovern
# KEIN CF-Token — keine DNS-Challenge im Smoke
security_opt:
- no-new-privileges:true
+153
View File
@@ -0,0 +1,153 @@
#!/bin/bash
set -euo pipefail
# Traefik Restore Smoke Test
#
# Beweist, dass die restaurierten Traefik-Dateien (dynamic/, letsencrypt/)
# aus dem Borg-Archiv einen funktionsfaehigen Traefik-Start ermoeglichen.
#
# Scope:
# - Borg-Extract von dynamic/ und letsencrypt/
# - Traefik startet mit File-Provider gegen restauriertes dynamic/
# - /ping Health-Endpoint antwortet
# - acme.json aus letsencrypt/ ist vorhanden und nicht leer
#
# Bewusst NICHT Teil des Smokes:
# - Docker-Provider (kein docker.sock im Test — wuerde produktive Container discovern)
# - ACME/DNS-Challenge (kein CF-Token im Test)
# - TLS-Terminierung (kein Cert-Test)
# - Produktive Ports 80/443
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
KEEP_DATA=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
--keep-data) KEEP_DATA=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
RESTORE_ROOT="/mnt/user/backups/restore-lab/traefik"
REPORT_ROOT="/mnt/user/backups/restore-reports"
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/traefik-extract"
COMPOSE_FILE="$SCRIPT_DIR/traefik-compose.test.yml"
REPORT_FILE="$REPORT_ROOT/traefik-$(date +%F).md"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Traefik restore test
Mode: WhatIf
RestoreRoot: $RESTORE_ROOT
Borg source: local/appdata/traefik
Test endpoint: 127.0.0.1:18880/ping
Scope: File-Provider + Ping, kein Docker-Provider, kein ACME, kein CF-Token
EOF
exit 0
fi
require_cmd docker
require_cmd curl
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
require_path "$COMPOSE_FILE"
RESTORE_SUCCESS=0
cleanup() {
cleanup_compose "$COMPOSE_FILE"
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
preserve_on_failure "traefik" "$RESTORE_ROOT"
rm -rf "$EXTRACT_DIR"
return
fi
if [ "$KEEP_DATA" -ne 1 ]; then
rm -rf "$RESTORE_ROOT"
fi
rm -rf "$EXTRACT_DIR"
}
trap cleanup EXIT
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
mkdir -p "$RESTORE_ROOT"
archive="$(latest_archive_name)"
repo="$(borg_repo_url)"
if [ -z "$archive" ] || [ -z "$repo" ]; then
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
exit 1
fi
# Stufe 1: Traefik-Dateien aus Borg (dynamic/ + letsencrypt/)
# Secrets/ wird bewusst NICHT extrahiert (enthaelt CF-Token)
borg_extract "/restore/traefik-extract" \
"local/appdata/traefik/dynamic" \
"local/appdata/traefik/letsencrypt"
if [ ! -d "$EXTRACT_DIR/local/appdata/traefik/dynamic" ]; then
echo "Traefik dynamic/ path missing in Borg archive" >&2
exit 1
fi
cp -a "$EXTRACT_DIR/local/appdata/traefik/dynamic" "$RESTORE_ROOT/dynamic"
cp -a "$EXTRACT_DIR/local/appdata/traefik/letsencrypt" "$RESTORE_ROOT/letsencrypt"
chmod -R a+rX "$RESTORE_ROOT/dynamic" "$RESTORE_ROOT/letsencrypt"
# Stufe 2: Datei-Checks
dynamic_files="$(find "$RESTORE_ROOT/dynamic" -type f | wc -l)"
acme_size="$(stat -c %s "$RESTORE_ROOT/letsencrypt/acme.json" 2>/dev/null || echo 0)"
# Stufe 3: Traefik starten
docker compose -f "$COMPOSE_FILE" up -d restoretest-traefik >/dev/null
http_status=""
for _ in $(seq 1 30); do
http_status="$(curl -s -o /dev/null -w '%{http_code}' \
http://127.0.0.1:18880/ping || true)"
if [ "$http_status" = "200" ]; then
break
fi
sleep 2
done
if [ "$http_status" != "200" ]; then
echo "Traefik /ping smoke failed: status=$http_status" >&2
docker logs --tail 60 restoretest-traefik >&2 || true
exit 1
fi
write_report "$REPORT_FILE" <<EOF
# Traefik Restore Test Report - $(date +%F)
- Service: \`traefik\`
- Source repo: \`$repo\`
- Archive: \`$archive\`
- Restore root: \`$RESTORE_ROOT\`
- Test container: \`restoretest-traefik\`
- Test endpoint: \`http://127.0.0.1:18880/ping\`
- Result: \`SUCCESS\`
## Checks
- Borg extract of dynamic/: \`ok\` ($dynamic_files files)
- Borg extract of letsencrypt/: \`ok\` (acme.json ${acme_size} bytes)
- Traefik /ping health: \`$http_status\`
## Scope
Config-Restore + File-Provider-Boot + Ping-Health. Kein Docker-Provider
(docker.sock nicht gemountet), kein ACME/DNS-Challenge (CF-Token nicht
im Test), keine produktiven Ports.
## Notes
- Productive CF-Token under /mnt/user/appdata/traefik/secrets/ was NOT extracted or mounted.
- dynamic/ contains middlewares.yml and usersfile — Traefik loads them via File-Provider.
- acme.json is present and non-empty; TLS cert validity not tested in smoke.
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
EOF
RESTORE_SUCCESS=1
echo "Traefik restore test ok -> $REPORT_FILE"
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
scrutiny: scrutiny:
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:41c5faefb96766d27d58a829fa19b3f4f27da4160926de3255cf142a85a90c12 image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:d8be7c11950cfdd2ec8d85f8d76cc67fedfa2825eead6cc60d0ed753492fb5f5
container_name: scrutiny container_name: scrutiny
restart: unless-stopped restart: unless-stopped
privileged: true privileged: true
+3 -1
View File
@@ -41,10 +41,12 @@ access_control:
- git.kaleschke.info - git.kaleschke.info
policy: bypass policy: bypass
# Admin-Dienste - 2FA erforderlich # Admin-Dienste - 2FA erforderlich (Operator-UIs mit Host-/Backup-Zugriff)
- domain: - domain:
- files.kaleschke.info - files.kaleschke.info
- scrutiny.kaleschke.info - scrutiny.kaleschke.info
- borg.kaleschke.info
- code.kaleschke.info
policy: two_factor policy: two_factor
# Alles andere mit Authelia-Middleware - 1FA. # Alles andere mit Authelia-Middleware - 1FA.