Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fed6a6a0b | |||
| c47639ecf4 | |||
| b158f9d871 | |||
| d947c7f066 | |||
| 9edd6c24e6 | |||
| 7a513e9fc8 | |||
| 4b96d13510 | |||
| 642eb88b40 | |||
| dd494046ce | |||
| 16d3b8f2fa | |||
| a9b232195d | |||
| 5ee4a158d6 | |||
| 86435d4091 | |||
| 5e52316fab | |||
| 8a4df239fa | |||
| 893b34a585 | |||
| d1f9491b24 |
@@ -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).
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
```
|
||||||
@@ -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 |
|
||||||
|
|||||||
@@ -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,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:v2.63.7@sha256:40ba654524feb29afd28de458fdcf996b119cd1b53be2630d7658e01419f5891
|
image: filebrowser/filebrowser:v2.63.10@sha256:b0308060fb45b7767f7cf4a21d40e5e0152bad798794907769288a7a12436895
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -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
|
||||||
return 2 # nicht pruefbar
|
fi
|
||||||
|
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"
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user