Compare commits
51 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 67ec40b762 | |||
| abf7137aea | |||
| 8095ab8b5d | |||
| 3bd35434d6 | |||
| ad9267c66a | |||
| 489958af18 | |||
| c16d62a04a | |||
| bdae014bff | |||
| 30aa696e61 | |||
| e4b0db2af6 | |||
| 1a4929f9ef | |||
| 2c0076c6a6 | |||
| 7da64ff316 | |||
| 12b63531d1 | |||
| 3daea94982 | |||
| 0ca29069c7 | |||
| eedb08316d | |||
| 54a7a0e783 | |||
| c677ef0515 | |||
| 2b60a58753 | |||
| 7d64248710 | |||
| edcb34c3f3 | |||
| 19604e0114 | |||
| 3c71a66c55 | |||
| 24d0d90670 | |||
| 0ae44bd797 | |||
| 0723eccca1 | |||
| 3bfecdd291 | |||
| c4fd4154db | |||
| dddb33d900 | |||
| 8eac93c1a5 | |||
| cfa02ce627 | |||
| 52414c47be | |||
| a8c440d4da | |||
| 12cf8fb728 | |||
| 5b0782a8fa | |||
| a805f03481 | |||
| 4feecf4a8e | |||
| 2e84700326 | |||
| 8a19c45485 | |||
| 6a445094bd | |||
| fc59e35c57 | |||
| 8e111d1e04 | |||
| 85a0eb4c3a | |||
| 38c3d87722 | |||
| c5d231a0db | |||
| 48099fb48d | |||
| 5c5ca2fcec | |||
| 3b438324dc | |||
| 0625594443 | |||
| 5936a4d9c1 |
@@ -271,7 +271,7 @@ Legende Status:
|
||||
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
||||
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
|
||||
| `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels |
|
||||
| `plex` | ✅ | `host` | Plex native / Host-Netz | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme | — |
|
||||
| `plex` | ✅ | `host` | Plex native, **LAN/Tailscale-only** (Remote Access aus seit 2026-05-28) | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme; Server geclaimt von `Xeridos`; Smart-TVs (Schlafzimmer, Wohnzimmer) ueber WLAN-LAN per mDNS | — |
|
||||
|
||||
### 7.5 Admin / Operations
|
||||
|
||||
@@ -395,7 +395,7 @@ Für den laufenden Betrieb gilt stattdessen:
|
||||
| `Komodo` | Docker-Socket Zugriff | Stack-Deployments benötigen Socket |
|
||||
| `glance-docker-socket-proxy` | Docker-Socket read-only | Glance benoetigt Containerstatus; Zugriff wird ueber einen internen Socket-Proxy auf lesende Docker-API-Endpunkte begrenzt und nicht ins `frontend_net` gelegt |
|
||||
| `Komodo` | keine pauschale zentrale Middleware | Webhooks (`/listener`), API und Periphery-WebSocket (`/ws/periphery`) sollen nicht durch vorgeschaltete ForwardAuth gebrochen werden |
|
||||
| `gitea` | SSH-Port 222 direkt gebunden | Git-SSH-Zugang; kein HTTP-Proxy für SSH möglich |
|
||||
| `gitea` | SSH-Port 222 direkt gebunden (LAN/Tailscale) | Git-SSH-Zugang; kein HTTP-Proxy für SSH möglich. Bewusst **nicht** in FRITZ!Box-WAN freigegeben (Operator-Entscheidung 2026-05-28): Tailscale ist Operator-Pfad, GitHub-Mirror deckt DR-Bootstrap ab, SSH-Brute-Force-Vektor extern vermeiden. |
|
||||
| `ddns-updater` | bleibt in `frontend_net` statt `backend_net` | braucht Cloudflare-API-Zugang; `backend_net` ist `internal: true` |
|
||||
| `mail-archiver` | `frontend_net` + `backend_net` | braucht Internetzugang für IMAP-Abruf (GMX, Gmail) und DB-Zugang |
|
||||
| `traefik/dynamic/*` | manueller Host-Sync trotz GitOps | File-Provider bleibt bewusst fuer `middlewares.yml`, `tls.yml` und `dashboards.yml`; Komodo deployed diese Dateien nicht automatisch |
|
||||
@@ -459,6 +459,29 @@ Damit ist sofort klar:
|
||||
|
||||
## 13. Betriebserfahrungen und Entscheidungs-Log
|
||||
|
||||
### 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).
|
||||
|
||||
Reclaim:
|
||||
|
||||
- Operator-Claim-Token via `https://www.plex.tv/claim` als `Xeridos` erzeugt.
|
||||
- Plex-Container per `PLEX_CLAIM=claim-... docker compose up -d --force-recreate plex` am Host-Pfad `/mnt/user/services/stacks/plex/host-services/plex` neu erstellt. Token wurde **nur** als Shell-Inline-ENV mitgegeben, **nicht** in eine `.env`-Datei, **nicht** in die Compose, **nicht** in die Komodo-Stack-ENV geschrieben.
|
||||
- Nach Erfolg: zweiter `docker compose up -d --force-recreate plex` ohne `PLEX_CLAIM`, damit der verbrauchte Token nicht im `docker inspect`-ENV-Snapshot persistiert.
|
||||
- Bash-History defensiv geleert.
|
||||
|
||||
Endstand:
|
||||
|
||||
- `PlexOnlineUsername="Xeridos"`, `PlexOnlineMail="michideheld@gmx.de"`, `PlexOnlineHome="1"`.
|
||||
- Bibliotheken neu angelegt via Plex-Web → Verwalte Mediatheken → `/data/movies`, `/data/Heimatfilme` etc.
|
||||
- `PublishServerOnPlexOnlineKey="0"` (Remote Access deaktiviert), Plex-Relay aus → Plex bleibt strikt LAN/Tailscale-only, konsistent zum Tailscale-First-Operator-Modell.
|
||||
|
||||
Konsequenzen fuer Doku/Betrieb:
|
||||
|
||||
- Plex-Home-Familien-Profil ("Familie") muss bei Bedarf neu eingeladen werden; war ohnehin nicht aktiv genutzt.
|
||||
- Watch-State aus der Zeit vor dem 18.05. ist nicht recoverbar; Filme/Serien laufen bei Wiederaufruf bei 00:00 los.
|
||||
- `host-services/plex/docker-compose.yml` enthaelt weiter `PLEX_CLAIM: ${PLEX_CLAIM:-}`, damit ein zukuenftiger Reclaim ohne Repo-Aenderung moeglich ist.
|
||||
|
||||
### Traefik — Wechsel zu reinen Docker-Labels (2026-03-28)
|
||||
Die statischen File-Provider-Konfigurationen in `/mnt/user/appdata/traefik/dynamic/` wurden vollständig bereinigt:
|
||||
- **Gelöscht:** `immich.yml`, `gitea.yml`, `mealie.yml`, `scrutiny.yml`, `vaultwarden.yml.bak`
|
||||
@@ -571,7 +594,7 @@ Mutable Tags wie `latest`, `stable`, `release` oder reine Major-Tags wurden auf
|
||||
### Authelia ohne Redis-Session-Backend (2026-05-04)
|
||||
- Authelia nutzt PostgreSQL fuer persistente Storage-Daten, aber bewusst kein Redis-Session-Backend.
|
||||
- Das haelt den Tier-1-Auth-Pfad einfacher; nach einem Authelia-Restart muessen aktive Sessions neu aufgebaut werden.
|
||||
- `infra/redis` bleibt shared Cache fuer Dienste wie Paperless, ist aber keine Authelia-Abhaengigkeit.
|
||||
- `infra/redis` ist historisch als "shared Cache" angelegt, wird aber faktisch nur von Paperless als App-Cache genutzt. Immich, Nextcloud und Mealie betreiben jeweils eigene Redis-Instanzen in ihren App-internen Netzen; Authelia laeuft bewusst ohne Redis. Eine spaetere Konsolidierung in `apps/paperless/` (analog zu Mealie/Immich/Nextcloud) bleibt fachlich denkbar, ist aber kein priorisierter Schritt.
|
||||
|
||||
### ddns-updater — Netz-Ausnahme
|
||||
Bleibt bewusst in `frontend_net` statt `backend_net`, weil `backend_net` `internal: true` ist und ddns-updater die Cloudflare-API erreichen muss.
|
||||
|
||||
@@ -26,6 +26,12 @@ services:
|
||||
- "222:22"
|
||||
networks:
|
||||
- frontend_net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:3000/api/healthz || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 60s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=frontend_net"
|
||||
|
||||
@@ -53,7 +53,7 @@ homelab-infra/
|
||||
| Architektur, Netze, Ausnahmen | `HOMELAB_ARCHITECTURE_MASTER_V2.md` |
|
||||
| GitOps-Workflow, Drift | `docs/WORKFLOW.md`, `docs/GITOPS_DRIFT_RUNBOOK.md` |
|
||||
| Backup-Scope, Restore-Wege, Tier-Modell | `docs/RESTORE_MATRIX.md`, `docs/DISASTER_RECOVERY.md`, `ops/borg-ui/BACKUP_SCOPE.md` |
|
||||
| Storage / Cache-Policy / FS / Posture | `docs/STORAGE_LAYOUT.draft.md` |
|
||||
| Storage / Cache-Policy / FS / Posture | `docs/STORAGE_LAYOUT.md` (zum Audit-Zeitpunkt noch `docs/STORAGE_LAYOUT.draft.md`) |
|
||||
| Secret-Inventur | `docs/SECRETS_MAP.md` |
|
||||
| Alert-Pfade | `docs/ALERTING_MAP.md` |
|
||||
|
||||
@@ -61,7 +61,7 @@ homelab-infra/
|
||||
|
||||
| Luecke | Warum es ein Audit-Loch ist |
|
||||
|---|---|
|
||||
| `docs/STORAGE_LAYOUT.draft.md` ist `Draft 1.3`, nicht `Active` | Mehrere Hard Rules (12 Constitution) gelten formal noch nicht. Hard Rule 11 (kein Stack ohne Restore-Pfad in RESTORE_MATRIX) wird heute schon eingehalten — also nur Formal-Luecke. |
|
||||
| `docs/STORAGE_LAYOUT.draft.md` ist `Draft 1.3`, nicht `Active` | Stand zum Audit-Zeitpunkt 2026-05-25: mehrere Hard Rules (12 Constitution) galten formal noch nicht. Hard Rule 11 (kein Stack ohne Restore-Pfad in RESTORE_MATRIX) wurde schon eingehalten — also nur Formal-Luecke. Folgearbeit: als `docs/STORAGE_LAYOUT.md` Active heben. |
|
||||
| `docs/SERVICES_RECOVERY.md` ist als verbindlich angekuendigt (STORAGE_LAYOUT 4), aber nicht im Repo | Konkrete Mirror-Mechanik fuer `services/gitea/git/repositories/` ≤ 6 h ist nirgends spezifiziert. |
|
||||
| Hardware-Inventar: kein zentrales Dokument | Keine Stelle im Repo nennt CPU-Modell, RAM-Groesse, NIC-Speed, Mainboard, Parity-Disk-Groessen — nur "Samsung 970 EVO Plus 2 TB" steht in STORAGE_LAYOUT 3. |
|
||||
| USV: keine Erwaehnung | Keine Datei nennt eine USV. Unklar, ob vorhanden. |
|
||||
@@ -72,7 +72,7 @@ homelab-infra/
|
||||
- `HOMELAB_ARCHITECTURE_MASTER_V2.md` — komplett
|
||||
- `docs/WORKFLOW.md`, `docs/REPO_MAP.md`, `docs/SERVICE_CATALOG.md` — komplett
|
||||
- `docs/DISASTER_RECOVERY.md`, `docs/RESTORE_MATRIX.md`, `docs/SECRETS_MAP.md` — komplett
|
||||
- `docs/STORAGE_LAYOUT.draft.md`, `docs/STRATEGISCHE_BEWERTUNG_2026-05-23.md` — komplett
|
||||
- `docs/STORAGE_LAYOUT.md` (zum Audit-Zeitpunkt `docs/STORAGE_LAYOUT.draft.md`), `docs/STRATEGISCHE_BEWERTUNG_2026-05-23.md` — komplett
|
||||
- `docs/AUDIT_2026-05-23_LIVE.md`, `docs/AUDIT_2026-05-23_FINAL.md`
|
||||
- `ops/policy-checks/last-report.md`
|
||||
- `monitoring/docker-compose.yml`, `monitoring/prometheus/alerts.yml`
|
||||
|
||||
@@ -2,29 +2,35 @@
|
||||
|
||||
Quelle: `docs/AUDIT_2026-05-25.md`
|
||||
|
||||
Status: Arbeitsliste fuer die Umsetzung. Authelia-2FA/OIDC bleibt bewusst spaet, weil die Ziel-Policy noch nicht final entschieden ist.
|
||||
Status: Arbeitsliste fuer die Umsetzung. Authelia-2FA/OIDC, CrowdSec und Nextcloud-2FA-Haertung bleiben ganz hinten und werden bewusst nicht in diesem Audit-Zyklus angefasst.
|
||||
|
||||
## Leitplanken
|
||||
|
||||
- Keine Authelia-2FA-ACL-Aenderungen in den ersten Sprints.
|
||||
- Authelia-2FA-ACL, Authelia-OIDC und CrowdSec werden in diesem Audit-Zyklus **nicht** umgesetzt (Operator-Vorgabe 2026-05-26).
|
||||
- Keine Live-riskanten Bind-/Port-Aenderungen ohne vorher erfasste Host-Werte, insbesondere Tailscale-IP.
|
||||
- Erst Inventar und Baseline, dann Aenderungen.
|
||||
- Hermes-Agent ist geparkt, nicht entfernt; Review-Deadline 2026-07-25.
|
||||
- USV-Anschaffung ist verschoben; Power-Loss-Risiko ist als Operator-Entscheidung 2026-05-26 bewusst akzeptiert.
|
||||
- Borg-Passphrase ist offline gesichert (bestaetigt 2026-05-26).
|
||||
- H:/ ist evaluiert als zweite lokale Nearline-Kopie, nicht als Offsite-Ersatz (siehe `docs/CAPACITY_AND_LIFECYCLE.md`).
|
||||
- Familien-Einladung ist fuer das Wochenende **nach** Erreichen des finalen Stands geplant; Family-Onboarding muss familienverstaendlich werden, nicht technisch.
|
||||
- Jede produktive Aenderung bekommt Validierung und Rollback-Hinweis.
|
||||
|
||||
## Naechster Startpunkt 2026-05-26
|
||||
|
||||
Kontext bewusst gesichert, bevor weitere Live-Aenderungen passieren:
|
||||
|
||||
1. USV-Entscheidung treffen: aktuell ist keine funktionierende USV-Abschaltung nachgewiesen.
|
||||
2. Gitea-Bundle-/Mirror-Mechanik und Borg-Passphrase-Offsite-Sicherung entscheiden.
|
||||
3. Authelia 2FA/OIDC weiterhin nicht anfassen; das bleibt bewusst der letzte Block.
|
||||
1. Host-Schedule fuer Gitea-Bundles und Restore-Freshness pruefen.
|
||||
2. FRITZ!Box-Portfreigaben (UI) gegen Repo-Soll abgleichen (`443/tcp` + `222/tcp`).
|
||||
3. H:/ Pull-Workflow festlegen.
|
||||
4. Family-Onboarding-Doku familienverstaendlich umarbeiten, vor der Wochenend-Einladung.
|
||||
5. Authelia 2FA/OIDC und CrowdSec weiterhin nicht anfassen; bleibt bewusst der letzte Block.
|
||||
|
||||
## Sprint 0 - Inventar und Baseline
|
||||
|
||||
| Status | Aufgabe | Ergebnis |
|
||||
|---|---|---|
|
||||
| erledigt | Hardware-Inventar ausfuellen | CPU, RAM, Mainboard, BIOS, NIC, Controller, Disks, SMART und Capacity-Baseline erfasst; USV ist als nicht validiert dokumentiert |
|
||||
| in Arbeit | Netzwerk-Inventar ausfuellen | Host-IP, Gateway, Tailscale-IP und AdGuard-Bind erfasst; Router-/VLAN-Details offen |
|
||||
| erledigt | Netzwerk-Inventar ausfuellen | Host-IP, Gateway, Tailscale-IP, AdGuard-Bind und FRITZ!Box-Baseline (7590, FRITZ!OS 8.21, Telekom DSL 87/36, 36 Geraete, Gast-WLAN inaktiv, Ausfallschutz inaktiv, 2 Portfreigaben aktiv) erfasst; IPv6 und FRITZ!OS-Update bleiben Operator-Folgeaufgaben |
|
||||
| erledigt (Baseline) | Externe Abhaengigkeiten dokumentieren | `docs/EXTERNAL_DEPENDENCIES.md` enthaelt bekannte Provider, Kritikalitaet, Ausfallplaene; Account-Recovery-Codes/Zahlungswege bleiben Off-Repo-Operatorcheck |
|
||||
| erledigt (Baseline) | Services-Recovery-Pfade beschreiben | `docs/SERVICES_RECOVERY.md` enthaelt Gitea-/Komodo-/Secrets-Sonderpfade; Gitea-Bundle-/Mirror-Mechanik bleibt als Umsetzungsentscheidung offen |
|
||||
| erledigt | Baseline-Tag setzen | `audit-2026-05-25-baseline` ist lokal und remote vorhanden |
|
||||
@@ -34,7 +40,7 @@ Kontext bewusst gesichert, bevor weitere Live-Aenderungen passieren:
|
||||
|
||||
| Status | Aufgabe | Ergebnis |
|
||||
|---|---|---|
|
||||
| offen | Borg-Passphrase analog sichern | Passphrase ist ohne Host/Vaultwarden wiederherstellbar |
|
||||
| erledigt | Borg-Passphrase analog sichern | Operator bestaetigt am 2026-05-26: Passphrase ist offline gesichert und ohne Host/Vaultwarden wiederherstellbar |
|
||||
| erledigt (repo) | AdGuard Admin-Bind vorbereiten | Tailscale-IP `100.80.98.33` erfasst, Compose-Soll geaendert |
|
||||
| erledigt | AdGuard Admin-Port auf Tailscale-IP binden | Live validiert: `ss -ltnp` zeigt `100.80.98.33:8082`, DNS auf Port 53 funktioniert, LAN-Zugriff auf `192.168.178.58:8082` schlaegt fehl |
|
||||
| erledigt | Alte Monitoring-Verzeichnisse entfernen | `ops/grafana-influxdb/` und `ops/loki/` sind aus dem aktiven Repo entfernt; Rollback erfolgt ueber Git-Historie |
|
||||
@@ -45,37 +51,73 @@ Kontext bewusst gesichert, bevor weitere Live-Aenderungen passieren:
|
||||
|
||||
| Status | Aufgabe | Ergebnis |
|
||||
|---|---|---|
|
||||
| offen | `docs/STORAGE_LAYOUT.draft.md` finalisieren | Datei wird als `docs/STORAGE_LAYOUT.md` Active gefuehrt |
|
||||
| offen | Disk- und Share-TBDs eintragen | Modelle, Groessen, Seriennummern, Filesysteme und Cache-Settings sind dokumentiert |
|
||||
| offen | Gitea-Repo-Mirror-Mechanik definieren | Mirror fuer `/mnt/user/services/gitea/git/repositories/` mit Frequenz <= 6 h ist spezifiziert |
|
||||
| offen | Komodo-Bootstrap-Pfad beschreiben | Kaltstart ohne laufendes Komodo ist dokumentiert |
|
||||
| offen | Immich-Restore-Test planen | Testumfang, Datenpfade und Smoke-Test-Kriterium stehen fest |
|
||||
| erledigt | `docs/STORAGE_LAYOUT.draft.md` finalisieren | Datei als `docs/STORAGE_LAYOUT.md` Active v1.4 gefuehrt; Draft-Blocker entfernt |
|
||||
| erledigt (Baseline) | Disk- und Share-TBDs eintragen | Disk-Modelle, Seriennummern, Groessen, Filesysteme und Share-Cache-Settings aus `docs/HARDWARE_INVENTORY.md` und Host-Readout 2026-05-27 uebernommen; Retention-/Schwellen-Kalibrierung bleibt Folgeaufgabe |
|
||||
| erledigt | Gitea-Repo-Mirror-Mechanik definieren | `ops/borg-ui/scripts/gitea-bundle-mirror.sh` erzeugt verifizierte Bundles unter `/mnt/user/backups/git-bundles/gitea`; Host-Erstlauf 2026-05-26: 4 Bundles, Checksums OK, `homelab-infra.bundle` klonbar und `git fsck` sauber. Schedule live seit 2026-05-27 ueber User-Script `gitea-bundle-mirror-6h` (`10 */6 * * *`); Bundles werden mit `chmod 644` geschrieben damit der Nearline-Pull sie greift. |
|
||||
| erledigt (Doku + Skript + Erstlauf) | Komodo-Bootstrap-Pfad beschreiben | `docs/SERVICES_RECOVERY.md` enthaelt linearen Bootstrap in Stufen A-F mit Recovery-Anker `ops/komodo/docker-compose.yml`, expliziter Abgrenzung zum Self-Stack, Secret-Reihenfolge und Validierungs-Kommandos; `docs/DISASTER_RECOVERY.md` Stufe 3 verlinkt auf Bootstrap-Pfad. Trockenlauf-Skript unter `ops/restore-tests/komodo-bootstrap-*` seit 2026-05-29 vorhanden, Erstlauf 2026-05-30 erfolgreich (siehe Sprint 8 Eintrag). |
|
||||
| erledigt | Immich-Restore-Test planen | Testumfang, Datenpfade und Smoke-Test-Kriterium sind in `docs/IMMICH_RESTORE_TEST.md`, `ops/restore-tests/immich-plan.md` und `ops/restore-tests/immich-runbook.md` festgehalten; erster Host-Lauf am 2026-05-27 erfolgreich |
|
||||
|
||||
## Sprint 3 - Restore und Monitoring
|
||||
|
||||
| Status | Aufgabe | Ergebnis |
|
||||
|---|---|---|
|
||||
| offen | Immich-Restore-Test implementieren | Restore-Report landet unter `/mnt/user/backups/restore-reports/` |
|
||||
| offen | Borg-Stale-Alert bauen | Alarm feuert, wenn Borg-Archiv zu alt ist |
|
||||
| offen | TLS-Cert-Expiry-Alert bauen | Alarm feuert bei Restlaufzeit unter Schwellwert |
|
||||
| offen | Container-Down-Alert bauen | Unerwartet fehlende Container werden sichtbar |
|
||||
| offen | Family-View Dashboard definieren | Uptime, Backup-Frische, Cert-Tage, Disk-Fuellung auf einer Seite |
|
||||
| erledigt | Immich-Restore-Test implementieren | Echter Host-Lauf 2026-05-27 erfolgreich: Borg-Archiv `Tägliche-Sicherung-2026-05-27T04:30:06.778`, `immich.dump` extrahiert, isolierter pgvecto-rs-Postgres importiert, Immich-Server ohne ML gestartet, HTTP `200`, Login-Marker ok, `11977` Assets und `1` User im Test-DB-Check; Report `/mnt/user/backups/restore-reports/immich-2026-05-27.md` |
|
||||
| erledigt | Borg-Stale-Alert bauen | Cron `*/5 * * * *` (`export-prometheus-textfile-5min` User-Script) schreibt `homelab.prom`; node-exporter scraped, Prometheus laedt Regel `HomelabBorgBackupStale` aktiv. Live 2026-05-27 20:33 reloaded; `lastConfigTime: 2026-05-27T18:33:06Z`; Smoke-Query `(time() - homelab_borg_last_completed_timestamp_seconds)/3600 = 16h`. Borg-Job-Warning ist aktuell `pending` (Letzter Lauf `completed_with_warnings`). |
|
||||
| erledigt | TLS-Cert-Expiry-Alert bauen | Regeln `HomelabCertificateExpiresSoon` (21d) und `HomelabCertificateExpiresCritical` (7d) sind in `alerts.yml` aktiv und nach Prometheus-Reload geladen. Smoke `inactive` (keine Cert <21d). |
|
||||
| erledigt | Container-Down-Alert bauen | `HomelabCriticalContainerDown` aktiv; Live-Smoke 2026-05-27 `sum(homelab_critical_container_running) = 30`, alle aktuellen Critical-Container `1`. Aktualisierung alle 5 Min ueber Cron. |
|
||||
| erledigt (Spezifikation) | Family-View Dashboard definieren | `docs/FAMILY_VIEW_DASHBOARD.md` enthaelt Layout, PromQL-Queries, Thresholds und Build-Reihenfolge fuer ein `homelab-family-view`-Dashboard. JSON wird bewusst erst angelegt, sobald Borg-Stale-/Cert-Expiry-/Container-Down-Metriken stabil live sind und ein manueller Build im Grafana-UI das Layout bestaetigt hat. |
|
||||
|
||||
## Sprint 4 - Familien- und Betriebsdoku
|
||||
|
||||
| Status | Aufgabe | Ergebnis |
|
||||
|---|---|---|
|
||||
| offen | Familien-Onboarding schreiben | Nextcloud, Immich, Vaultwarden, 2FA-Verlust, Ausfallverhalten kurz erklaert |
|
||||
| erledigt (Baseline) | Capacity-/Lifecycle-Review erstellen | Cache 6 %, Array/User-Shares 33 %, lokale Backups 2.2G; externe Backup-/Cold-Storage-Groessen bleiben offen |
|
||||
| offen | USV-Test oder USV-Entscheidung | Power-Loss-Verhalten ist bekannt und dokumentiert |
|
||||
| erledigt (final vor Einladung) | Familien-Onboarding schreiben | `docs/FAMILY_ONBOARDING.md` ist final redigiert: familienverstaendliche Sprache, App-eigene 2FA statt SSO-Versprechen, neuer "Bewusst nicht versprochen"-Block (kein Einheits-Login, kein 24/7-SLA, kein Hotline-Support, keine Datenweitergabe), konkrete Was-tun-Anleitungen. Einladungstermin bleibt Operator-Aufgabe. |
|
||||
| erledigt (Baseline) | Capacity-/Lifecycle-Review erstellen | Cache 6 %, Array/User-Shares 33 %, lokale Backups 2.2G; H:/-Nearline-Bewertung ergaenzt; externe Cold-Storage-Groessen bleiben offen |
|
||||
| erledigt | USV-Test oder USV-Entscheidung | Operator-Entscheidung 2026-05-26: aktuell keine USV-Anschaffung; Power-Loss-Risiko wird bewusst akzeptiert und dokumentiert |
|
||||
| erledigt (Baseline) | H:/ als zusaetzliches lokales Backupziel bewerten | Als zweite Nearline-Kopie und Freeze-Sicherung sinnvoll; kein Offsite-Ersatz, kein CIFS-Hard-Mount am Unraid; Pull-Modell vom Windows-PC ist der getestete Weg (siehe `docs/CAPACITY_AND_LIFECYCLE.md`) |
|
||||
| erledigt (Pull live, Scheduled Task offen) | H:/ Groesse und Pull-Schedule festschreiben | Groesse erfasst: 8.0T NTFS, 3.91T belegt, 4.10T frei, `Healthy`. Erster echter Pull 2026-05-27 20:45 erfolgreich: 19 Borg-Dumps + 10 Gitea-Bundle-Files unter `H:\kallilab-nearline-backups`. `unraid-flash-config.*` bewusst ausserhalb Scope (`/XF`-Exclude, Restore aus Hetzner-Borg). Permission-Fixes in `pre-backup-dumps.sh` (alle Dumps 0644 ausser Flash-Config), `gitea-bundle-mirror.sh` (Bundles 0644), `export-prometheus-textfile.sh` (Metric-File 0644) und `pull-critical-backups.ps1` (Report-Bug + ExcludeFiles). Windows Scheduled Task taeglich 05:30 bleibt offen (Operator-Bestaetigung). |
|
||||
| erledigt 2026-05-28 | FRITZ!Box-Portfreigaben gegen Repo-Soll abgleichen | Bereinigt: `80/tcp` entfernt (Mobilfunk-validiert: HTTP timeout, HTTPS weiter erreichbar). `222/tcp` bleibt bewusst nicht eingerichtet (Tailscale-only-Linie). UPnP-Selbstfreigabe-Recht fuer `PC-192-168-178-71` (VONETS-Bridge, vermutlich SolarEdge-Wechselrichter) deaktiviert. Aktiver Endstand: ausschliesslich `443/tcp -> 192.168.178.58`. Details in `docs/FRITZBOX_PORT_CORRECTION_PLAN.md`. |
|
||||
|
||||
## Sprint 5 - Auth und Frontdoor, bewusst zuletzt
|
||||
|
||||
In diesem Audit-Zyklus werden diese Punkte **nicht** umgesetzt. Sie sind dokumentiert, damit sie bei einer kuenftigen Policy-Entscheidung sofort priorisiert werden koennen.
|
||||
|
||||
| Status | Aufgabe | Begruendung der Parkung |
|
||||
|---|---|---|
|
||||
| geparkt | Authelia 2FA fuer Operator-UIs erweitern (F-04) | Operator-Vorgabe 2026-05-26: keine Auth-Aenderungen in diesem Zyklus |
|
||||
| geparkt | Authelia OIDC fuer Apps pruefen (F-13) | Operator-Vorgabe 2026-05-26: keine Auth-Aenderungen in diesem Zyklus |
|
||||
| geparkt | CrowdSec vor Traefik pruefen (F-14) | Operator-Vorgabe 2026-05-26: erst nach finaler Auth-Policy |
|
||||
| geparkt | Nextcloud-2FA-/Brute-Force-Haertung dokumentieren (F-18) | beruehrt Auth-Policy fuer Familien-Konten; gemeinsam mit OIDC-Entscheidung |
|
||||
|
||||
## Sprint 6 - Geparkte Apps und Folgeentscheidungen
|
||||
|
||||
| Status | Aufgabe | Naechster Pruefschritt |
|
||||
|---|---|---|
|
||||
| geparkt | Hermes-Agent (F-06) — Operator-Entscheidung produktiv vs. entfernen | Review-Deadline **2026-07-25**; bis dahin bleibt der NAS-Stack deaktiviert, das Repo-Verzeichnis erhalten, Dashboard-Domain und ACL-Eintrag unveraendert |
|
||||
| erledigt 2026-05-28 | paperless-gpt / BentoPDF Nutzungsentscheidung | Operator behaelt beide. **paperless-gpt** bleibt bis Paperless-NGX 3.0 (erwartete native KI-Features); danach neu bewerten. **BentoPDF** bleibt als situatives Tool (Resource-Footprint ~4 MB). Beide ohne aktive Traefik-Zugriffe in der letzten Woche, aber bewusste Behalten-Entscheidung mit Begruendungs-Anker im SERVICE_CATALOG. |
|
||||
| erledigt 2026-05-28 | Plex Remote Access in UI deaktivieren falls nur LAN/Tailscale (F-17) | Beim Versuch entdeckt: Server war seit 18.05. unclaimed und Bibliotheken leer. Reclaim als `Xeridos` via inline `PLEX_CLAIM`-Token, danach Bibliotheken (`/data/movies` 1.4 TB, `/data/Heimatfilme` 300 GB) neu angelegt und Remote Access deaktiviert (`PublishServerOnPlexOnlineKey=0`, Plex-Relay aus). Details in `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13. |
|
||||
| erledigt | `infra/redis` Doku-Etikett korrigieren (F-16) | SERVICE_CATALOG, REPO_MAP, MASTER (Sektion 13) und DISASTER_RECOVERY Bootstrap-Stufe 2 auf "primaer Paperless-Redis" praezisiert; keine Compose-Aenderung |
|
||||
| erledigt | Paperless-DBPass DR-Restore-Reihenfolge in DR-Doc (F-20) | DISASTER_RECOVERY 6.2.1 (Restore-Quellen fuer Stack-ENV-Werte) ergaenzt; SECRETS_MAP um Abschnitt "Stack-ENV-only Secrets - Restore-Wege" mit Reihenfolge Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz erweitert; Paperless, Immich, Mail-Archiver, Speedtest, Komodo, Hermes und Glance je mit Restore-Quelle dokumentiert |
|
||||
|
||||
## Sprint 8 - Reife der Stack-Hygiene (2026-05-29)
|
||||
|
||||
| Status | Aufgabe | Ergebnis |
|
||||
|---|---|---|
|
||||
| geparkt | Authelia 2FA fuer Operator-UIs erweitern | Erst nach finaler Policy-Entscheidung |
|
||||
| geparkt | Authelia OIDC fuer Apps pruefen | Erst nach Familien-/Client-Auswirkungsanalyse |
|
||||
| geparkt | CrowdSec vor Traefik pruefen | Nach stabiler Auth-/Monitoring-Basis |
|
||||
| erledigt 2026-05-29 | Healthchecks fuer Tier-1 (F-15) | postgresql17 (`pg_isready`), Redis (`redis-cli ping` mit Auth), Vaultwarden (`curl /alive`), Gitea (`wget /api/healthz`), Traefik (`traefik healthcheck --ping`, `--ping=true` in CLI), Authelia (`wget /api/health`, weil v4.39 `helper health-check` entfernt hat); komodo-mongo war bereits gepinnt healthy. Live-Smoke: alle 6 healthy nach Recreate. Postgres- und Gitea-Stack-Workspace waren Komodo-seitig zurueckgeblieben (124 bzw. 52 commits behind); manuell per `cp` + `docker compose up -d` synchronisiert. |
|
||||
| erledigt 2026-05-29 | Monitoring-Stack Digest-Pinning (F-07) | 9 Container in `monitoring/docker-compose.yml` per Tag@sha256 gepinnt: prometheus, alertmanager, alertmanager-ntfy-bridge (python:3.13-alpine), blackbox-exporter, loki, promtail, grafana, node-exporter, cadvisor. Digests aus dem aktuell laufenden Container ausgelesen, damit der Pin den Live-Stand reflektiert. influxdb3-core war bereits gepinnt. |
|
||||
| erledigt 2026-05-29 (Skript) / 2026-05-30 (Erstlauf) | Komodo-Bootstrap-Trockenlauf-Skript (F-09 Rest) | `ops/restore-tests/komodo-bootstrap-{compose.test.yml,test.sh,plan.md,runbook.md}` analog zum Immich-Restore-Test angelegt. Test-Compose nutzt dieselben Image-Digests wie Produktion, isoliert unter Project `restoretest-komodo`, Test-Periphery ohne docker.sock-Mount, Test-Port nur `127.0.0.1:19120`. Wegwerf-Secrets im Compose. **Erstlauf 2026-05-30 erfolgreich**: Result `SUCCESS`, alle 5 Checks gruen — compose config valid, Test-Mongo healthy (6s), Mongo authenticated ping ok, Komodo Core HTTP `200`, Test-Periphery container state `running`. Report unter `/mnt/user/backups/restore-reports/komodo-bootstrap-2026-05-30.md`. Produktive Komodo-Container, Mongo-Datadir und Secrets nicht beruehrt. Damit ist `ops/komodo/docker-compose.yml` als Recovery-Anker belegt tauglich (nicht mehr nur angenommen). |
|
||||
| erledigt 2026-05-29 | Renovate-Bot gegen Gitea (F-12) | Live: Service-Account `renovate` (uid 2, kein Admin) angelegt, Collaborator Write auf `Micha/homelab-infra`, PAT in `/mnt/user/appdata/secrets/renovate_token.txt` (chmod 600). Cron `renovate-six-hourly` (`20 */6 * * *`) live in `/etc/cron.d/root`. Erstlauf 2026-05-29 erfolgreich: 5 PRs (mongo digest+minor, postgres digest+minor, minor-and-patch-updates gruppiert), 1 Dependency-Dashboard-Issue, 8 Branches. Komodo-Major durch packageRule deaktiviert wie erwartet. Architektur-Detail: Repo-Config in `renovate.json`, Bot-Config in `ops/renovate/bot-config.js` (Renovate liest die im Repo nur als Repo-Config, Bot-Settings dort triggern "forbidden/disabled"). |
|
||||
| erledigt 2026-05-30 | Authelia Repo<->Host Drift-Check (F-10) | `services/authelia-diff.sh` vergleicht die `access_control:`-Sektion zwischen Repo-Baseline und Host-Datei (Default; per env `AUTHELIA_DIFF_SECTIONS` erweiterbar). OIDC-Clients/Identity-Provider und Secret-Werte bleiben bewusst aussen vor. Exit-Codes: 0 = ok, 1 = Drift, 2 = Datei fehlt, 3 = Sektion fehlt, 4 = Werkzeug fehlt. Posture-Check ruft das Skript als Check `authelia_config_drift` auf (`SKIP_AUTHELIA_DRIFT=1` skippt, `AUTHELIA_DIFF_SCRIPT` ueberschreibt den Pfad); Drift wird als Warning gemeldet, nicht Critical. Smoke-Test lokal: identische Files -> rc=0, ACL-Drift im Domain-Eintrag -> rc=1 mit unified diff. WORKFLOW.md hat jetzt eine eigene Pflicht-Sektion "Ausnahme: Authelia configuration.yml" analog zur Traefik-Dynamic-Sektion. Pflicht-Setup auf dem Host: Repo-Spiegel unter `/mnt/user/services/homelab-infra/`. |
|
||||
|
||||
## Sprint 7 - Off-site und 3-2-1 (offen)
|
||||
|
||||
| Status | Aufgabe | Bemerkung |
|
||||
|---|---|---|
|
||||
| erledigt 2026-05-28 (bewusst nicht umgesetzt) | Zweites echtes Off-site (F-03) | Operator-Entscheidung 2026-05-28: kein zweites Off-site. 3-2-1 ist mit Live + lokalem Borg + Hetzner + H:/-Nearline erfuellt; ein zweites Off-site wuerde nur den Fall "Hetzner-Account verloren" zusaetzlich abdecken, Aufwand/Kosten unverhaeltnismaessig fuer Familien-Homelab. Statt dessen drei Hetzner-Haertungen als Folge-TODOs (siehe `docs/OFFSITE_BACKUP_OPTIONS.md` Beschluss-Block). Review-Trigger sind dort definiert. |
|
||||
| offen (Operator-Aufgabe) | Hetzner-Account-Hygiene ohne 2FA | Starkes, einzigartiges Passwort in Vaultwarden + Backup-Zahlungsweg + Login-Benachrichtigungen per E-Mail. 2FA bewusst nicht (analog USV: Risiko bewusst akzeptiert, Aufwand ueberwiegt Risiko-Reduktion fuer Familien-Homelab). |
|
||||
| offen (Folge-Sprint) | Borg `--append-only` auf Hetzner setzen | Aktuell laeuft Repo `appdata-critical` im Mode `full`, `custom_flags` leer. Setup server-seitig in Hetzner `~/.ssh/authorized_keys` mit `command="borg serve --append-only"`. Schuetzt gegen Ransomware, die client-seitig Borg-Credentials abgreifen koennte. |
|
||||
| erledigt 2026-05-28 | H:/-Pull als Windows Scheduled Task | Task `KalliLab H Drive Nearline Pull` registriert: taeglich 05:30, `RunLevel Limited`, `AllowStartIfOnBatteries`, `StartWhenAvailable`, `ExecutionTimeLimit 2h`. Naechster Lauf 2026-05-29 05:30. Erstlauf manuell 2026-05-27 20:45 erfolgreich, Task-Setup in `docs/H_DRIVE_NEARLINE_PULL.md` aktualisiert (RunLevel-Enum-Fix `LeastPrivilege` -> `Limited`). |
|
||||
| erledigt (Routine dokumentiert) | Restore-Lab-Drill quartalsweise dokumentieren | `docs/RESTORE_DRILL_ROUTINE.md` definiert Drei-Stufen-Modell (Freshness woechentlich / Mini-Restore monatlich-bimonatlich / DR-Sanity quartalsweise), Quartals-Belegung Q1-Q4 mit Dienst-Rotation, Immich 2026-05-27 als bestaetigter Erstlauf gefuehrt, 10-Punkte-Sanity-Check, kein Host-Schedule angelegt. `ops/restore-tests/schedule.md` verweist jetzt auf Drill-Routine. |
|
||||
|
||||
## Offene Host-Werte
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Capacity and Lifecycle - KalliLab CORE
|
||||
|
||||
Status: Initiale Capacity-Baseline 2026-05-26; externe Backup-/Cold-Storage-Groessen offen.
|
||||
Status: Initiale Capacity-Baseline 2026-05-26; H:/-Nearline-Bewertung 2026-05-26; externe Cold-Storage-Groessen offen.
|
||||
|
||||
## Zweck
|
||||
|
||||
@@ -47,7 +47,52 @@ du -sh /mnt/user/documents /mnt/user/photos /mnt/user/media /mnt/user/backups 2>
|
||||
| RAM >90 % ueber 10 Minuten regelmaessig | RAM-Ausbau oder Service-Limits pruefen |
|
||||
| Borg-Laufzeit deutlich steigend | Scope, Netzwerk und Ziel pruefen |
|
||||
| SMART-Warnung | Ersatz planen, Restore-/Backup-Frische pruefen |
|
||||
| Keine USV-Abschaltung | USV anschaffen/anschliessen oder Power-Loss-Risiko bewusst akzeptieren |
|
||||
| Keine USV-Abschaltung | Risiko ist per Operator-Entscheidung 2026-05-26 bewusst akzeptiert; bei Stromausfaellen/Datenkorruption neu bewerten |
|
||||
|
||||
## H:/ als zusaetzliches lokales Backup-Ziel
|
||||
|
||||
Operator-Befund 2026-05-26: das Windows-Laufwerk `H:/` laeuft dauerhaft am Arbeits-PC und wurde bereits als `H:-Freeze-Backup` waehrend der Disk1 NTFS->XFS Phase 2 erfolgreich genutzt (siehe `docs/MIGRATION_LOG.md`).
|
||||
|
||||
### Eigenschaften von H:/
|
||||
|
||||
| Eigenschaft | Wert | Konsequenz |
|
||||
|---|---|---|
|
||||
| Anbindung | Windows-PC, dauerhaft verbunden | kein Air-Gap, kein Hardware-Trenn-Schutz |
|
||||
| Standort | gleicher physischer Standort wie Unraid-Host | kein Off-site, kein Schutz gegen Brand/Diebstahl/Wasser |
|
||||
| Schreibzugriff | dauerhaft moeglich | Ransomware oder Operator-Fehler koennen Original und Kopie gleichzeitig treffen |
|
||||
| Filesystem (Windows) | typischerweise NTFS | fuer Borg-Ziel ueber CIFS/SMB ungeeignet als Hard-Mount auf Unraid (STORAGE_LAYOUT §12.6) |
|
||||
| Reproduzierbar nutzbar | ja, bewaehrt durch Disk1-Phase-2-Freeze-Backup | Pull-Modell vom Windows-PC ist der getestete Weg |
|
||||
|
||||
### Bewertung
|
||||
|
||||
H:/ ist **keine echte Offsite-/Airgap-Kopie und kein Ersatz fuer Hetzner**. Es ist aber sinnvoll als:
|
||||
|
||||
- **Zweite lokale Nearline-Kopie** fuer kritische Restore-Quellen (Borg-Dumps, Repo-Bundles, Flash-Backup). Schnellster Restore-Pfad bei Hetzner-Stoerung, weil kein Off-site-Download noetig ist.
|
||||
- **Cold-Storage-Vorstufe**: wenn H:/ kuenftig periodisch (z. B. monatlich) physisch getrennt und durch eine zweite Platte ersetzt wird, wuerde sich daraus ein echtes Air-Gap-Rotationsschema ableiten lassen.
|
||||
- **Freeze-Sicherung** vor strukturellen Eingriffen (Disk-Tausch, Pool-Umzug, Format), wie bereits 2026-05-25 praktiziert.
|
||||
|
||||
### Empfohlene Nutzung
|
||||
|
||||
| Nutzung | Umsetzung | Hinweis |
|
||||
|---|---|---|
|
||||
| Pull von `/mnt/user/backups/borg/dumps/latest` auf H:/ | Windows-seitiger Scheduled Task per `robocopy` oder `rclone` von einem SMB-Read-Share | keine CIFS-Hard-Mounts auf Unraid; STORAGE_LAYOUT-Konstitution bleibt erhalten |
|
||||
| Pull der Gitea-Bundles aus `/mnt/user/backups/git-bundles/gitea` | identisch | Bundles sind klein und schnell synchronisiert |
|
||||
| Pull des Unraid-Flash-Artefakts `unraid-flash-config.tar.gz` | identisch | wie Secret behandeln, Windows-seitig nicht in geteilten Ordnern ablegen |
|
||||
|
||||
Der konkrete Pull-Pfad ist in `docs/H_DRIVE_NEARLINE_PULL.md` und `ops/h-drive-nearline/pull-critical-backups.ps1` vorbereitet. Der Windows Scheduled Task wird erst nach Operator-Sichtpruefung aktiviert.
|
||||
| **Nicht** als Ersatz fuer Hetzner-Off-site | — | 3-2-1 bleibt mit Hetzner als einzigem Off-site weiterhin unerfuellt; siehe `docs/AUDIT_2026-05-25.md` F-03 |
|
||||
| **Nicht** als zweites Borg-Repo am Unraid | — | dauerhafte CIFS-Verbindung im Borg-Lauf verletzt Hard Rule §12.6 |
|
||||
|
||||
### Kapazitaets-Eintrag
|
||||
|
||||
| Bereich | Groesse | Belegt | Schwellwert | Bewertung |
|
||||
|---|---:|---:|---:|---|
|
||||
| H:/ (Windows-Arbeitsplatz, `Externe HDD`) | 8.0T | 3.91T belegt / 4.10T frei | Review wenn > 70 % | NTFS, `Healthy`; Pull-Ziel fuer Borg-Dumps, Gitea-Bundles und Flash-Backup |
|
||||
|
||||
### Naechste Schritte
|
||||
|
||||
- Pull-Script (Operator-Aufgabe, kein Repo-Pflichtteil) etablieren; alternativ ueber Filebrowser/Nextcloud-Sync abdecken.
|
||||
- Review-Intervall: quartalsweise. Bei jeder grossen Strukturaenderung (Disk-Tausch, Pool-Umzug) Freeze-Pull manuell ausloesen.
|
||||
|
||||
## Restore-Zeitziele
|
||||
|
||||
@@ -62,4 +107,7 @@ du -sh /mnt/user/documents /mnt/user/photos /mnt/user/media /mnt/user/backups 2>
|
||||
|
||||
| Datum | Befund | Entscheidung |
|
||||
|---|---|---|
|
||||
| 2026-05-26 | Cache 6 %, Array/User-Shares 33 %, lokale Backups 2.2G; keine validierte USV-Abschaltung | Capacity gruen; naechste operative Risiken sind USV-Entscheidung und externe Backup-/Cold-Storage-Groessen |
|
||||
| 2026-05-26 | Cache 6 %, Array/User-Shares 33 %, lokale Backups 2.2G; keine validierte USV-Abschaltung | Capacity gruen; USV wird aktuell nicht angeschafft, Power-Loss-Risiko bewusst akzeptiert; externe Backup-/Cold-Storage-Groessen bleiben offen |
|
||||
| 2026-05-26 | H:/ als dauerhaft verbundenes Windows-Laufwerk evaluiert | als zweite lokale Nearline-Kopie und Freeze-Sicherung sinnvoll; nicht als Offsite-Ersatz und nicht als Borg-CIFS-Hard-Mount am Unraid; Pull-Modell vom Windows-PC bleibt der getestete Weg |
|
||||
| 2026-05-26 | H:/ Kapazitaet erfasst: 8.0T NTFS, 3.91T belegt, 4.10T frei, `Healthy` | genug Reserve fuer Nearline-Pull der kritischen Restore-Artefakte; Pull-Schedule bleibt offen |
|
||||
| 2026-05-27 | H:/ Pull-Workflow vorbereitet | SMB-Quelle `\\192.168.178.58\backups` erreichbar; PowerShell-Skript und Runbook erstellt; empfohlener Schedule taeglich 05:30, aber Task noch nicht aktiviert |
|
||||
|
||||
@@ -65,7 +65,7 @@ Diese Punkte sollten **vor** einem echten Ausfall geklaert sein:
|
||||
| Repo-Zugang ausserhalb von Gitea | privater GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` und lokaler aktueller Clone vorhanden |
|
||||
| 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-Passphrase | Host-Secret-Datei vorhanden und fuer Borg-Zugriff verifiziert; externe analoge Hinterlegung bleibt Operator-Aufgabe |
|
||||
| Borg-Passphrase | Host-Secret-Datei vorhanden und fuer Borg-Zugriff verifiziert; externe Offline-Hinterlegung vom Operator am 2026-05-26 bestaetigt |
|
||||
| Secrets-Dateien | ueber Borg bzw. Restore-Quellen abgedeckt |
|
||||
| Komodo Stack ENV-Werte | extern dokumentiert, z. B. Vaultwarden |
|
||||
| Services-Recovery | `docs/SERVICES_RECOVERY.md` gepflegt, insbesondere Gitea-Repo-Mirror und Komodo-Bootstrap |
|
||||
@@ -173,6 +173,28 @@ Diese Werte sind vor dem Start der betroffenen Dienste zu pruefen bzw. wieder in
|
||||
- `KOMODO_PERIPHERY_PASSKEY`
|
||||
- `APP_KEY` und `ADMIN_PASSWORD` fuer `speedtest-tracker`
|
||||
|
||||
Zusaetzlich rebuildbar (keine kritische Recovery-Quelle, koennen aus Provider-/App-UIs neu erzeugt werden):
|
||||
|
||||
- `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` fuer `glance` Community-/Live-Widgets
|
||||
|
||||
### 6.2.1 Restore-Quellen fuer Stack-ENV-Werte
|
||||
|
||||
Stack-ENV-Werte liegen **nicht im Repo** und **nicht als Datei-Secret** unter `/mnt/user/appdata/secrets/`. Sie sind nur an drei Stellen erreichbar; bei Recovery in dieser Reihenfolge pruefen:
|
||||
|
||||
1. **Komodo-Mongo-Dump** `komodo-mongo.archive.gz` unter `/mnt/user/backups/borg/dumps/latest/`. Solange Komodo selbst noch nicht laeuft, ist der Mongo-Dump die kanonische Quelle. Restore in eine Test-Mongo-Instanz, anschliessend Werte aus der `stack`-Collection lesen. **Niemals** Werte in andere Dokumente kopieren.
|
||||
2. **Vaultwarden** Eintrag "Komodo Stack ENV / KalliLab CORE" (bzw. der entsprechende Eintrag pro Stack). Voraussetzung: Vaultwarden ist bereits restauriert (`docs/RESTORE_MATRIX.md`).
|
||||
3. **Externe Operator-Notiz** (versiegelter Umschlag, Bankschliessfach, oder analoge Sicherung neben der Borg-Passphrase). Nur als Notfall-Quelle, wenn weder Komodo-Mongo noch Vaultwarden verfuegbar sind.
|
||||
|
||||
**Reihenfolge-Konsequenz fuer den Bootstrap-Pfad in Phase 4 (Stufe 4 weiter unten):**
|
||||
|
||||
- Vor dem Start von `apps/paperless/`, `apps/immich/`, `apps/mail-archiver/` und `ops/speedtest/` muessen die jeweiligen Stack-ENV-Werte in Komodo wieder hinterlegt sein.
|
||||
- Wenn `komodo-mongo.archive.gz` frisch ist, koennen die Werte beim Komodo-Restart aus dem Dump zurueckgespielt werden, ohne dass jemand sie sieht.
|
||||
- Wenn Vaultwarden vor Komodo restauriert wird (was hier nicht der Standardweg ist), kann auch von dort gelesen werden.
|
||||
|
||||
**Paperless ist die wichtigste bewusste Ausnahme:** `PAPERLESS_DBPASS` und `PAPERLESS_REDIS` sind seit der Hardening-Phase bewusst Stack-ENV (Paperless unterstuetzt `_FILE` fuer DB-Pass nicht). Ein Komodo-Mongo-Dump-Verlust ist daher fuer Paperless gleichbedeutend mit Re-Initialisierung der App-DB; in diesem Fall hilft nur ein Restore aus Vaultwarden oder externer Notiz.
|
||||
|
||||
**Regel:** Konkrete Werte werden **nirgendwo** im Repo, in Logs, in Doku-Kommentaren oder in ntfy-Meldungen wiedergegeben. Auch dieses Dokument haelt nur Variablennamen, Quellen und Reihenfolge fest, keine Werte.
|
||||
|
||||
### 6.3 Rechte
|
||||
|
||||
Nach einem Restore oder manuellem Rueckkopieren:
|
||||
@@ -249,12 +271,18 @@ Ziel:
|
||||
- gemeinsame DB verfuegbar
|
||||
- zentrale Auth laeuft; Authelia nutzt bewusst kein Redis-Session-Backend
|
||||
- Authelia SMTP-Notifier kann GMX erreichen
|
||||
- Redis als shared Cache fuer abhaengige Apps verfuegbar
|
||||
- Redis verfuegbar als App-Cache fuer Paperless (`infra/redis` ist historisch als "shared" angelegt, wird faktisch nur von Paperless genutzt)
|
||||
- Git-Zugriff wiederhergestellt
|
||||
|
||||
### Stufe 3 - Deploy-System
|
||||
|
||||
8. `ops/komodo/`
|
||||
8. `ops/komodo/` - **Kaltstart-Anker, kein Auto-Deploy**
|
||||
|
||||
Komodo wird in dieser Stufe bewusst **nicht** ueber Gitea-Webhook deployed. Der vollstaendige Bootstrap-Pfad ist in `docs/SERVICES_RECOVERY.md` Abschnitt "Komodo Bootstrap" als lineare Stufen A-F dokumentiert. Hier in der DR-Reihenfolge gilt der Einstiegspunkt:
|
||||
|
||||
- Recovery-Anker ist `ops/komodo/docker-compose.yml` aus dem Repo (lokaler Clone, GitHub-Mirror oder `homelab-infra.bundle`-Restore).
|
||||
- Komodo-Stack-ENV-Werte (`KOMODO_*`) sind Stack-ENV-only und werden aus Vaultwarden oder externer Notiz wiederhergestellt (siehe `docs/SECRETS_MAP.md` Abschnitt "Stack-ENV-only Secrets - Restore-Wege").
|
||||
- Erst nach erfolgreicher Validierung der Komodo-Web-UI und Periphery-Verbindung werden in den naechsten Stufen die produktiven Stacks aufgenommen.
|
||||
|
||||
Ziel:
|
||||
|
||||
@@ -400,7 +428,7 @@ Smoke-Test: `hermes-gateway` healthcheck ist gruen, `hermes.kaleschke.info` leit
|
||||
## 11. Offene Vorbereitungs-To-dos
|
||||
|
||||
- Unraid-USB-/Flash-Backup regelmaessig ueber `unraid-flash-config.tar.gz` und optional Unraid Connect pruefen
|
||||
- Borg-Passphrase aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` extern analog sicher hinterlegen
|
||||
- Borg-Passphrase ist laut Operator-Bestaetigung vom 2026-05-26 extern/offline hinterlegt; bei Reviews nur Existenz/Lesbarkeit der Offline-Kopie pruefen, nie den Wert dokumentieren
|
||||
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
|
||||
- regelmaessige automatisierte Restore-Smoke-Tests fuer Vaultwarden, Gitea und Paperless etablieren
|
||||
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
|
||||
|
||||
@@ -10,6 +10,8 @@ Dieses Dokument beschreibt externe Anbieter und Konten, von denen Betrieb, Recov
|
||||
|
||||
| Anbieter / System | Zweck | Kritikalitaet | Recovery-Auswirkung | Zugang / Besitz | Notfallplan |
|
||||
|---|---|---:|---|---|---|
|
||||
| Telekom DSL | Internet-Uplink | hoch | Public Apps, ACME, DDNS, Hetzner-Off-site und Tailscale-Initial-Verbindung fallen aus | Telekom-Kundenkonto | Kein WAN-Failover am Standort eingerichtet (FRITZ!Box-Ausfallschutz inaktiv); lokale LAN-Dienste laufen weiter; Hotspot-Behelf nur fuer Operator-Arbeit, nicht fuer Public Apps |
|
||||
| FRITZ!Box 7590 | Router, DHCP, Telefonie, WAN | hoch | LAN ohne DHCP/Routing; auch lokale Inter-Subnet-Kommunikation kann brechen | Operator-Login auf `192.168.178.1` | FRITZ!Box-Konfig regelmaessig sichern (FRITZ!OS-Backup), Reset-Pin und Account-Pfad bereithalten |
|
||||
| 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 |
|
||||
| Hetzner Storage Box | Off-site Borg Backup | kritisch | Restore aus Off-site ggf. nicht moeglich | Hetzner-Konto / Storage-Box-Zugang ausserhalb Repo | Zweites Off-site-Ziel oder Cold-Platte etablieren; Borg-Passphrase extern sichern |
|
||||
@@ -27,7 +29,7 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng
|
||||
|
||||
| Secret | Zweck | Recovery-Hinweis |
|
||||
|---|---|---|
|
||||
| Borg Passphrase | Entschluesselung Borg-Repos | Muss analog/off-system vorhanden sein |
|
||||
| Borg Passphrase | Entschluesselung Borg-Repos | Offline gesichert, Operator-Bestaetigung 2026-05-26 |
|
||||
| Cloudflare DNS API Token | ACME DNS-Challenge | Token-Rotation und Scope pruefen |
|
||||
| GitHub Mirror Token | Push-Mirror | In Gitea/GitHub verwaltet, nicht im Repo |
|
||||
| Tailscale Account Recovery | Tailnet-Zugang | Account-2FA/Recovery Codes sichern |
|
||||
@@ -56,6 +58,13 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng
|
||||
- AdGuard-Admin-Bind muss so geplant werden, dass ein lokaler Break-glass-Weg bekannt ist.
|
||||
- Seit 2026-05-26 ist AdGuard Admin nur ueber `100.80.98.33:8082` gebunden; bei Tailnet-Ausfall ist lokaler Host-/Compose-Zugriff der Break-glass-Weg.
|
||||
|
||||
### Telekom-DSL / FRITZ!Box gestoert
|
||||
|
||||
- Lokale LAN-Apps (Plex, AdGuard-DNS, lokales Borg-Dump-Repository) bleiben verfuegbar, solange Host und Switch laufen.
|
||||
- Tailscale-Sessions, die bereits stehen, koennen ueber DERP/Relays kurzzeitig weiterlaufen; neue Verbindungen koennen ausfallen.
|
||||
- ACME-/DDNS-/Hetzner-Backup-Laeufe pausieren bis WAN zurueck ist.
|
||||
- FRITZ!OS 8.21 Update wird bewusst nur in einem geplanten Service-Fenster eingespielt, weil Reboot WAN/Tailscale-Aufbau unterbricht.
|
||||
|
||||
### Domain verloren oder Registrar-Zugriff verloren
|
||||
|
||||
- Gitea/GitHub Mirror und lokale IP/Tailscale-Pfade fuer Recovery nutzen.
|
||||
@@ -65,4 +74,6 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng
|
||||
|
||||
| Datum | Ergebnis | Naechste Aktion |
|
||||
|---|---|---|
|
||||
| 2026-05-26 | Bekannte externe Abhaengigkeiten aus Repo-/Betriebsdoku dokumentiert; keine Secret-Werte aufgenommen | Account-Besitz, 2FA-Recovery-Codes, Zahlungswege und Borg-Passphrase extern bestaetigen |
|
||||
| 2026-05-26 | Bekannte externe Abhaengigkeiten aus Repo-/Betriebsdoku dokumentiert; keine Secret-Werte aufgenommen. Borg-Passphrase ist laut Operator offline gesichert. | Account-Besitz, 2FA-Recovery-Codes und Zahlungswege extern bestaetigen |
|
||||
| 2026-05-26 | Telekom-DSL und FRITZ!Box 7590 (FRITZ!OS 8.21) als WAN-/Router-Abhaengigkeit aufgenommen; Ausfallschutz nicht eingerichtet; 2 Portfreigaben aktiv (Soll: 443/tcp + 222/tcp) | FRITZ!OS-Update im Service-Fenster pruefen; Portfreigaben-UI gegen Repo-Soll abgleichen |
|
||||
| 2026-05-27 | FRITZ!Box-Portfreigaben-UI abgeglichen: aktiv sind `80/tcp` und `443/tcp` auf Kallilabcore; `222/tcp` fehlt gegen Repo-Soll. Keine Router-Aenderung vorgenommen. | Nach Operator-Freigabe `80/tcp` entfernen und `222/tcp` nur anlegen, wenn externes Gitea-SSH weiter gewuenscht ist |
|
||||
|
||||
+145
-26
@@ -1,38 +1,157 @@
|
||||
# Family Onboarding - KalliLab CORE
|
||||
# Familien-Willkommen - KalliLab CORE
|
||||
|
||||
Status: Entwurf. Zielgruppe sind Familienmitglieder, nicht Operatoren.
|
||||
Status: **Final-Stand vor Wochenend-Einladung** (2026-05-27). Zielgruppe: Familie. Kein Technik-Wortschatz noetig.
|
||||
|
||||
## Zweck
|
||||
Diese Seite richtet sich an alle, die zuhause unsere eigenen Apps nutzen. Du brauchst kein Technikwissen. Wenn etwas unklar ist: einfach Michi fragen.
|
||||
|
||||
Diese Datei soll spaeter kurz und alltagstauglich erklaeren, wie die wichtigsten Dienste genutzt werden und was bei Problemen zu tun ist. Keine Restore-Matrix, keine Docker-Begriffe.
|
||||
Du musst nichts auswendig lernen. Wenn du nur eine Sache aus dieser Seite mitnimmst: **Passwoerter gehoeren in Vaultwarden, nicht auf Zettel und nicht in den Browser.**
|
||||
|
||||
## Dienste
|
||||
---
|
||||
|
||||
| Dienst | URL | Zweck | Konto / Login | Notiz |
|
||||
|---|---|---|---|---|
|
||||
| Nextcloud | `https://cloud.kaleschke.info` | Dateien, Kalender, Kontakte | TBD | Mobile App/WebDAV/CardDAV |
|
||||
| Immich | `https://immich.kaleschke.info` | Fotos und Smartphone-Backup | TBD | Backup-App pro Handy |
|
||||
| Vaultwarden | `https://vault.kaleschke.info` | Passwoerter | TBD | Familien-Organisation pruefen |
|
||||
| Mealie | `https://mealie.kaleschke.info` | Rezepte und Einkauf | TBD | TBD |
|
||||
| Paperless | `https://paperless.kaleschke.info` | Dokumente | TBD | Scan-/Inbox-Prozess beschreiben |
|
||||
| Plex | intern/App | Medien | TBD | TBD |
|
||||
## Was wir zuhause selbst betreiben
|
||||
|
||||
## Was tun bei Problemen?
|
||||
Wir haben einen eigenen kleinen Server im Haus. Auf dem laufen ein paar Programme, die wir alle gemeinsam nutzen koennen, statt sie bei Google, Apple oder Dropbox liegen zu haben. Vorteile: Unsere Fotos und Dokumente bleiben bei uns. Wir entscheiden, wer was sehen darf. Es kostet uns kein Abo.
|
||||
|
||||
| Situation | Verhalten |
|
||||
|---|---|
|
||||
| Webseite nicht erreichbar | 10 Minuten warten, dann Operator informieren |
|
||||
| Passwort vergessen | Operator informieren, nicht selbst neue Konten anlegen |
|
||||
| Handy-Foto-Backup stoppt | App oeffnen, WLAN/Batteriesparmodus pruefen, Operator informieren |
|
||||
| 2FA verloren | Operator informieren; Recovery-Prozess wird separat festgelegt |
|
||||
| Warnmeldung vom Browser | Nicht weiterklicken, Screenshot machen, Operator informieren |
|
||||
Nachteile, ehrlich gesagt: Wenn der Server zuhause aus ist, sind die Apps weg, bis er wieder laeuft. Das passiert selten — meistens nur, wenn Michi etwas umbaut oder der Strom weg ist. Und es gibt keine Hotline, die Passwoerter zuruecksetzt. Das macht Michi.
|
||||
|
||||
## Offene Inhalte
|
||||
---
|
||||
|
||||
## Die Apps in einem Satz
|
||||
|
||||
| App | Was sie kann | Wie du sie nutzt |
|
||||
|---|---|---|
|
||||
| **Nextcloud** | Dateien und Ordner teilen, Kalender, Adressbuch | Web `cloud.kaleschke.info` oder Nextcloud-App auf dem Handy |
|
||||
| **Immich** | Fotos und Videos automatisch vom Handy sichern, gemeinsam durchblaettern | Immich-App auf dem Handy einrichten lassen |
|
||||
| **Vaultwarden** | Passwoerter sicher speichern und auf jedem Geraet nachschauen | Bitwarden-App (kostenlos), beim ersten Start Server-URL auf `vault.kaleschke.info` aendern lassen |
|
||||
| **Mealie** | Rezepte sammeln, Wochenplan, Einkaufsliste | Web `mealie.kaleschke.info` oder Mealie-App |
|
||||
| **Paperless** | Briefe und wichtige Dokumente scannen, durchsuchen, ablegen | Web `paperless.kaleschke.info`; Scan-Workflow erklaert Michi |
|
||||
| **Plex** | Filme und Musik auf Fernseher, Handy und Tablet | Plex-App auf dem Geraet, mit Konto anmelden |
|
||||
|
||||
> Wenn du eine App auf dem Handy installierst und sie fragt nach einer Server-URL, ist das immer eine `...kaleschke.info`-Adresse. Wenn du dir nicht sicher bist, frag bevor du etwas eintippst.
|
||||
|
||||
---
|
||||
|
||||
## Wie du dich anmeldest
|
||||
|
||||
Beim ersten Mal bekommst du von Michi:
|
||||
|
||||
- deinen Benutzernamen (in der Regel dein Vorname klein geschrieben)
|
||||
- ein Start-Passwort, das du beim ersten Login aenderst
|
||||
|
||||
Wenn eine App selbst einen **Zweitfaktor** anbietet (zum Beispiel Nextcloud), bekommst du dafuer am Anfang gemeinsam mit Michi eine kleine Authentifizierungs-App eingerichtet. Das ist eine extra Schicht: zusaetzlich zum Passwort tippst du beim Login eine 6-stellige Zahl aus dieser App ein.
|
||||
|
||||
> Hinweis: Ein **einheitliches 2FA fuer alle Apps gleichzeitig** ist noch nicht eingerichtet. Aktuell hat jede App ihre eigene Anmeldung. Das bleibt erst einmal so. Wenn sich da etwas aendert, sagt Michi rechtzeitig Bescheid.
|
||||
|
||||
**Bitte:**
|
||||
|
||||
- Speichere alle Passwoerter im Vaultwarden, nicht im Browser.
|
||||
- Schreibe Passwoerter nicht auf Zettel.
|
||||
- Gib dein Passwort niemandem, auch nicht "kurz mal".
|
||||
|
||||
---
|
||||
|
||||
## Foto-Backup vom Handy einrichten (Immich)
|
||||
|
||||
Das ist die App, die deinen Eltern wahrscheinlich am meisten bringt.
|
||||
|
||||
1. App **Immich** im App-Store / Play Store installieren.
|
||||
2. Beim ersten Start nach Server fragen lassen: `https://immich.kaleschke.info`.
|
||||
3. Mit deinem Login anmelden.
|
||||
4. In den App-Einstellungen "Hintergrund-Backup" aktivieren — am besten nur ueber WLAN.
|
||||
5. Fertig. Neue Fotos landen automatisch zuhause auf dem Server.
|
||||
|
||||
> Wenn dein Handy 4 Wochen nicht im Haus-WLAN war, sind die Fotos noch in der Handy-Galerie, aber noch nicht zuhause. Sobald du wieder im WLAN bist und die App startest, holt sie alles nach.
|
||||
|
||||
---
|
||||
|
||||
## Was tun, wenn etwas nicht geht
|
||||
|
||||
### "Die Webseite oeffnet nicht."
|
||||
|
||||
1. 10 Minuten warten — Michi macht vielleicht gerade etwas am Server.
|
||||
2. Anderes Geraet ausprobieren (Handy statt PC oder umgekehrt).
|
||||
3. Wenn es danach immer noch nicht geht: Michi schreiben. Bitte schreib dazu, was du genau aufgerufen hast (`cloud.kaleschke.info` / `immich.kaleschke.info` / ...).
|
||||
|
||||
### "Ich habe mein Passwort vergessen."
|
||||
|
||||
- Nicht selbst neu registrieren. Das geht in den Apps gar nicht.
|
||||
- Michi schreiben. Er setzt dir ein neues Start-Passwort. Du aenderst es beim ersten Login.
|
||||
|
||||
### "Ich habe mein 2FA verloren (neues Handy, App geloescht)."
|
||||
|
||||
- Michi schreiben. Er kann den Zweitfaktor in der betroffenen App fuer dich zuruecksetzen, sobald er dich persoenlich identifiziert.
|
||||
- Wir richten den Zweitfaktor dann neu auf dem neuen Handy ein.
|
||||
- Wenn moeglich: vor einem Handy-Wechsel kurz Bescheid sagen, dann koennen wir 2FA vorher umziehen, statt zuruecksetzen.
|
||||
|
||||
### "Das Handy-Foto-Backup ist stehen geblieben."
|
||||
|
||||
1. Immich-App oeffnen, ist sie noch eingeloggt?
|
||||
2. Bist du im Haus-WLAN? Mobilfunk ist meistens nicht aktiviert.
|
||||
3. Akkusparmodus pruefen — wenn er aktiv ist, kann die App pausieren.
|
||||
4. App schliessen, wieder oeffnen, ein paar Minuten warten.
|
||||
5. Wenn das nicht hilft: Michi schreiben.
|
||||
|
||||
### "Der Browser warnt vor der Seite."
|
||||
|
||||
- **Nicht weiterklicken.**
|
||||
- Screenshot machen.
|
||||
- Michi schicken.
|
||||
- Wahrscheinlich ist gerade ein Zertifikat abgelaufen — das ist normalerweise schnell behoben, aber Michi muss kurz draufschauen.
|
||||
|
||||
### "Mein Familien-Mitglied sagt, sein Konto sei gesperrt."
|
||||
|
||||
- Manchmal sperren die Apps Konten nach mehreren falschen Passwoertern fuer ein paar Minuten. Das ist Absicht. Einfach 10 Minuten warten und nochmal versuchen.
|
||||
- Wenn es laenger dauert: Michi schreiben.
|
||||
|
||||
---
|
||||
|
||||
## Was du **nicht** musst
|
||||
|
||||
- Du musst nichts installieren, einrichten oder warten.
|
||||
- Du musst keine Updates pruefen.
|
||||
- Du musst nicht wissen, wo die Daten genau liegen — sie liegen auf dem Server zuhause und werden automatisch gesichert.
|
||||
- Du musst dir keine Adressen merken — alle Apps sind ueber `...kaleschke.info` erreichbar und Michi schickt dir den Direktlink, wenn du einen brauchst.
|
||||
|
||||
## Was wir uns gemeinsam wuenschen
|
||||
|
||||
- Bitte nutze Vaultwarden fuer alle Familien-Passwoerter. Das schuetzt uns alle.
|
||||
- Bitte sag Bescheid, wenn etwas komisch wirkt (seltsame E-Mail, Login-Aufforderung an der falschen Stelle). Lieber einmal zu oft fragen.
|
||||
- Wenn dir eine App fehlt oder du eine Idee hast, was wir gemeinsam besser machen koennten: ansprechen, nicht selbst herumprobieren.
|
||||
|
||||
---
|
||||
|
||||
## Wenn der Server zuhause mal komplett aus ist
|
||||
|
||||
Das kommt selten vor. In dem Fall:
|
||||
|
||||
- Webseiten von `...kaleschke.info` oeffnen nicht.
|
||||
- Foto-Backup von Immich pausiert automatisch — neue Fotos bleiben auf dem Handy und werden nachgeholt, sobald der Server wieder da ist.
|
||||
- Plex-App zeigt "Server offline" — Filme sind weiterhin da, sobald der Server zurueck ist.
|
||||
- Vaultwarden: gespeicherte Passwoerter sind weiterhin in der Bitwarden-App offline verfuegbar.
|
||||
- Es geht **nichts kaputt**, nur weil der Server kurz aus ist.
|
||||
|
||||
Michi laesst es dich wissen, wenn ein Wartungsfenster geplant ist.
|
||||
|
||||
---
|
||||
|
||||
## Offene Inhalte (Operator-Notiz)
|
||||
|
||||
Diese Punkte gehoeren in das Wochenend-Onboarding-Gespraech und sind nicht Teil dieser Familien-Seite:
|
||||
|
||||
| Status | Aufgabe |
|
||||
|---|---|
|
||||
| offen | Pro Dienst kurze Schritt-fuer-Schritt-Anleitung schreiben |
|
||||
| offen | Konto-/2FA-Policy final entscheiden |
|
||||
| offen | Immich Mobile Backup fuer alle Geraete testen |
|
||||
| offen | Vaultwarden Familienorganisation pruefen |
|
||||
| offen | Pro Familien-Konto Benutzernamen und Start-Passwort persoenlich uebergeben (Vaultwarden Familien-Organisation als Uebergabeweg) |
|
||||
| offen | 2FA-App-Empfehlung pro Person festlegen (zum Beispiel Bitwarden Authenticator, Aegis, 2FAS) |
|
||||
| offen | Vaultwarden Familien-Organisation einrichten und Mitglieder einladen |
|
||||
| offen | Immich Mobile Backup mit jedem Familien-Geraet einmal gemeinsam ausprobieren |
|
||||
| offen | Scan-/Inbox-Anleitung fuer Paperless ergaenzen, sobald der Workflow final ist |
|
||||
| offen | Einladungstermin Wochenende mit konkretem Datum festlegen |
|
||||
|
||||
## Bewusst nicht versprochen
|
||||
|
||||
Damit niemand spaeter enttaeuscht ist, hier kurz, was die Seite **nicht** verspricht:
|
||||
|
||||
- Es gibt aktuell **kein** Einheits-Login fuer alle Apps. Jede App hat ihre eigene Anmeldung. Eine Vereinheitlichung ist als Idee notiert, aber zeitlich noch nicht geplant.
|
||||
- Es gibt **keine** Garantie, dass eine App 24/7 verfuegbar ist. Es ist ein Heim-Server, kein Rechenzentrum.
|
||||
- Es gibt **keinen** automatischen Support per Hotline. Probleme gehen an Michi.
|
||||
- Es gibt **kein** Werbe-Konto und keinen Versand deiner Daten an Externe. Alles bleibt zuhause.
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
# Family-View Dashboard - Spezifikation
|
||||
|
||||
Status: **Spezifikation (Doku-only)**, kein Grafana-JSON in diesem Schritt.
|
||||
Audit-Bezug: `docs/AUDIT_2026-05-25.md` Finding **F-08** (Alerts/Sichtbarkeit) und das Sprint-3-TODO "Family-View Dashboard definieren" aus `docs/AUDIT_2026-05-25_TODO.md`.
|
||||
|
||||
## Zweck
|
||||
|
||||
Ein Grafana-Dashboard, das beim Morgen-Check in unter 30 Sekunden zeigt, ob das Homelab gesund ist. Zielgruppe ist primaer der Operator. Wenn die Familie es zufaellig anschaut, soll niemand erschrecken: ueberall gruene Felder bedeuten "alles in Ordnung", ohne dass man die Technik dahinter verstehen muss.
|
||||
|
||||
Das Dashboard ist die Konsolidierung des morgendlichen Pruefablaufs:
|
||||
|
||||
- Sind die wichtigsten Apps erreichbar?
|
||||
- Hat das Backup gestern Nacht funktioniert?
|
||||
- Wann laufen die Zertifikate aus?
|
||||
- Sind die Disks ausreichend frei?
|
||||
- Laufen die kritischen Container?
|
||||
|
||||
## Abgrenzung
|
||||
|
||||
Diese Datei beschreibt nur Layout, Datenquellen und PromQL-Queries. Die JSON-Datei `monitoring/grafana/dashboards/family-view.json` wird **bewusst noch nicht** angelegt, weil:
|
||||
|
||||
- Es noch keine Live-Pruefung gegen die echte Grafana-Instanz gab.
|
||||
- Die Alert-Regeln (Borg-Stale, Cert-Expiry, Container-Down) sind laut `docs/AUDIT_2026-05-25_TODO.md` Sprint 3 selbst noch im "in Arbeit (Regeln vorbereitet)"-Status.
|
||||
- Bei einem halbgaren Dashboard-JSON entstehen mehr Wartungsfragen als Klarheit.
|
||||
|
||||
Die JSON-Datei wird angelegt, sobald (a) die genannten Metriken stabil verfuegbar sind und (b) ein erster manueller Build im Grafana-UI das Layout bestaetigt hat.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
Authoritativ ist `monitoring/grafana/provisioning/datasources/datasources.yml`. Das Dashboard nutzt nur die schon provisionierten Datasources:
|
||||
|
||||
- `Prometheus` - Blackbox, node-exporter, cAdvisor, Traefik-Metrics
|
||||
- optional `Loki` - Log-Volume-Spike als Zusatz-Panel
|
||||
- bewusst nicht: `InfluxDB 3 Core` (das ist Home-Assistant-/Ecowitt-Sicht, nicht Homelab-Health)
|
||||
|
||||
## Layout (4x4 Grid, mobile-vertraeglich)
|
||||
|
||||
| Zeile | Panel | Breite (Grafana w) | Hoehe (Grafana h) |
|
||||
|---|---|---:|---:|
|
||||
| 1 | Endpoints up (Stat, gross gruen/rot) | 12 | 5 |
|
||||
| 1 | Backup heute Nacht (Stat) | 6 | 5 |
|
||||
| 1 | Naechster Cert-Ablauf (Stat, Tage) | 6 | 5 |
|
||||
| 2 | Kritische Container running (Stat-Liste) | 12 | 6 |
|
||||
| 2 | Disk-Fuellung (Bargraph, je Mountpoint) | 12 | 6 |
|
||||
| 3 | Endpoint-Tabelle (Table: Host, Status, Latenz) | 24 | 8 |
|
||||
| 4 | Cert-Tage-Tabelle (Table: Host, Tage bis Ablauf) | 12 | 6 |
|
||||
| 4 | Container-Status-Tabelle (Table: kritischer Container, Running, letztes Restart) | 12 | 6 |
|
||||
|
||||
Dashboard-Metadaten:
|
||||
|
||||
- UID: `homelab-family-view`
|
||||
- Title: `Homelab / Family View`
|
||||
- Tags: `homelab`, `family-view`, `morning-check`
|
||||
- Refresh: `30s`
|
||||
- Default-Zeitfenster: `now-24h` bis `now`
|
||||
- Folder: `Homelab`
|
||||
|
||||
## Panel-Spezifikation
|
||||
|
||||
### Panel 1: Endpoints up
|
||||
|
||||
- Type: `stat`
|
||||
- Title: `Apps online`
|
||||
- Query: `sum(probe_success{job="blackbox-http"})`
|
||||
- Anzeige: gruene Zahl bei Gesamtzahl, Wechsel auf rot wenn `< Soll-Anzahl`.
|
||||
- Threshold: Soll-Anzahl wird aus `monitoring/blackbox/blackbox.yml` und Prometheus-Scrape-Liste abgeleitet (zum Doku-Zeitpunkt 19 HTTPS-Ziele laut `docs/MIGRATION_LOG.md` 2026-05-25-Monitoring-Konsolidierung).
|
||||
- Subtitel im Panel: `von <N> erreichbar`. (Soll-Wert wird beim Bau aus dem aktuellen Target-Count gesetzt; nicht hartcoden.)
|
||||
|
||||
### Panel 2: Backup heute Nacht
|
||||
|
||||
- Type: `stat`
|
||||
- Title: `Borg-Lauf`
|
||||
- Query (sobald Borg-Stale-Metrik im Textfile-Collector live ist):
|
||||
```promql
|
||||
(time() - homelab_borg_last_completed_timestamp_seconds) / 3600
|
||||
```
|
||||
- Einheit: `h`
|
||||
- Threshold:
|
||||
- 0-26 h gruen
|
||||
- 26-30 h gelb
|
||||
- >30 h rot
|
||||
- Subtitel im Panel: `Stunden seit letztem completed-Lauf`.
|
||||
- Fallback bis Metrik live: Panel zeigt `n/a`, Doku-Hinweis in der Beschreibung.
|
||||
|
||||
### Panel 3: Naechster Cert-Ablauf
|
||||
|
||||
- Type: `stat`
|
||||
- Title: `Cert laeuft in`
|
||||
- Query:
|
||||
```promql
|
||||
min((probe_ssl_earliest_cert_expiry{job="blackbox-http"} - time()) / 86400)
|
||||
```
|
||||
- Einheit: `d` (Tage)
|
||||
- Threshold:
|
||||
- >14 gruen
|
||||
- 7-14 gelb
|
||||
- <7 rot
|
||||
- Subtitel: `Tage bis kleinste Restlaufzeit aller geprueften Hosts`.
|
||||
|
||||
### Panel 4: Kritische Container running
|
||||
|
||||
- Type: `stat`
|
||||
- Title: `Kritische Container`
|
||||
- Query (sobald Container-Up-Metrik live ist):
|
||||
```promql
|
||||
sum(homelab_critical_container_running)
|
||||
```
|
||||
- Threshold: erwartete Anzahl gruen, jeder fehlende Container rot.
|
||||
- Subtitel: `von <N> erwartet`. Erwartete Liste pflegen wir in `services/posture-check` / Textfile-Exporter (siehe `docs/AUDIT_2026-05-25_TODO.md` Sprint 3 "Container-Down-Alert").
|
||||
- Fallback: cAdvisor-Query als Naeherung, solange Textfile-Metrik noch nicht produktiv ist:
|
||||
```promql
|
||||
count(rate(container_last_seen{name=~"traefik|authelia|postgresql17|Redis|gitea|komodo-core|komodo-mongo|komodo-periphery|monitoring-prometheus|monitoring-grafana|monitoring-loki|monitoring-alertmanager"}[5m]) > 0)
|
||||
```
|
||||
|
||||
### Panel 5: Disk-Fuellung
|
||||
|
||||
- Type: `bargauge`
|
||||
- Title: `Disk-Fuellung`
|
||||
- Query:
|
||||
```promql
|
||||
100 * (1 - node_filesystem_avail_bytes{fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{fstype!~"tmpfs|overlay"})
|
||||
```
|
||||
- Anzeige: pro Mountpoint, sortiert absteigend.
|
||||
- Threshold:
|
||||
- <70 gruen
|
||||
- 70-85 gelb
|
||||
- >85 rot
|
||||
- Subtitel: `Prozent belegt`.
|
||||
|
||||
### Panel 6: Endpoint-Tabelle
|
||||
|
||||
- Type: `table`
|
||||
- Title: `Endpoint-Status`
|
||||
- Spalten:
|
||||
- Host (Instance)
|
||||
- Status (`UP` / `DOWN`)
|
||||
- Antwortzeit (probe_duration_seconds)
|
||||
- Queries:
|
||||
- `probe_success{job="blackbox-http"}` -> Mapping: `1` -> `UP` (gruen), `0` -> `DOWN` (rot)
|
||||
- `probe_duration_seconds{job="blackbox-http"}` -> Sekunden
|
||||
- Sortierung: DOWN oben, dann nach Antwortzeit absteigend.
|
||||
|
||||
### Panel 7: Cert-Tage-Tabelle
|
||||
|
||||
- Type: `table`
|
||||
- Title: `Cert-Tage bis Ablauf`
|
||||
- Spalten: Host, Tage
|
||||
- Query:
|
||||
```promql
|
||||
(probe_ssl_earliest_cert_expiry{job="blackbox-http"} - time()) / 86400
|
||||
```
|
||||
- Sortierung: aufsteigend (am ehesten ablaufende oben).
|
||||
- Color-Mapping wie Panel 3.
|
||||
|
||||
### Panel 8: Container-Status-Tabelle
|
||||
|
||||
- Type: `table`
|
||||
- Title: `Kritische Container`
|
||||
- Spalten: Container, Running (1/0), letztes Restart (Sekunden seit Start)
|
||||
- Queries:
|
||||
- sobald Textfile-Metrik live: `homelab_critical_container_running` (Label `name`)
|
||||
- Fallback aus cAdvisor: `container_start_time_seconds{name=~"<Whitelist>"}`
|
||||
- Sortierung: nicht-running zuerst.
|
||||
|
||||
## Spaeter ergaenzbar (nicht Teil der ersten Version)
|
||||
|
||||
- Loki-Log-Volume-Spike-Panel
|
||||
- node_exporter Memory-Saturation-Panel
|
||||
- Plex-Sessions (nur wenn Plex-Exporter eingerichtet ist; aktuell nicht geplant)
|
||||
- Immich Asset-Wachstum (eigenes Dashboard, nicht Family-View)
|
||||
|
||||
## Build-Reihenfolge fuer den spaeteren JSON
|
||||
|
||||
Wenn das JSON gebaut wird, bitte in dieser Reihenfolge:
|
||||
|
||||
1. Sicherstellen, dass alle Metriken aus den Queries oben in Prometheus auffindbar sind (`/graph` Smoke-Test).
|
||||
2. Dashboard in Grafana-UI manuell zusammenklicken; Layout an dieser Spezifikation entlang.
|
||||
3. JSON exportieren, in `monitoring/grafana/dashboards/family-view.json` ablegen.
|
||||
4. Provisioning-Provider laesst die Datei automatisch laden (siehe `monitoring/grafana/provisioning/dashboards/dashboards.yml`).
|
||||
5. Bei jeder Schema-Aenderung Doku hier nachziehen, damit Spec und JSON nicht driften.
|
||||
|
||||
## Smoke-Test nach Aktivierung
|
||||
|
||||
- Dashboard laedt unter `https://monitoring.kaleschke.info/d/homelab-family-view/`.
|
||||
- Alle 8 Panels rendern ohne `No data`.
|
||||
- Im Normalbetrieb erscheinen Panel 1-5 vollstaendig gruen.
|
||||
- Ein bewusster Test-Stale-Borg oder ein Container-Stop laesst die zugehoerigen Panels auf gelb/rot wechseln.
|
||||
|
||||
## Was das Dashboard NICHT ersetzt
|
||||
|
||||
- ntfy-Alerts: das Dashboard ist passiv (Pull), ntfy ist aktiv (Push). Beide sind notwendig.
|
||||
- DR-Doku: `docs/DISASTER_RECOVERY.md` bleibt die Recovery-Quelle.
|
||||
- Restore-Tests: `docs/RESTORE_DRILL_ROUTINE.md` ist die Kadenz, die das Dashboard nicht ersetzt.
|
||||
- Familien-Onboarding: `docs/FAMILY_ONBOARDING.md` ist die Doku fuer die Familie, dieses Dashboard ist Operator-Tool.
|
||||
@@ -0,0 +1,104 @@
|
||||
# FRITZ!Box Portfreigaben - Korrektur-Vorbereitung
|
||||
|
||||
Status: **umgesetzt 2026-05-28**, Doku bleibt als Historie und Begruendungs-Anker.
|
||||
Audit-Bezug: `docs/AUDIT_2026-05-25_TODO.md` Sprint 4 ("FRITZ!Box-Portfreigaben gegen Repo-Soll abgleichen") und `docs/NETWORK_INVENTORY.md`.
|
||||
|
||||
## Umsetzungs-Befund 2026-05-28
|
||||
|
||||
| Punkt | Entscheidung | Validierung |
|
||||
|---|---|---|
|
||||
| 1. `80/tcp` entfernen | **umgesetzt** | Mobilfunk-Test: `http://vault.kaleschke.info` Timeout, `https://...` weiter erreichbar. Lokal via LAN: HTTP->HTTPS-Redirect funktioniert weiter durch Traefik. |
|
||||
| 2. `222/tcp` ergaenzen | **bewusst NICHT umgesetzt** | Tailscale bleibt Operator-Pfad; GitHub-Mirror deckt DR-Bootstrap ab; SSH-Brute-Force-Vektor vermieden. Repo-Soll in `NETWORK_INVENTORY.md` und `MASTER_V2.md` Sektion 10 entsprechend angepasst. |
|
||||
| 3. UPnP-Selbstfreigabe `PC-192-168-178-71` | **umgesetzt** | "Selbstständige Portfreigaben fuer dieses Geraet erlauben" deaktiviert. Identifiziert als VONETS-WiFi-Bridge (Hostname `VONETS.COM`, MAC `00:17:13:2F:61:96`) — vermutlich Bridge zum SolarEdge-Wechselrichter. SolarEdge-Cloud-Sync ist outbound und benoetigt keine UPnP. |
|
||||
|
||||
Aktiver Endstand: ausschliesslich `443/tcp -> 192.168.178.58:443` (Traefik HTTPS) ist von WAN aus erreichbar.
|
||||
|
||||
## Aktueller FRITZ!Box-Befund (2026-05-27)
|
||||
|
||||
Aus dem Operator-Live-Check der FRITZ!Box-UI:
|
||||
|
||||
- aktiv: `80/tcp` -> `192.168.178.58`
|
||||
- aktiv: `443/tcp` -> `192.168.178.58`
|
||||
- fehlt: `222/tcp` -> `192.168.178.58` (Gitea-SSH)
|
||||
- zusaetzlich gemeldet: eine Portfreigabe durch das Geraet `PC-192-168-178-71` (Selbst-Freigabe per UPnP)
|
||||
|
||||
## Soll-Stand laut Repo
|
||||
|
||||
`docs/NETWORK_INVENTORY.md` definiert genau zwei aktive Portfreigaben:
|
||||
|
||||
| Erwartete Freigabe | Ziel | Begruendung |
|
||||
|---|---|---|
|
||||
| `443/tcp` -> `192.168.178.58:443` | Traefik HTTPS | einziger Public-HTTPS-Einstieg, Wildcard-Zert ueber Cloudflare-DNS-Challenge |
|
||||
| `222/tcp` -> `192.168.178.58:222` | Gitea SSH | Git-SSH-Push/Pull; dokumentierte Ausnahme |
|
||||
|
||||
Port `80/tcp` ist im Cloudflare-DNS-Challenge-Modell **nicht** notwendig.
|
||||
|
||||
## Drei Korrektur-Punkte
|
||||
|
||||
### 1. `80/tcp` entfernen
|
||||
|
||||
Begruendung:
|
||||
|
||||
- ACME laeuft als Cloudflare-DNS-Challenge (`traefik/docker-compose.yml`), nicht als HTTP-01.
|
||||
- Traefik leitet intern jeden HTTP-Request auf `https://` weiter; ein WAN-`80`-Listener bietet keinen Mehrwert, oeffnet aber einen zusaetzlichen Angriffsvektor (Header-/Method-Scanning, Open-Redirect-Versuche bevor TLS terminiert).
|
||||
- Innerhalb des LAN funktioniert die Browser-Auto-HTTPS-Umleitung weiter ueber AdGuard-DNS.
|
||||
|
||||
Empfehlung: WAN-Eintrag `80/tcp` in FRITZ!Box-UI **entfernen** nach Operator-Go.
|
||||
|
||||
Validierung nach Aenderung:
|
||||
|
||||
```bash
|
||||
# WAN-seitiger Test, idealerweise von einem Geraet im Mobilfunknetz oder Tailscale-Exit-Node
|
||||
curl -sI http://kaleschke.info/ # erwartet: Connection refused / Timeout
|
||||
curl -sI https://vault.kaleschke.info/ # erwartet: HTTP/2 200 oder Authelia-Redirect
|
||||
```
|
||||
|
||||
### 2. `222/tcp` ergaenzen (nur wenn externes Git-SSH wirklich gewuenscht)
|
||||
|
||||
Frage an Operator: Wird `git@git.kaleschke.info -p 222` von extern gebraucht? Hinweise:
|
||||
|
||||
| Pro `222/tcp` extern | Contra `222/tcp` extern |
|
||||
|---|---|
|
||||
| Push/Pull vom unterwegs-Laptop ohne Tailscale | Tailscale ist schon der Operator-Pfad, deckt das voll ab |
|
||||
| GitHub-Mirror-Bootstrap funktioniert dann auch ohne Tailscale | GitHub-Push-Mirror laeuft automatisch von Gitea aus, braucht kein WAN-SSH |
|
||||
| Externe Webhooks gegen Git push (nicht in Nutzung) | weniger Angriffsflaeche fuer SSH-Brute-Force |
|
||||
|
||||
Empfehlung: `222/tcp` **nicht** ergaenzen, solange Tailscale stabil verfuegbar ist. Stattdessen `docs/NETWORK_INVENTORY.md` und `HOMELAB_ARCHITECTURE_MASTER_V2.md` darauf abgleichen, dass Gitea-SSH bewusst LAN/Tailscale-only ist.
|
||||
|
||||
Wenn Operator entscheidet, `222/tcp` doch extern zu oeffnen: zusaetzlich SSH-Login auf Key-only setzen, Brute-Force-Limits in Gitea pruefen, `docs/NETWORK_INVENTORY.md` "Erwartete Freigabe"-Tabelle aktualisieren.
|
||||
|
||||
### 3. UPnP-Selbstfreigabe von `PC-192-168-178-71` deaktivieren
|
||||
|
||||
Begruendung:
|
||||
|
||||
- Geraete-initiierte Portfreigaben (UPnP) sind ausserhalb der Repo-Sollkonfiguration.
|
||||
- Welcher Port von welchem Programm geoeffnet wurde, ist aus der FRITZ!Box-UI heraus nicht versionierbar.
|
||||
- Wenn der Port gebraucht wird, gehoert er als bewusste Operator-Freigabe in `docs/NETWORK_INVENTORY.md`.
|
||||
|
||||
Empfehlung in zwei Stufen:
|
||||
|
||||
1. FRITZ!Box-UI: in den Geraete-Details fuer `PC-192-168-178-71` die aktuelle Selbstfreigabe-Liste pruefen und mit dem Operator besprechen.
|
||||
2. Wenn der Port nicht gebraucht wird: Selbstfreigabe deaktivieren. Optional: UPnP global pro Geraet abschalten ("Selbststaendige Portfreigaben fuer dieses Geraet erlauben" abwaehlen).
|
||||
|
||||
## Schutzregeln
|
||||
|
||||
- Keine Router-Aenderung ohne ausdrueckliches Operator-Go.
|
||||
- Nach jeder Aenderung: `docs/NETWORK_INVENTORY.md` Abschnitt "FRITZ!Box (WAN -> Host)" gegen den neuen UI-Stand abgleichen.
|
||||
- Aenderung in `docs/MIGRATION_LOG.md` als kurzer Eintrag dokumentieren (was/warum/Validierung).
|
||||
- Bei `80/tcp`-Entfernung kurz prufen, ob irgendein externer Dienst noch HTTP-01 nutzen wollte (sollte nicht der Fall sein).
|
||||
|
||||
## Nicht Teil dieser Vorbereitung
|
||||
|
||||
- FRITZ!OS-Update 8.21 -> aktuell. Das ist eigenes Service-Fenster und braucht WAN/Tailscale-Aufbau-Beobachtung.
|
||||
- IPv6-Exposure. Wenn Telekom IPv6 zustellt und Apps via Cloudflare-AAAA erreichbar sind, kann WAN-Filter pro Port noetig werden. Separater Doku-Punkt in `docs/NETWORK_INVENTORY.md` Offene Entscheidungen.
|
||||
- Mobilfunk-Failover-Stick. Bewusst nicht eingerichtet.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
| Status | Punkt | Naechster Schritt |
|
||||
|---|---|---|
|
||||
| erledigt 2026-05-28 | `80/tcp` entfernen | umgesetzt, Mobilfunk-validiert |
|
||||
| erledigt 2026-05-28 (bewusst nicht) | `222/tcp` Entscheidung | bleibt Tailscale-only; Repo-Soll entsprechend angepasst |
|
||||
| erledigt 2026-05-28 | UPnP-Freigabe `PC-192-168-178-71` | UPnP-Selbstfreigabe-Recht fuer VONETS-Bridge deaktiviert |
|
||||
| offen | FRITZ!OS 8.21 Update | Service-Fenster, separat geplant |
|
||||
| offen | IPv6-Exposure pruefen | bei naechstem WAN-Touch mit erfassen |
|
||||
@@ -1,6 +1,6 @@
|
||||
# Hardware Inventory - KalliLab CORE
|
||||
|
||||
Status: Hardware-Baseline erfasst; USV/Power-Loss bleibt offene Betreiberentscheidung.
|
||||
Status: Hardware-Baseline erfasst; USV/Power-Loss ist als bewusst akzeptiertes Betreiber-Risiko dokumentiert.
|
||||
Host: `Kallilabcore`
|
||||
Letzte Pruefung: 2026-05-26
|
||||
Naechster Review: 2026-08-26
|
||||
@@ -126,7 +126,7 @@ smartctl -a /dev/sdc
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| USV vorhanden | Nicht validiert / keine erkannte USV |
|
||||
| USV vorhanden | Nein / keine erkannte USV |
|
||||
| Modell | Kein APC/Eaton/CyberPower-Geraet per `lsusb` erkannt |
|
||||
| Verbindung | `apcupsd` ist auf USB vorkonfiguriert, aber kein passendes USB-USV-Geraet sichtbar |
|
||||
| Software | `apcaccess` vorhanden; `apcupsd` laeuft nicht, `localhost:3551` liefert Connection refused |
|
||||
@@ -138,8 +138,9 @@ Bewertung:
|
||||
|
||||
- Aktueller Befund 2026-05-26: keine funktionierende USV-Absicherung nachgewiesen.
|
||||
- `apcupsd` ist zwar auf dem System vorhanden, aber nicht aktiv.
|
||||
- Power-Loss bleibt damit ein bewusst offenes Risiko fuer Docker-/DB-State und laufende Writes.
|
||||
- Naechste Entscheidung: echte USV anschliessen und Shutdown testen oder Risiko bewusst akzeptieren und dokumentieren.
|
||||
- Operator-Entscheidung 2026-05-26: aktuell keine USV-Anschaffung.
|
||||
- Power-Loss bleibt damit ein bewusst akzeptiertes Risiko fuer Docker-/DB-State und laufende Writes.
|
||||
- Review-Ausloeser: Hardware-Erweiterung, wiederholte Stromausfaelle, Datenkorruption oder Veraenderung der Betreiber-Prioritaet.
|
||||
|
||||
## Stromverbrauch
|
||||
|
||||
@@ -159,7 +160,7 @@ Bewertung:
|
||||
| Parity | Kleiner als neue groesste Datenplatte | Parity-Upgrade vor Datenplatten-Upgrade |
|
||||
| Boot-USB | Lesefehler oder Alter TBD | Flash-Backup verifizieren, Ersatzstick vorbereiten |
|
||||
| RAM | Swap/OOM oder Immich/Nextcloud-Druck | Ausbau planen |
|
||||
| USV | keine funktionierende USV-Abschaltung | USV anschaffen/anschliessen oder Risiko schriftlich akzeptieren |
|
||||
| USV | keine funktionierende USV-Abschaltung | Risiko am 2026-05-26 bewusst akzeptiert; bei Review erneut bewerten |
|
||||
|
||||
## Audit-Kommandos
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# H:/ Nearline Pull
|
||||
|
||||
Status: **produktiv** (2026-05-28). Erster echter Lauf 2026-05-27 20:45 erfolgreich. Windows Scheduled Task `KalliLab H Drive Nearline Pull` taeglich 05:30 ist seit 2026-05-28 aktiv.
|
||||
|
||||
## Erstlauf-Befund 2026-05-27
|
||||
|
||||
- Erster `-WhatIf`-loser Lauf: 18 Borg-Dump-Files erfolgreich gepullt, 4 unraid-flash-config-Files und 10 Gitea-Bundle-Files blockiert (`Zugriff verweigert`).
|
||||
- Ursache: Bundles wurden mit `chmod 600` geschrieben, Flash-Config bewusst `0600 root:root`, Filebrowser-Dump erbte 0640. Der SMB-Read-Share auf dem Operator-PC liest mit unprivilegierten Rechten, kein root.
|
||||
- Fixes im selben Sprint:
|
||||
- `ops/borg-ui/scripts/gitea-bundle-mirror.sh` schreibt Bundles und Sidecars jetzt 0644 (Bundle-Inhalt = Git-Historie, ohne Secrets durch `.gitignore`).
|
||||
- `ops/borg-ui/scripts/pre-backup-dumps.sh` setzt alle Dumps via `atomic_write` per Default auf 0644; `unraid-flash-config.*` bleibt explizit 0600.
|
||||
- `ops/h-drive-nearline/pull-critical-backups.ps1` excluded die `unraid-flash-config.*`-Familie ueber `/XF`, damit Flash-Config bewusst nicht in den Nearline-Scope kommt.
|
||||
- Zweiter Lauf (nach Fixes): beide Robocopy-Jobs Exit-Code 1, **19 Borg-Dumps + 10 Gitea-Bundle-Files** auf H:/.
|
||||
|
||||
## Zweck
|
||||
|
||||
`H:/` ist eine zweite lokale Nearline-Kopie fuer die wichtigsten Restore-Artefakte. Es ersetzt weder Hetzner/Borg noch ein echtes Off-site-/Airgap-Ziel, reduziert aber das Risiko, dass ein lokaler Restore nur vom Unraid-Array abhaengt.
|
||||
|
||||
## Quelle und Ziel
|
||||
|
||||
| Zweck | Quelle | Ziel |
|
||||
|---|---|---|
|
||||
| Aktuelle Dumps inklusive Flash-Backup | `\\192.168.178.58\backups\borg\dumps\latest` | `H:\kallilab-nearline-backups\borg-dumps\latest` |
|
||||
| Gitea-Bundles | `\\192.168.178.58\backups\git-bundles\gitea` | `H:\kallilab-nearline-backups\git-bundles\gitea` |
|
||||
|
||||
Das Skript kopiert bewusst **nicht** mit `/MIR` und loescht keine Dateien auf `H:/`. Alte Artefakte duerfen dort erst nach manueller Sichtpruefung geloescht werden.
|
||||
|
||||
## Skript
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File G:\Gitea_Clone\homelab-infra\ops\h-drive-nearline\pull-critical-backups.ps1 -WhatIf
|
||||
```
|
||||
|
||||
Echter Lauf:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File G:\Gitea_Clone\homelab-infra\ops\h-drive-nearline\pull-critical-backups.ps1
|
||||
```
|
||||
|
||||
Reports landen unter:
|
||||
|
||||
```text
|
||||
H:\kallilab-nearline-backups\_reports
|
||||
```
|
||||
|
||||
Robocopy-Logs landen unter:
|
||||
|
||||
```text
|
||||
H:\kallilab-nearline-backups\_logs
|
||||
```
|
||||
|
||||
## Geplanter Schedule
|
||||
|
||||
Empfohlen: taeglich 05:30 Uhr, nach dem Borg-Dump-Fenster um ca. 04:00 Uhr.
|
||||
|
||||
Aktiv seit 2026-05-28. Tatsaechlicher Register-Befehl (RunLevel-Enum-Wert ist `Limited`, nicht `LeastPrivilege`):
|
||||
|
||||
```powershell
|
||||
$Action = New-ScheduledTaskAction `
|
||||
-Execute "powershell.exe" `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"G:\Gitea_Clone\homelab-infra\ops\h-drive-nearline\pull-critical-backups.ps1`""
|
||||
|
||||
$Trigger = New-ScheduledTaskTrigger -Daily -At 05:30
|
||||
|
||||
$Settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
|
||||
Register-ScheduledTask `
|
||||
-TaskName "KalliLab H Drive Nearline Pull" `
|
||||
-Action $Action `
|
||||
-Trigger $Trigger `
|
||||
-Settings $Settings `
|
||||
-Description "Copies critical KalliLab restore artifacts from Unraid SMB backup share to H:/ nearline disk." `
|
||||
-RunLevel Limited
|
||||
```
|
||||
|
||||
Status pruefen:
|
||||
|
||||
```powershell
|
||||
Get-ScheduledTask -TaskName "KalliLab H Drive Nearline Pull" | Format-List TaskName, State
|
||||
Get-ScheduledTaskInfo -TaskName "KalliLab H Drive Nearline Pull" | Format-List LastRunTime, LastTaskResult, NextRunTime, NumberOfMissedRuns
|
||||
```
|
||||
|
||||
Manueller Trigger zum Testen:
|
||||
|
||||
```powershell
|
||||
Start-ScheduledTask -TaskName "KalliLab H Drive Nearline Pull"
|
||||
```
|
||||
|
||||
Verhalten:
|
||||
|
||||
- Laeuft als angemeldeter User (`RunLevel Limited`); wenn der PC abgemeldet ist, wartet der Task bis zur naechsten Anmeldung (`StartWhenAvailable`).
|
||||
- Akku-Modus blockiert nicht (`AllowStartIfOnBatteries`).
|
||||
- Maximale Laufzeit 2 h, danach wird der Task abgebrochen.
|
||||
|
||||
## Erfolgscheck
|
||||
|
||||
Nach einem echten Lauf muessen mindestens diese Artefakte unter `H:\kallilab-nearline-backups` liegen:
|
||||
|
||||
- `borg-dumps\latest\immich.dump`
|
||||
- `borg-dumps\latest\komodo-mongo.archive.gz`
|
||||
- `borg-dumps\latest\postgresql17-paperless.dump`
|
||||
- `borg-dumps\latest\postgresql17-mailarchiver.dump`
|
||||
- `borg-dumps\latest\nextcloud.dump`
|
||||
- `borg-dumps\latest\mealie.dump`
|
||||
- `borg-dumps\latest\gitea.sqlite.dump`
|
||||
- `borg-dumps\latest\vaultwarden.sqlite.dump`
|
||||
- `git-bundles\gitea\latest-report.md`
|
||||
- `git-bundles\gitea\micha\*.bundle`
|
||||
|
||||
Bewusst **nicht** im Nearline-Scope:
|
||||
|
||||
- `unraid-flash-config.tar.gz` (hostseitig 0600 root:root; Restore-Quelle bleibt das Hetzner-Borg-Repo, siehe `docs/RESTORE_MATRIX.md` Tier 1 Unraid OS Flash).
|
||||
|
||||
## Schutzregeln
|
||||
|
||||
- Kein CIFS-/SMB-Hard-Mount von `H:/` auf Unraid.
|
||||
- Kein Borg-Repo direkt auf `H:/` ueber SMB.
|
||||
- Kein `/MIR` und kein automatisches Loeschen auf `H:/`.
|
||||
- Flash-Backup wie Secret behandeln; `H:/` bleibt lokaler Operator-Datentraeger.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Immich Restore Test
|
||||
|
||||
Status: **erfolgreich live verifiziert** (2026-05-27)
|
||||
Audit-Bezug: `docs/AUDIT_2026-05-25.md` Finding **F-11**
|
||||
|
||||
## Zweck
|
||||
|
||||
Schliesst die Audit-Luecke aus F-11: Immich ist der groesste Datentopf (Familien-Fotos), und bisher gibt es im Gegensatz zu Vaultwarden, Gitea und Paperless **keinen** verifizierten Mini-Restore-Test. Dieses Dokument verlinkt die Repo-Artefakte und beschreibt den Ablauf aus Operator-Sicht.
|
||||
|
||||
## Repo-Artefakte
|
||||
|
||||
| Datei | Zweck |
|
||||
|---|---|
|
||||
| `ops/restore-tests/immich-compose.test.yml` | isoliertes Test-Compose: pgvecto-rs Postgres + Redis + Immich-Server, ML weggelassen, `127.0.0.1:12283` |
|
||||
| `ops/restore-tests/immich-restore-test.sh` | Host-Bash-Skript fuer den Lauf, mit `--what-if` und `--keep-data` Flags |
|
||||
| `ops/restore-tests/immich-restore-test.ps1` | Plan-Scaffold fuer Windows-Operator-Sicht (kein Live-Run) |
|
||||
| `ops/restore-tests/immich-plan.md` | Fachlicher Plan: Quellen, Schutzregeln, Smoke-Test-Kriterien, bekannte Risiken |
|
||||
| `ops/restore-tests/immich-runbook.md` | Konkreter Operator-Ablauf, Fehlerfaelle, Schedule-Vorschlag |
|
||||
|
||||
## Was der Test abdeckt
|
||||
|
||||
- Extraktion von `local/borg-dumps/latest/immich.dump` aus dem aktuellsten Borg-Archiv
|
||||
- Import in eine isolierte `tensorchord/pgvecto-rs:pg14-v0.2.0` Postgres-Instanz mit demselben Digest wie Produktion
|
||||
- Start eines isolierten Immich-Server-Containers mit demselben Digest wie Produktion, **ohne** ML-Container und **ohne** Traefik
|
||||
- Smoke-Test: Login-Seite erreichbar, `asset`- und `"user"`-Tabelle lesbar
|
||||
- Markdown-Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md`
|
||||
- Bereinigung von Test-Container und Restore-Lab-Daten nach Erfolg
|
||||
|
||||
## Was der Test bewusst NICHT abdeckt
|
||||
|
||||
- Wiederherstellung produktiver Foto-Dateien (`/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`). Diese Pfade werden vom Test nicht angefasst und nicht in den Test-Container gemountet.
|
||||
- Machine-Learning-Container. Spart Image-Pull-Zeit und RAM; ML-Features sind im Smoke-Test irrelevant.
|
||||
- Echte Login-Flow per API. Smoke-Test prueft nur, dass Login-Seite ausgeliefert wird.
|
||||
- Asset-Rendering / Thumbnail-Generierung. Ohne Foto-Files erwartet.
|
||||
- Produktive Domain `immich.kaleschke.info`. Test laeuft ausschliesslich auf `127.0.0.1:12283`.
|
||||
|
||||
## Restore-Stufe
|
||||
|
||||
Der Test deckt **Stufe 4 (kritische Anwendungen)** aus `docs/DISASTER_RECOVERY.md` Phase 4 fuer Immich ab, allerdings nur DB-Ebene und UI-Smoke. Voll-Restore inklusive Foto-Dateien aus Borg ist eigener Folgeschritt; das Skript bereitet die Restore-Lab-Struktur dafuer vor.
|
||||
|
||||
## Erster Lauf und Preflight
|
||||
|
||||
| Pruefung | Verantwortlich | Wo |
|
||||
|---|---|---|
|
||||
| Dump-Groesse von `immich.dump` bestimmen | erledigt 2026-05-27 | 66M unter `/mnt/user/backups/borg/dumps/latest/immich.dump` |
|
||||
| Freier Platz unter `/mnt/user/backups/restore-lab/` | erledigt 2026-05-27 | ca. 3.7T frei auf `/mnt/user/backups` |
|
||||
| Borg-UI-Container laeuft | Operator | `docker ps | grep borg-ui` |
|
||||
| Trockenlauf mit `--what-if` | erledigt 2026-05-27 | Host-Clone auf `c5d231a`, `bash ops/restore-tests/run-restore-checks.sh immich --what-if` erfolgreich |
|
||||
| Erster echter Lauf | erledigt 2026-05-27 | Report `/mnt/user/backups/restore-reports/immich-2026-05-27.md`; Archiv `Tägliche-Sicherung-2026-05-27T04:30:06.778`; HTTP `200`; Assets `11977`; User `1` |
|
||||
|
||||
## Nach dem ersten erfolgreichen Lauf
|
||||
|
||||
1. Report unter `/mnt/user/backups/restore-reports/immich-2026-05-27.md` liegt vor und ist erfolgreich.
|
||||
2. `docs/RESTORE_MATRIX.md`, `ops/restore-tests/schedule.md`, `docs/AUDIT_2026-05-25_TODO.md` und `docs/MIGRATION_LOG.md` wurden nachgezogen.
|
||||
3. Quartalsweise Wiederholung einplanen; erster Live-Lauf bleibt bewusst manuell/Operator-kontrolliert, bis mehrere Laeufe stabil waren.
|
||||
|
||||
## Schutzregeln
|
||||
|
||||
- Skript greift ausschliesslich auf den Restore-Lab-Pfad und den Borg-Extract-Cache zu.
|
||||
- Produktive Pfade unter `/mnt/user/photos/*` und `/mnt/user/appdata/immich_postgres/` werden nicht angefasst.
|
||||
- Produktive Container `immich_server`, `immich_postgres`, `immich_redis`, `immich_machine_learning` werden nicht gestoppt, nicht beruehrt.
|
||||
- Borg-Passphrase wird aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` gelesen und nicht in Reports, Logs oder Doku geschrieben.
|
||||
- Test-Container publishen nur auf `127.0.0.1:12283`, nicht auf LAN- oder Tailscale-Interface.
|
||||
- Keine Traefik-Labels, keine Public-URL fuer Testcontainer.
|
||||
|
||||
## Risiken (aus `ops/restore-tests/immich-plan.md`)
|
||||
|
||||
- Dump-Groesse und erster `pg_restore`-Lauf sind gemessen: `immich.dump` 66M, echter Smoke-Test erfolgreich am 2026-05-27.
|
||||
- pgvecto-rs Extension-Mismatch bei Image-Drift moeglich; Compose pinnt denselben Digest wie Produktion.
|
||||
- Immich v2 meldet pgvecto-rs als deprecated; VectorChord-Migration ist ein kuenftiger Immich-Upgrade-Punkt, nicht Teil dieses Restore-Tests.
|
||||
- Immich-Server-Migrations koennen Startup nach Restore verzoegern; Skript pollt 120 s.
|
||||
- Bei Schema-Drift (z. B. nach Major-Update) koennen einzelne DB-Queries abweichen; das Skript versucht Immich-v2-Singular-Tabellen und aeltere Plural-Fallbacks.
|
||||
|
||||
## Naechste Operator-Schritte
|
||||
|
||||
1. Quartalsweise Wiederholung nach `ops/restore-tests/schedule.md` einplanen.
|
||||
2. Bei zukuenftigem Immich-Major-Upgrade den Restore-Test unmittelbar danach einmal manuell ausfuehren.
|
||||
3. Voll-Restore inklusive Foto-Dateien bleibt ein eigener, deutlich groesserer DR-Drill.
|
||||
@@ -17,6 +17,264 @@ Dieses Dokument ist nur noch ein historischer Verlauf. Der aktuelle operative Ab
|
||||
|
||||
## Historische Meilensteine
|
||||
|
||||
### 2026-05-30 - Komodo-Bootstrap-Trockenlauf Erstlauf (F-09 Rest abgeschlossen)
|
||||
|
||||
Skript ist seit 2026-05-29 vorbereitet, heute erster echter Lauf auf dem Host.
|
||||
|
||||
- Aufruf: `bash /mnt/user/services/homelab-infra/ops/restore-tests/komodo-bootstrap-test.sh --keep-data`
|
||||
- Vorlauf: `--what-if` zur Plan-Verifikation, danach echter Lauf, beides ohne Eingriff in den produktiven Komodo-Stack.
|
||||
- Ergebnis: `SUCCESS`, alle 5 Smoke-Checks gruen.
|
||||
- `docker compose config valid: ok`
|
||||
- `Test-Mongo healthy: ok` (Mongo healthy in ~6 s)
|
||||
- `Mongo authenticated ping (Test-Creds): ok`
|
||||
- `Komodo Core HTTP status: 200` (Login-Seite ausgeliefert)
|
||||
- `Test-Periphery container state: running`
|
||||
- Report: `/mnt/user/backups/restore-reports/komodo-bootstrap-2026-05-30.md`
|
||||
- Isolation hielt wie geplant: produktive Container `komodo-mongo`, `komodo-core`, `komodo-periphery` unter Project `komodo` blieben unangetastet, ebenso `/mnt/user/appdata/komodo/{mongo,core,periphery}` und die produktiven `KOMODO_*`-Secrets. Test lief unter Project `restoretest-komodo` mit Wegwerf-Datadir `/mnt/user/backups/restore-lab/komodo/`, Wegwerf-Secrets im Test-Compose und Test-Port nur auf `127.0.0.1:19120`.
|
||||
- Operator-Klick bewusst nicht von Claude uebernommen: `ssh root@kallilabcore` ist eine Aktionsklasse, die in CLAUDE.md ausdruecklich Operator-Anweisung verlangt. Der Auto-Mode-Classifier hat einen nicht-destruktiven SSH-Probe entsprechend blockiert. Der Operator hat den Befehl im Unraid-Webterminal selbst gestartet.
|
||||
- Bedeutung: `ops/komodo/docker-compose.yml` ist als Recovery-Anker fuer die Bootstrap-Stufen A-F in `docs/SERVICES_RECOVERY.md` jetzt **belegt** tauglich, nicht mehr nur angenommen tauglich. Image-Digests (mongo:7.0.32, komodo-core:2, komodo-periphery:2) und Mongo-Auth-Schema sind verifiziert.
|
||||
- Lab-Daten unter `/mnt/user/backups/restore-lab/komodo/` bleiben mit `--keep-data` erhalten, Test-Container wurden im EXIT-Trap sauber abgeraeumt. Operator entscheidet, ob das Lab-Verzeichnis (~300 MB) entfernt wird.
|
||||
|
||||
Folgeschritt fuer `docs/RESTORE_DRILL_ROUTINE.md`: Komodo-Bootstrap-Trockenlauf passt zum quartalsweisen DR-Sanity-Check (Q4) oder als wiederholbarer Standalone-Drill. Aktuell kein Host-Schedule, Aufruf bleibt manuell.
|
||||
|
||||
### 2026-05-30 - F-10: Authelia Repo<->Host Drift-Check
|
||||
|
||||
Der dokumentierte "by-design"-Drift zwischen `security/authelia/configuration.yml` (Repo-Baseline) und `/mnt/user/appdata/authelia/config/configuration.yml` (Host) wird jetzt automatisch ueberwacht. Vorher: Manueller Merge auf den Host war Pflicht, aber keine Pruefung. Eine vergessene ACL-Synchronisation waere erst bei einem Login-Fehler aufgefallen.
|
||||
|
||||
- Neues Skript `services/authelia-diff.sh`: extrahiert die `access_control:`-Sektion aus beiden YAMLs per awk-Block-Extractor (Top-Level-Key bis zum naechsten Top-Level-Key), normalisiert Kommentar- und Leerzeilen, vergleicht via `diff -u`. Default-Sektion ist `access_control`, weil das laut F-10 der primaere Drift-Vektor ist; per env `AUTHELIA_DIFF_SECTIONS` koennen weitere Top-Level-Sektionen (`session`, `regulation`, `totp`, ...) ergaenzt werden. OIDC-Clients, Identity-Provider und Secret-Werte bleiben bewusst aussen vor.
|
||||
- Exit-Code-Schema: 0 = ok, 1 = Drift (Diff auf stdout), 2 = Datei fehlt, 3 = Sektion fehlt, 4 = Werkzeug fehlt. Macht das Skript auch standalone nutzbar (`ssh kallilab "bash /mnt/user/services/homelab-infra/services/authelia-diff.sh"`).
|
||||
- `services/posture-check/posture-check.sh` ruft das Skript am Ende des Checks-Blocks auf (`check_authelia_config_drift`). Drift wird als **Warning** gemeldet, nicht Critical, weil die produktive Authelia trotz Drift weiter laeuft und die ACL fuer schon angemeldete Sessions weiter wirkt. Skip-Mechanismus: `SKIP_AUTHELIA_DRIFT=1`. Pfad-Override: `AUTHELIA_DIFF_SCRIPT`.
|
||||
- Pflicht-Setup auf dem Host: Repo-Spiegel unter `/mnt/user/services/homelab-infra/` als read-only-Clone von Gitea `Micha/homelab-infra` mit regelmaessigem `git pull --ff-only`. Default-Pfade des Skripts setzen das voraus. Ohne Repo-Spiegel meldet der Check Warning, weil die Baseline-Datei fehlt - keine stille Inaktivierung.
|
||||
- Lokaler Smoke-Test 2026-05-30 erfolgreich: identische Files -> rc=0; ACL-Drift im Domain-Eintrag `scrutiny.kaleschke.info -> scrutiny-renamed.kaleschke.info` -> rc=1 mit unified diff, ACL-Block korrekt extrahiert, Kommentar- und Leerzeilen rausgefiltert. False-Positive auf `session.default_redirection_url`-Aenderung korrekt vermieden (gehoert nicht zu `access_control`).
|
||||
- `docs/WORKFLOW.md` hat jetzt eine eigene Sektion "Ausnahme: Authelia configuration.yml" analog zur Traefik-Dynamic-Sektion. Pflicht-Workflow: 1. Repo-Aenderung + Commit + Push, 2. manueller Merge in die Host-Datei mit Erhalt der OIDC-Sektionen, 3. `docker restart authelia` + Login-Smoke-Test, 4. `services/authelia-diff.sh` muss `exit 0` liefern.
|
||||
- `docs/REPO_MAP.md` und `docs/SERVICE_CATALOG.md` zeigen das Skript und den neuen Posture-Check-Eintrag.
|
||||
|
||||
Operator-Folgeschritt (klein, nicht heute): Repo-Spiegel `/mnt/user/services/homelab-infra/` auf dem Host einrichten und in den vorhandenen `gitea-bundle-mirror-6h`-Plan oder einen eigenen 6h-Cron einbinden, damit das Skript einen aktuellen Vergleichsstand findet.
|
||||
|
||||
### 2026-05-29 - Stack-Hygiene Sprint: Healthchecks, Monitoring-Digests, Komodo-Bootstrap-Skript, Renovate-Vorbereitung
|
||||
|
||||
Vier Audit-Punkte am Stueck abgearbeitet. Pro Block: Live-Verifikation am Host, Doku im Repo.
|
||||
|
||||
**F-15 Tier-1 Healthchecks**
|
||||
|
||||
- 6 Tier-1-Stacks bekommen Healthchecks: postgresql17 (`pg_isready`), Redis (`redis-cli ping` mit Auth aus dem mount), Vaultwarden (`curl /alive`), Gitea (`wget /api/healthz`), Traefik (`traefik healthcheck --ping`, vorher `--ping=true` in CLI aktiviert), Authelia (`wget /api/health` - Authelia v4.39 hat `helper health-check` entfernt, daher direkter Endpoint).
|
||||
- Erste Iteration in Vaultwarden + Authelia schlug fehl: Vaultwarden hat kein `wget`, Authelia kennt das `helper`-Subcommand nicht mehr. Probe per `docker exec` zeigte: Vaultwarden hat `curl`, Authelia hat `wget`. Compose entsprechend nachgezogen, zweiter Lauf gruen.
|
||||
- Komodo-Stack-Workspaces fuer `postgresql17` (124 commits behind) und `gitea` (52 commits behind) wurden Komodo-seitig nicht automatisch gepullt. Manuell ueber `git pull --ff-only` plus `cp` der aktuellen Compose-Datei aus dem Host-Repo-Clone in den Stack-Workspace synchronisiert, dann `docker compose up -d`. Gitea-Workspace hatte zusaetzlich untracked Doku-Files; nur die im aktuellen Master tracked-en Files entfernt, nicht via `git clean -fd`. Workspace-Drift selbst ist nicht heute Auftrag, aber als Folge-Befund notiert.
|
||||
- Endstand Live: alle 6 Healthchecks `healthy`.
|
||||
|
||||
**F-07 Monitoring-Stack Digest-Pinning**
|
||||
|
||||
- 9 Container in `monitoring/docker-compose.yml` per Tag@sha256 gepinnt (prometheus, alertmanager, alertmanager-ntfy-bridge, blackbox-exporter, loki, promtail, grafana, node-exporter, cadvisor; plus zweiter python:3.13-alpine im Bootstrap-Dashboard-Importer). InfluxDB war bereits gepinnt.
|
||||
- Digests aus den laufenden Containern per `docker inspect ... .Config.Image` + `docker image inspect ... .RepoDigests` ausgelesen, damit die Pins exakt dem Live-Stand entsprechen.
|
||||
- Kein Recreate ausgeloest, weil die Images identisch sind; nur die Compose-Datei ist jetzt reproduzierbar wie die Tier-1-Stateful-Stacks.
|
||||
|
||||
**F-09 Rest - Komodo-Bootstrap-Trockenlauf-Skript**
|
||||
|
||||
- `ops/restore-tests/komodo-bootstrap-{compose.test.yml,test.sh,plan.md,runbook.md}` analog zum Immich-Restore-Test-Muster angelegt.
|
||||
- Test-Compose nutzt dieselben Image-Digests wie Produktion (mongo:7.0.32, komodo-core:2, komodo-periphery:2), isoliert unter Compose-Project `restoretest-komodo`, Test-Port nur `127.0.0.1:19120`, **Test-Periphery ohne docker.sock-Mount und ohne `/mnt/user/services`-Mount** - kann produktive Container nicht managen.
|
||||
- Wegwerf-Secrets sind im Compose hardcodiert, produktive `KOMODO_*`-Werte werden nicht beruehrt.
|
||||
- Smoke-Test-Kriterien: docker compose config valid, Mongo healthy, Mongo Auth-Ping ok, Core HTTP 200/302/303/401, Periphery container `running`.
|
||||
- Erster Lauf bleibt manueller Operator-Schritt.
|
||||
|
||||
**F-12 Renovate-Bot (live)**
|
||||
|
||||
- Repo-Config in `renovate.json` (Repo-Root): nur extends, packageRules, ignorePaths, manager file patterns, labels, rangeStrategy. Bot-Config separat in `ops/renovate/bot-config.js`: platform, endpoint, autodiscover=false, repositories=["Micha/homelab-infra"], gitAuthor, Concurrent-Limits. Trennung war noetig: Renovate liest die `renovate.json` im Repo als REPO-Config; Bot-Felder darin wurden als "this repo is disabled" fehlinterpretiert (Repository result: forbidden, status: disabled).
|
||||
- `ops/renovate/run-renovate.sh` als One-Shot-Container-Wrapper. Wichtige Haertungen waehrend des Setups:
|
||||
- `--add-host git.kaleschke.info:192.168.178.58`: Renovate-Container kann den Hostname sonst nicht aufloesen (`EAI_AGAIN`). Analog zur `extra_hosts`-Loesung in der Komodo-Compose.
|
||||
- `--env-file` statt `-e RENOVATE_TOKEN=...`: Token war sonst in `ps` und `docker inspect` sichtbar.
|
||||
- `chmod 0777` auf `/mnt/user/services/renovate/state`: Renovate-Image laeuft als uid 12021 (ubuntu), kann root-owned Mount sonst nicht beschreiben.
|
||||
- Live-Setup am Host:
|
||||
- Service-Account `renovate` (uid 2, **kein Admin**) ueber `gitea admin user create` angelegt.
|
||||
- Collaborator-Status mit Write-Permission auf `homelab-infra` (initialer DB-Insert hat den Gitea-Permissions-Cache nicht aktualisiert; Renovate sah `permissions.push=false` und brach mit "Repository does not permit pull or push" ab; saubere Loesung war Operator-UI-Klick "Entfernen + neu hinzufuegen", was den Cache konsistent aktualisiert; Befund-Bestaetigung via Doku-Studium `lib/modules/platform/gitea/index.ts`: die Push-Check ist hardcoded, kein Bypass moeglich).
|
||||
- Personal-Access-Token mit Scopes `read:user,write:repository,write:issue`, in `/mnt/user/appdata/secrets/renovate_token.txt` (chmod 600). Token wurde einmal rotiert, weil der Wert beim ersten Erzeugen im SSH-Output sichtbar war.
|
||||
- User-Script `renovate-six-hourly` mit Cron `20 */6 * * *` live in `/etc/cron.d/root`.
|
||||
- Erstlauf 2026-05-29 erfolgreich: 5 PRs (mongo digest, mongo 7.0.32->7.0.34, postgres digest, postgres 17.9->17.10, minor-and-patch-updates gruppiert), 1 Issue "Renovate Dependency Dashboard", 8 Branches (drei Major-Branches warten auf naechsten Lauf wegen prConcurrentLimit=5). Komodo-Major-Updates wurden korrekt durch packageRule unterdrueckt.
|
||||
- `docs/RENOVATE.md` zeigt die ursprueglichen 5 Operator-Schritte fuer Neuaufsetzen bzw. Disaster Recovery.
|
||||
|
||||
### 2026-05-29 - Borg-Source `/local/appdata/homepage` verspaetet entfernt + Removal-Checkliste in WORKFLOW
|
||||
|
||||
- Befund aus den ersten Tagen scharfer Alert-Pipeline: `HomelabBorgLastJobCompletedWithWarnings` firing fuer die letzten vier Borg-Laeufe (26.05.-29.05.), jeweils Exit-Code 107.
|
||||
- Ursache im Borg-UI-Logfile `backup_job_39_*.log`: `"/local/appdata/homepage: stat: [Errno 2] No such file or directory"`. Borg-UI-Source-Liste enthielt seit der Homepage-Entfernung am 25.05. weiterhin den Eintrag `/local/appdata/homepage`; der Appdata-Pfad war aber bereits nach `_archive/homepage-removed-2026-05-25/` verschoben.
|
||||
- Operator hat den Eintrag in der Borg-UI manuell entfernt; Source-Liste jetzt 23 statt 24 Eintraege, `homepage` nicht mehr drin. Naechster Borg-Lauf 2026-05-30 04:30 sollte wieder `completed` ohne Warning sein.
|
||||
- Backups waren nicht gefaehrdet: trotz Exit-Code 107 wurden alle anderen 23 Quellen sauber archiviert (Stats Job 39: 100.895 Dateien, 26.72 GB Original, 317 MB deduplicated).
|
||||
- Erkenntnis: bei Stack-Removal wurde die Borg-Source-Liste damals nicht mit-aufgeraeumt. **`docs/WORKFLOW.md`** um neuen Abschnitt "Service-Removal-Checkliste" erweitert, der die Borg-UI-Source-Bereinigung explizit als Pflichtschritt 8 nennt (zusammen mit allen anderen Schritten wie Komodo-Destroy, Gitea-Webhook, Authelia-ACL, Blackbox-Target, Doku).
|
||||
- Positiv-Befund: die ntfy-Push-Pipeline (Cron `*/5` Textfile-Export -> node-exporter -> Prometheus -> Alertmanager -> ntfy-Bridge), die am 2026-05-27 scharfgeschaltet wurde, hat den Drift binnen 24 h sichtbar gemacht. Das ist der intendierte Mechanismus.
|
||||
|
||||
### 2026-05-28 - H:/-Pull als Windows Scheduled Task aktiviert
|
||||
|
||||
- Task `KalliLab H Drive Nearline Pull` registriert auf dem Operator-Windows-PC: taeglich 05:30 (nach dem Borg-Dump-Fenster um ca. 04:00), `RunLevel Limited`, `AllowStartIfOnBatteries`, `DontStopIfGoingOnBatteries`, `StartWhenAvailable`, `ExecutionTimeLimit 2 h`. Naechster Lauf 2026-05-29 05:30.
|
||||
- Repo-Doku `docs/H_DRIVE_NEARLINE_PULL.md` Status auf "produktiv" gesetzt, Register-Snippet auf den tatsaechlich ausgefuehrten Befehl korrigiert (PowerShell-Enum `LeastPrivilege` -> `Limited`; alter Snippet haette beim ersten Aufruf einen Parameter-Binding-Fehler geworfen).
|
||||
- Verifikation am Windows-PC: `Get-ScheduledTask` zeigt State `Ready`, Trigger-Start 2026-05-28T05:30, RunLevel `Limited`.
|
||||
- Kein Eingriff am Host noetig; SMB-Quelle und H:/-Ziel waren bereits vorbereitet.
|
||||
|
||||
### 2026-05-28 - Zweites Off-site bewusst nicht umgesetzt
|
||||
|
||||
- Operator-Bewertung: 3-2-1-Regel ist mit aktueller Topologie erfuellt (Live + lokales Borg-Repo + Hetzner-Borg + H:/-Nearline = 4 Kopien / 3 Medien / 1 Off-site).
|
||||
- Ein zweites Off-site wuerde **ausschliesslich** das Szenario "Hetzner-Account verloren" zusaetzlich abdecken. Eintrittswahrscheinlichkeit niedrig (etablierter deutscher Anbieter, dokumentierter Zahlungsweg). Aufwand und Kosten unverhaeltnismaessig fuer Familien-Homelab.
|
||||
- Beschluss in `docs/OFFSITE_BACKUP_OPTIONS.md` mit Review-Triggern dokumentiert; F-03 in `docs/AUDIT_2026-05-25_TODO.md` von "offen" auf "erledigt (bewusst nicht umgesetzt)".
|
||||
- Stattdessen drei Folge-TODOs zur Haertung der bestehenden Topologie:
|
||||
- Hetzner-Account-Hygiene: starkes Passwort + Backup-Zahlungsweg + Login-Benachrichtigungen. **Bewusst keine 2FA** (Operator-Entscheidung analog USV-Risiko-Akzeptanz).
|
||||
- Borg `--append-only` auf Hetzner pruefen. Befund: Repo `appdata-critical` laeuft aktuell im Mode `full`, `custom_flags` leer. Setup waere server-seitig in Hetzner-`authorized_keys`.
|
||||
- H:/-Pull als Windows Scheduled Task aktivieren (Skript und Doku ready, Erstlauf 2026-05-27 erfolgreich, Task selbst noch nicht angelegt).
|
||||
- Bewusst NICHT angefasst: laufende Backup-Pipeline (kein Test-Restore, kein Modus-Wechsel ohne Folge-Sprint).
|
||||
|
||||
### 2026-05-28 - paperless-gpt und BentoPDF bewusst behalten
|
||||
|
||||
- Befund: Beide Container laufen Up 3 days, aber **0 Traefik-Zugriffe in den letzten 7 Tagen** und kein User-LLM-Event in den paperless-gpt-Logs. BentoPDF-Logs zeigen ausschliesslich Docker-Healthchecks.
|
||||
- Resource-Footprint vernachlaessigbar: `paperless-gpt` 34 MB RAM, `bentopdf` 4 MB RAM, beide 0 % CPU.
|
||||
- Operator-Entscheidung 2026-05-28 (gegen die Nicht-Nutzung): **beide behalten**.
|
||||
- `paperless-gpt`: bleibt aktiv bis Paperless-NGX 3.0 verfuegbar ist. Paperless 3.0 wird native KI-Features mitbringen; danach neu entscheiden, ob `paperless-gpt` noch noetig ist oder abgeloest werden kann.
|
||||
- `bentopdf`: bleibt aktiv als situatives PDF-Werkzeug; Footprint zu klein, um eine harte Entfernung zu rechtfertigen.
|
||||
- Doku-Anker in `docs/SERVICE_CATALOG.md` ergaenzt, damit die Frage in 6 Monaten nicht erneut als "warum laeuft das?" auftaucht.
|
||||
|
||||
### 2026-05-28 - Plex Server Reclaim und Remote Access deaktiviert
|
||||
|
||||
- Befund beim Versuch, Remote Access in der Plex-UI zu deaktivieren: Plex-Server war seit 18.05.2026 13:18 nicht mehr mit einem Plex.tv-Account geclaimt. `Preferences.xml` 391 Bytes, ohne `PlexOnlineMail`/`PlexOnlineUsername`/`PlexOnlineToken`. Login als `Xeridos` lieferte "Keine Berechtigung" auf den lokalen Server. Zusaetzlich waren die `library_sections` leer (Backups vom 19./22./28.05. ebenfalls ~370 KB statt MBs); die Bibliotheks-Konfiguration war seit dem 18.05. weg. Filmdateien unter `/mnt/user/media/*` blieben unangetastet (833 Verzeichnisse, ~1.7 TB).
|
||||
- Reclaim als `Xeridos` durchgefuehrt: Operator-Token via `plex.tv/claim` erzeugt, am Host als Shell-Inline-ENV beim `docker compose up -d --force-recreate plex` mitgegeben. Token wurde **nicht** in `.env`, **nicht** in Compose, **nicht** in Komodo-Stack-ENV geschrieben. Nach Erfolg sauberer zweiter Recreate ohne Token, damit `docker inspect`-Snapshot keinen Token mehr enthaelt. Bash-History defensiv geleert.
|
||||
- Endstand laut `Preferences.xml`: `PlexOnlineUsername="Xeridos"`, `PlexOnlineMail="michideheld@gmx.de"`, `PlexOnlineHome="1"`, `PublishServerOnPlexOnlineKey="0"` (Remote Access aus).
|
||||
- Operator hat im Anschluss die Bibliotheken neu angelegt (`/data/movies`, `/data/Heimatfilme`) und Remote Access in der Plex-UI auf "deaktiviert" gesetzt; Metadata-Cache wuchs in den ersten 18 Minuten auf 630 MB.
|
||||
- Plex bleibt damit strikt LAN/Tailscale-only, konsistent zur FRITZ!Box-Bereinigung vom selben Tag. Smart-TVs (Schlaf-/Wohnzimmer) finden den Server ueber WLAN-LAN per mDNS/Plex-GDM unveraendert.
|
||||
- `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13 enthaelt die ausfuehrliche Recovery-Geschichte. `docs/SERVICE_CATALOG.md` und Sektion 7.4 auf "LAN/Tailscale-only, Remote Access aus" praezisiert.
|
||||
|
||||
### 2026-05-28 - FRITZ!Box-Portfreigaben bereinigt
|
||||
|
||||
- WAN-Soll auf eine einzige Freigabe reduziert: `443/tcp -> 192.168.178.58:443` (Traefik HTTPS).
|
||||
- `80/tcp` aus FRITZ!Box-UI entfernt. Validierung: Mobilfunk-Test ergibt Timeout auf `http://vault.kaleschke.info`, `https://vault.kaleschke.info` weiter erreichbar; lokal greift Traefik-Redirect 80->443 nach wie vor. Cloudflare-DNS-Challenge braucht kein Port 80.
|
||||
- `222/tcp` bleibt bewusst nicht eingerichtet. Begruendung: Tailscale ist Operator-Pfad, GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` deckt Repo-Bootstrap-Pfad ab, Gitea-Bundles unter `/mnt/user/backups/git-bundles/gitea` decken Offline-Restore ab. `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 10 entsprechend mit "Tailscale-only, bewusst nicht WAN-freigegeben" praezisiert.
|
||||
- UPnP-Selbstfreigabe-Recht fuer `PC-192-168-178-71` (Hostname `VONETS.COM`, MAC `00:17:13:2F:61:96`) deaktiviert. Identifiziert als VONETS-WiFi-Bridge, vermutlich Bridge-Anbindung zum SolarEdge-Wechselrichter. SolarEdge-Cloud-Sync ist outbound und benoetigt keine UPnP. Aktuell waren 0 Selbstfreigaben aktiv; die Aenderung ist praeventiv gegen kuenftige Anforderungen.
|
||||
- `docs/NETWORK_INVENTORY.md`, `docs/FRITZBOX_PORT_CORRECTION_PLAN.md` und `docs/AUDIT_2026-05-25_TODO.md` Sprint 4 entsprechend nachgezogen.
|
||||
- Bewusst NICHT angefasst: FRITZ!OS 8.21 Update (Service-Fenster), IPv6-Exposure (separater Folgeschritt), WAN-Ausfallschutz (bewusst aus).
|
||||
|
||||
### 2026-05-27 - Monitoring-Alerts live, Gitea-Bundle-Cron live, H:/-Pull live
|
||||
|
||||
Drei Audit-TODOs gleichzeitig auf "erledigt" gezogen; alle Aenderungen mit Host-Smoke verifiziert.
|
||||
|
||||
**Monitoring-Alerts (Borg-Stale / Cert-Expiry / Container-Down)**
|
||||
|
||||
- Auf dem Host neuer User-Script `export-prometheus-textfile-5min` mit Cron `*/5 * * * *` angelegt. Schreibt `/mnt/user/services/posture-check/textfile/homelab.prom`.
|
||||
- Repo: `services/posture-check/export-prometheus-textfile.sh` setzt jetzt vor dem `mv` per `chmod 644`, damit node-exporter (`nobody:65534`) lesen kann. Vorher `0600 root:root` → `node_textfile_scrape_error 1`.
|
||||
- `monitoring-prometheus` wurde einzeln per `docker restart` neu gestartet, um den `stale file handle` auf der gebundenen `alerts.yml` zu loesen. Kein Stack-Down. `promtool check rules` SUCCESS 14 rules, `lastConfigTime 2026-05-27T18:33:06Z`. Aktive Alerts: 1 firing (`HomelabTraefik5xx` aus dem 2026-05-20-Befund), 1 pending (`HomelabBorgLastJobCompletedWithWarnings` durch `completed_with_warnings`-Status des letzten Borg-Laufs).
|
||||
- Pipeline end-to-end: Textfile-Skript ⇒ node-exporter Textfile-Collector ⇒ Prometheus ⇒ alerts.yml-Regeln.
|
||||
|
||||
**Gitea-Bundle-Schedule**
|
||||
|
||||
- User-Script `gitea-bundle-mirror-6h` mit Cron `10 */6 * * *` (00:10/06:10/12:10/18:10).
|
||||
- Repo: `ops/borg-ui/scripts/gitea-bundle-mirror.sh` schreibt Bundles und Sidecars jetzt `chmod 644` statt `600`. Begruendung: Bundle-Inhalt ist Git-Historie ohne Secrets (durch `.gitignore` abgedeckt), nicht sensibler als die uebrigen 0644-Dumps. Damit funktioniert der Nearline-Pull ueber SMB.
|
||||
- Existierende Bundles wurden manuell auf 0644 angehoben. Erster Cron-Lauf 2026-05-27 18:41 UTC erfolgreich: 4 Bundles, Checksums OK.
|
||||
|
||||
**H:/ Nearline-Pull**
|
||||
|
||||
- Erster scharfer Lauf 2026-05-27 20:25 zeigte vier Permission-Befunde: `unraid-flash-config.*` (4 Files, by-design 0600), `filebrowser.bolt.dump` (0640, Source erbt), und alle 10 Gitea-Bundle-Files (0600).
|
||||
- Repo: `ops/h-drive-nearline/pull-critical-backups.ps1` excluded jetzt die `unraid-flash-config.*`-Familie ueber `/XF` (Restore-Quelle bleibt Hetzner-Borg). `ops/borg-ui/scripts/pre-backup-dumps.sh` setzt alle Dumps via `atomic_write` per Default auf 0644, Flash-Config-Familie explizit mit `atomic_write target tmp 600`.
|
||||
- Existierende Files am Host nachtraeglich auf 0644 angehoben.
|
||||
- Zweiter Lauf 2026-05-27 20:45: beide Robocopy-Jobs Exit 1, **19 Borg-Dumps + 10 Gitea-Bundle-Files** unter `H:\kallilab-nearline-backups`. Report-Tabellen-Quirk in PowerShell durch `& robocopy @args | Out-Null` behoben (Live-Output landete vorher in `$results`).
|
||||
- `docs/H_DRIVE_NEARLINE_PULL.md` mit Erstlauf-Befund, gefixter Erwartungs-Liste und expliziter Out-of-Scope-Anmerkung fuer Flash-Config aktualisiert.
|
||||
- Windows Scheduled Task taeglich 05:30 bleibt **bewusst** offen bis zur Operator-Bestaetigung.
|
||||
|
||||
**Was bewusst NICHT angefasst wurde**
|
||||
|
||||
- Authelia/OIDC/CrowdSec/Nextcloud-Haertung (geparkt).
|
||||
- Hermes (Review 2026-07-25).
|
||||
- USV (Risiko bewusst akzeptiert).
|
||||
- FRITZ!Box-/Plex-/UI-Punkte (Punkt 5 fuer morgen frueh).
|
||||
- `unraid-flash-config.tar.gz` bleibt bewusst 0600 und ausserhalb des Nearline-Scopes.
|
||||
|
||||
### 2026-05-27 - Vorbereitungsdokumente FRITZ!Box-Korrektur und Off-site-Optionen
|
||||
|
||||
- Reine Doku-Aenderung; kein Router-, Provider- oder Host-Eingriff.
|
||||
- `docs/FRITZBOX_PORT_CORRECTION_PLAN.md` neu angelegt: Korrektur-Plan fuer drei Punkte (`80/tcp` entfernen, `222/tcp` nicht ergaenzen solange Tailscale stabil, UPnP-Selbstfreigabe `PC-192-168-178-71` deaktivieren). Operator-Go ausstehend; jede UI-Aenderung wird gesondert dokumentiert.
|
||||
- `docs/OFFSITE_BACKUP_OPTIONS.md` neu angelegt: Entscheidungsvorlage fuer zweites Off-site-Ziel mit drei Optionen (rsync.net Borg-Plan, BorgBase EU2, rotierende Cold-Platte). Bewertung gegen Provider-Trennung, Standort, Preis und Konto-Risiko; Empfehlung rsync.net oder Cold-Platte; BorgBase EU2 explizit nicht empfohlen wegen gleichem Anbieter. Kein Provider gebucht, keine Kosten ausgeloest.
|
||||
- `docs/REPO_MAP.md` um beide neuen Dokumente ergaenzt.
|
||||
- `docs/AUDIT_2026-05-25_TODO.md` Sprint 4 (FRITZ!Box) und Sprint 7 (Off-site) mit Verweis auf die neuen Vorbereitungsdokumente nachgezogen.
|
||||
|
||||
### 2026-05-27 - Doku-Sprint Bootstrap / Family-View / Onboarding / Drill-Routine
|
||||
|
||||
- Reine Doku-Aenderung; kein Compose-, Secret-, Host- oder Router-Eingriff.
|
||||
- `docs/SERVICES_RECOVERY.md` "Komodo Bootstrap" auf linearen Stufenpfad A-F ausgebaut: Recovery-Anker bleibt `ops/komodo/docker-compose.yml`, Self-Stack ist explizit kein Anker, Secret-Restore-Reihenfolge verweist auf `docs/SECRETS_MAP.md` Stack-ENV-only-Sektion, Validierungs-Kommandos ergaenzt. Trockenlauf-Idee als Folgeaufgabe eingetragen, kein Repo-Skript dafuer.
|
||||
- `docs/DISASTER_RECOVERY.md` Phase 4 Stufe 3 verweist explizit auf den Bootstrap-Pfad in `docs/SERVICES_RECOVERY.md` und auf die Stack-ENV-only-Sektion in `docs/SECRETS_MAP.md`.
|
||||
- `docs/FAMILY_VIEW_DASHBOARD.md` neu angelegt: Spezifikation fuer `homelab-family-view`-Dashboard, 8 Panels (Endpoints up, Borg-Frische, Cert-Tage, Kritische Container, Disk-Fuellung, Endpoint-Tabelle, Cert-Tabelle, Container-Tabelle), PromQL-Queries, Thresholds, Build-Reihenfolge. Bewusst noch **kein** `monitoring/grafana/dashboards/family-view.json` angelegt, weil Borg-Stale-/Cert-Expiry-/Container-Down-Metriken laut Sprint 3 noch "in Arbeit" sind.
|
||||
- `docs/FAMILY_ONBOARDING.md` final redigiert: Status auf "Final-Stand vor Wochenend-Einladung", 2FA-Beschreibung auf App-eigene 2FA praezisiert (kein SSO-Versprechen, weil Authelia-OIDC weiter geparkt ist), neuer "Bewusst nicht versprochen"-Block (kein Einheits-Login, kein 24/7-SLA, kein Hotline-Support, keine Datenweitergabe).
|
||||
- `docs/RESTORE_DRILL_ROUTINE.md` neu angelegt: Drei-Stufen-Modell (Freshness woechentlich / Mini-Restore monatlich-bimonatlich / DR-Sanity quartalsweise), Quartals-Kadenz Q1-Q4 mit Dienst-Rotation, bestaetigte Mini-Restores (Vaultwarden, Gitea, Paperless 2026-05-07; Immich 2026-05-27), 10-Punkte-Sanity-Check, Abbruch-Regel mit Verweis auf `docs/GITOPS_DRIFT_RUNBOOK.md`. Kein Host-Schedule angelegt, nur Doku.
|
||||
- `ops/restore-tests/schedule.md` verweist jetzt auf `docs/RESTORE_DRILL_ROUTINE.md` als Quelle der Quartals-Belegung.
|
||||
- `docs/REPO_MAP.md` um `docs/FAMILY_VIEW_DASHBOARD.md`, `docs/RESTORE_DRILL_ROUTINE.md` und `docs/IMMICH_RESTORE_TEST.md` ergaenzt.
|
||||
- `docs/AUDIT_2026-05-25_TODO.md` aktualisiert: Sprint 2 "Komodo-Bootstrap-Pfad beschreiben", Sprint 3 "Family-View Dashboard definieren", Sprint 4 "Familien-Onboarding schreiben" und Sprint 7 "Restore-Lab-Drill quartalsweise dokumentieren" jeweils auf "erledigt".
|
||||
- Geparkte Punkte bleiben unveraendert: Authelia-2FA/OIDC/CrowdSec, Nextcloud-Haertung, Hermes (Review 2026-07-25), USV-Anschaffung.
|
||||
|
||||
### 2026-05-27 - Immich Restore-Smoke-Test praktisch verifiziert (F-11)
|
||||
|
||||
- Erster echter Immich-Restore-Smoke-Test gegen das produktive Borg-Archiv erfolgreich: `Tägliche-Sicherung-2026-05-27T04:30:06.778`, Report `/mnt/user/backups/restore-reports/immich-2026-05-27.md`.
|
||||
- Validiert wurden Borg-Extract von `local/borg-dumps/latest/immich.dump`, Import in isolierten `tensorchord/pgvecto-rs:pg14-v0.2.0` Test-Postgres, Start des Immich-Servers ohne ML und ohne Traefik, HTTP `200` auf `127.0.0.1:12283`, Login-Marker, `11977` Assets und `1` User im Test-DB-Check.
|
||||
- Produktive Container und produktive Foto-Pfade wurden nicht angefasst; Testdaten und Testcontainer wurden nach Erfolg bereinigt.
|
||||
- Im Lauf wurden Restore-Test-Haertungen umgesetzt: Borg-`known_hosts` aus `/data/known_hosts` wird fuer SSH-Trust genutzt, `completed_with_warnings`-Archive gelten als verwendbare Restore-Quelle, Postgres-Startfenster werden retry-faehig behandelt, Immich-v2-Upload-Marker werden im leeren Test-Mount erzeugt und Smoke-Checks schlagen bei HTTP-/Marker-Fehlern hart fehl.
|
||||
- `docs/IMMICH_RESTORE_TEST.md`, `docs/RESTORE_MATRIX.md`, `ops/restore-tests/schedule.md` und `docs/AUDIT_2026-05-25_TODO.md` nachgezogen; F-11 ist damit abgeschlossen. Voll-Restore inklusive Foto-Dateien bleibt ein separater DR-Drill.
|
||||
|
||||
### 2026-05-27 - FRITZ!Box-Portfreigaben gegen Repo-Soll abgeglichen
|
||||
|
||||
- FRITZ!Box-UI `Internet -> Freigaben -> Kallilabcore` geprueft: aktiv sind `HTTP-Server` TCP `80/tcp` und `HTTPS-Server` TCP `443/tcp` auf `192.168.178.58`.
|
||||
- Repo-Soll aus `docs/NETWORK_INVENTORY.md` ist nur `443/tcp` plus optional gewolltes Gitea-SSH `222/tcp`. Der aktuelle Zustand weicht ab: `80/tcp` ist offen, `222/tcp` fehlt.
|
||||
- Kallilabcore ist nicht als Exposed Host markiert und erlaubt keine selbststaendige Portfreigabe. `PC-192-168-178-71` erlaubt selbststaendige Portfreigabe, hat aber `0 aktiv`.
|
||||
- Keine FRITZ!Box-Aenderung vorgenommen. Router-Korrektur bleibt ein produktiver Operator-Schritt nach ausdruecklicher Freigabe.
|
||||
|
||||
### 2026-05-26 - Immich Restore-Smoke-Test vorbereitet (F-11)
|
||||
|
||||
- `docs/IMMICH_RESTORE_TEST.md` und `ops/restore-tests/immich-plan.md`/`immich-runbook.md` beschreiben den geplanten Immich-Mini-Restore: `immich.dump` aus Borg, isolierter pgvecto-rs-Test-Postgres, Test-Redis, Immich-Server ohne ML, lokaler Port `127.0.0.1:12283`, keine produktiven Foto-Mounts.
|
||||
- `ops/restore-tests/immich-restore-test.sh`, `immich-restore-test.ps1` und `immich-compose.test.yml` wurden vorbereitet; der Dispatcher kennt `immich --what-if`.
|
||||
- Lokal verifiziert: Bash-Syntax, `run-restore-checks.sh immich --what-if`, PowerShell-Dispatcher `-Mode immich -WhatIf`, Docker-Compose-Render und Policy-Check. Kein echter Host-Restore, kein Borg-Extract, kein Produktiv-Container-Eingriff.
|
||||
- Host-Preflight 2026-05-27: Host-Clone per Fast-forward auf `c5d231a`, `immich.dump` 66M, `/mnt/user/backups` ca. 3.7T frei, `run-restore-checks.sh immich --what-if` erfolgreich. Kein echter Restore-Lauf.
|
||||
- F-11 bleibt fachlich offen bis zum ersten Host-Lauf mit Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md`.
|
||||
|
||||
### 2026-05-27 - H:/ Nearline-Pull vorbereitet
|
||||
|
||||
- `docs/H_DRIVE_NEARLINE_PULL.md` und `ops/h-drive-nearline/pull-critical-backups.ps1` definieren den Windows-seitigen Pull von `\\192.168.178.58\backups\borg\dumps\latest` und `\\192.168.178.58\backups\git-bundles\gitea` nach `H:\kallilab-nearline-backups`.
|
||||
- SMB-Quelle `\\192.168.178.58\backups` und H:/ sind erreichbar; `-WhatIf` prueft den Plan ohne Kopie.
|
||||
- Kein Scheduled Task angelegt und kein echter Kopierlauf gestartet. Empfohlen ist taeglich 05:30 nach dem Borg-Dump-Fenster, Aktivierung erst nach Operator-Sichtpruefung.
|
||||
|
||||
### 2026-05-27 - Storage Layout als Active v1.4 gefuehrt
|
||||
|
||||
- `docs/STORAGE_LAYOUT.draft.md` wurde zu `docs/STORAGE_LAYOUT.md` umbenannt. Das Dokument war inhaltlich bereits als Active markiert; Version 1.4 entfernt den formalen Draft-Blocker.
|
||||
- Physikalische Disk-Werte aus `docs/HARDWARE_INVENTORY.md` und Host-Readout uebernommen: Cache Samsung 970 EVO Plus 1.8T XFS, Disk1 WDC WD60EFAX 5.5T XFS auf `md1p1`, Parity TOSHIBA HDWG480 7.3T, Boot Samsung Flash Drive 59.8G FAT32, H:/ als Nearline-Ziel.
|
||||
- `docs/AUDIT_2026-05-25_TODO.md` Sprint 2 fuer Storage-Layout und Disk-/Share-Baseline auf erledigt gesetzt. Retention-Kalibrierung, Monitoring-Schwellen und RESTORE_MATRIX-Detailklassifikation bleiben normale Folgeaufgaben.
|
||||
|
||||
### 2026-05-27 - F-08 Alert-Regeln vorbereitet
|
||||
|
||||
- `services/posture-check/export-prometheus-textfile.sh` erzeugt Textfile-Metriken fuer Borg-Backup-Frische und kritische Container unter `/mnt/user/services/posture-check/textfile/homelab.prom`.
|
||||
- `monitoring/docker-compose.yml` aktiviert den Node-Exporter-Textfile-Collector. `monitoring/prometheus/alerts.yml` enthaelt vorbereitete Alerts fuer Borg-Stale, Borg-Fehlerstatus, Borg-Warnstatus, Textfile-Stale, Critical-Container-Down und TLS-Cert-Expiry 21/7 Tage.
|
||||
- Host-Smoke 2026-05-27: Skript erzeugt `homelab.prom`, alle gelisteten kritischen Container melden `1`, Borg-Status ist `completed_with_warnings` und wird als Warning statt Critical modelliert.
|
||||
- Kein Monitoring-Redeploy und kein Scheduled Task in diesem Schritt. Abschluss erfolgt nach Host-Schedule, Prometheus-Reload und Testalert.
|
||||
|
||||
### 2026-05-26 - Audit F-16 und F-20 abgeschlossen (Doku-only)
|
||||
|
||||
- F-16: `infra/redis`-Etikett auf die Realitaet abgeglichen. `docs/SERVICE_CATALOG.md`, `docs/REPO_MAP.md`, `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13 und `docs/DISASTER_RECOVERY.md` Bootstrap-Stufe 2 beschreiben Redis jetzt als "primaer Paperless-Redis (App-Cache); historisch als shared angelegt, faktisch nur von Paperless genutzt". Immich, Nextcloud, Mealie eigene Redis-Instanzen; Authelia bewusst ohne Redis. Keine Compose-Aenderung.
|
||||
- F-20: Restore-Wege fuer Stack-ENV-only Secrets explizit gemacht. Neuer Abschnitt `6.2.1 Restore-Quellen fuer Stack-ENV-Werte` in `docs/DISASTER_RECOVERY.md` (Reihenfolge Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz, Komodo-Sonderfall, Paperless als Hauptanwendung). Neuer Abschnitt `Stack-ENV-only Secrets - Restore-Wege` in `docs/SECRETS_MAP.md` mit Tabelle je Stack (Paperless, Immich, Mail-Archiver, Speedtest, Komodo, Hermes, Glance). Glance-Widget-Tokens explizit als rebuildbar markiert. Konkrete Werte werden nirgendwo dokumentiert.
|
||||
- `docs/AUDIT_2026-05-25_TODO.md` Sprint 6 entsprechend auf "erledigt" gestellt.
|
||||
- Kein Eingriff in `ops/borg-ui/scripts/gitea-bundle-mirror.sh`, kein Gitea-Bundle-Trockenlauf, keine SSH-/Host-Pruefung.
|
||||
|
||||
### 2026-05-26 - FRITZ!Box-/H:/-/Family-Onboarding-Doku-Update
|
||||
|
||||
- `docs/NETWORK_INVENTORY.md` mit FRITZ!Box-Baseline gefuellt: FRITZ!Box 7590, FRITZ!OS 8.21 (Update gemeldet, nicht eingespielt), Telekom DSL ~87/36 Mbit/s, 36 aktive Heimnetz-Geraete, LAN 1-4 verbunden, WLAN `Fritzi`, Gast-WLAN inaktiv, Telefonie/DECT aktiv, Ausfallschutz nicht eingerichtet, USB nicht verbunden, 2 Portfreigaben aktiv. Soll fuer Portfreigaben: nur `443/tcp` und `222/tcp` auf `192.168.178.58`.
|
||||
- `docs/EXTERNAL_DEPENDENCIES.md` um Telekom-DSL und FRITZ!Box 7590 als WAN-/Router-Abhaengigkeit erweitert; Ausfall-Szenario "Telekom-DSL / FRITZ!Box gestoert" ergaenzt.
|
||||
- `docs/CAPACITY_AND_LIFECYCLE.md` um Abschnitt "H:/ als zusaetzliches lokales Backup-Ziel" ergaenzt. Bewertung: H:/ ist als zweite lokale Nearline-Kopie und Freeze-Sicherung sinnvoll, aber bewusst **kein** Offsite-Ersatz und **kein** CIFS-Hard-Mount am Unraid (STORAGE_LAYOUT §12.6). Pull-Modell vom Windows-PC bleibt der getestete Weg (vgl. Disk1 Phase 2 Freeze 2026-05-25).
|
||||
- `docs/FAMILY_ONBOARDING.md` von Tabellen-Entwurf auf familienverstaendlichen Begruessungstext umgestellt: kurze App-Erklaerungen, konkrete Was-tun-Wenn-Anleitungen (Webseite weg, Passwort vergessen, 2FA verloren, Foto-Backup haengt, Browser-Warnung), "Was du nicht musst"-Block, Hinweis fuer geplante Wochenend-Einladung.
|
||||
- `docs/AUDIT_2026-05-25_TODO.md` Leitplanken aktualisiert: Authelia 2FA/OIDC/CrowdSec und Nextcloud-2FA-Haertung werden in diesem Zyklus nicht angefasst (Operator-Vorgabe). Hermes-Agent geparkt mit Review-Deadline 2026-07-25. USV-Risiko bewusst akzeptiert. Sprint 4 um H:/-Bewertung und FRITZ!Box-Portfreigaben-Abgleich erweitert. Neue Sprints 6 (geparkte Apps) und 7 (Off-site) ergaenzt.
|
||||
- Keine Live-/Compose-Aenderung in diesem Commit; nur Doku.
|
||||
|
||||
### 2026-05-26 - USV-Risiko bewusst akzeptiert
|
||||
|
||||
- Operator-Entscheidung: aktuell wird keine USV angeschafft.
|
||||
- Der Befund bleibt technisch unveraendert: keine funktionierende USV-Abschaltung nachgewiesen. Power-Loss-Risiko fuer Docker-/DB-State und laufende Writes wird bewusst akzeptiert und bei spaeteren Reviews neu bewertet.
|
||||
|
||||
### 2026-05-26 - Borg-Passphrase offline gesichert
|
||||
|
||||
- Operator bestaetigt: Die Borg-Passphrase ist offline/off-system gesichert und kann ohne Host oder Vaultwarden wiederhergestellt werden.
|
||||
- Doku aktualisiert nur den Sicherungsstatus; Secret-Wert und Ablageort bleiben bewusst ausserhalb des Repos.
|
||||
|
||||
### 2026-05-26 - Gitea-Bundle-Mechanik definiert
|
||||
|
||||
- `ops/borg-ui/scripts/gitea-bundle-mirror.sh` ergaenzt: erstellt verifizierte `git bundle`-Artefakte fuer alle bare Gitea-Repositories, schreibt Checksums und einen Markdown-Report.
|
||||
- Zielpfad ist `/mnt/user/backups/git-bundles/gitea`; dieser Pfad muss in den Borg/off-site Scope aufgenommen und hostseitig geplant werden.
|
||||
- `docs/SERVICES_RECOVERY.md` und `docs/RESTORE_MATRIX.md` dokumentieren Bundles jetzt als zweite Repo-Bootstrap-Schicht neben dem GitHub-Mirror.
|
||||
- Host-Erstlauf nach Skript-Fix erfolgreich: 4 Bundles erzeugt (`homelab-infra`, `homelab`, `homepage`, `smart-home-kalli`), Checksums OK, `homelab-infra.bundle` in Restore-Lab geklont und `git fsck` sauber. Offen bleibt die dauerhafte Schedule-Einbindung.
|
||||
|
||||
### 2026-05-26 - Audit-Baseline-Tag gesetzt
|
||||
|
||||
- Der Stand nach Hardware-/Capacity-Baseline, Policy-Triage und Recovery-Doku wurde als `audit-2026-05-25-baseline` markiert und nach Gitea gepusht.
|
||||
|
||||
+71
-19
@@ -1,7 +1,7 @@
|
||||
# Network Inventory - KalliLab CORE
|
||||
|
||||
Status: Initialer Host-Audit erfasst, Router-/VLAN-Details offen.
|
||||
Letzte Pruefung: 2026-05-26
|
||||
Status: Host-Audit erfasst; Router-Baseline und Portfreigaben-UI geprueft; VLAN/IPv6-Details offen.
|
||||
Letzte Pruefung: 2026-05-27
|
||||
|
||||
## Zweck
|
||||
|
||||
@@ -11,14 +11,26 @@ Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennun
|
||||
|
||||
| Feld | Wert |
|
||||
|---|---|
|
||||
| Anschluss / Provider | TBD |
|
||||
| Router-Modell | TBD |
|
||||
| Firmware | TBD |
|
||||
| Anschluss / Provider | DSL, Telekom |
|
||||
| Bandbreite (FRITZ!Box-UI) | ca. 87,3 Mbit/s Download, ca. 36 Mbit/s Upload |
|
||||
| Router-Modell | FRITZ!Box 7590 |
|
||||
| Firmware | FRITZ!OS 8.21 (Update gemeldet, nicht eingespielt) |
|
||||
| Router-IP | 192.168.178.1 |
|
||||
| DHCP-Server | vermutlich Router, zu pruefen |
|
||||
| DHCP-Server | FRITZ!Box (Standardannahme, Override durch Operator nicht dokumentiert) |
|
||||
| Lokales Subnetz | 192.168.178.0/24 |
|
||||
| IPv6 aktiv | TBD |
|
||||
| DynDNS / DDNS | Cloudflare via `ddns-updater`, Details TBD |
|
||||
| IPv6 aktiv | TBD (FRITZ!Box-UI separat pruefen) |
|
||||
| DynDNS / DDNS | Cloudflare via `ddns-updater` (kein FRITZ!Box-DynDNS in Nutzung) |
|
||||
| Heimnetz-Geraete (FRITZ!Box-UI) | 36 aktive Geraete |
|
||||
| LAN-Ports belegt | LAN 1-4 verbunden |
|
||||
| Telefonie / DECT | aktiv |
|
||||
| USB an FRITZ!Box | nicht verbunden |
|
||||
| Ausfallschutz (FRITZ!Box) | nicht eingerichtet (Mobilfunk-Stick-Failover nicht aktiv) |
|
||||
|
||||
### Beobachtungen
|
||||
|
||||
- Telekom-DSL ist Single-WAN; ohne Ausfallschutz ist Internet-Ausfall = kein DDNS-Update, keine ACME-Erneuerung, keine externen Push-Quellen.
|
||||
- Upload 36 Mbit/s ist die effektive Obergrenze fuer Off-site-Backup-Geschwindigkeit nach Hetzner und fuer Plex-Remote-Streaming.
|
||||
- FRITZ!OS 8.21 hat ein angezeigtes Update; Einspielung ist Betreiber-Aufgabe und nicht Teil des Repos.
|
||||
|
||||
## DNS
|
||||
|
||||
@@ -50,13 +62,49 @@ tailscale ip -6
|
||||
|
||||
## Portfreigaben und Exposure
|
||||
|
||||
### FRITZ!Box (WAN -> Host)
|
||||
|
||||
Aktiver Soll-Stand nach Operator-Bereinigung 2026-05-28:
|
||||
|
||||
| Aktive Freigabe | Ziel | Zweck | Bemerkung |
|
||||
|---|---|---|---|
|
||||
| `443/tcp` -> `192.168.178.58:443` | Traefik HTTPS | einziger Public-HTTPS-Einstieg, Wildcard-Cert via Cloudflare-DNS-Challenge | bleibt |
|
||||
|
||||
Bewusst **nicht** freigegeben:
|
||||
|
||||
| Port | Begruendung |
|
||||
|---|---|
|
||||
| `80/tcp` | Cloudflare-DNS-Challenge ersetzt HTTP-01; Traefik macht HTTP->HTTPS-Redirect nur LAN-seitig; WAN-`80` waere zusaetzliche Angriffsflaeche ohne Funktionsnutzen. **2026-05-28 in FRITZ!Box-UI entfernt**, Validierung: Mobilfunk-Test ergibt Timeout auf `http://vault.kaleschke.info`, `https://...` weiter erreichbar. |
|
||||
| `222/tcp` (Gitea SSH) | bewusst Tailscale-only: Operator-Pfad ist Tailscale, GitHub-Mirror deckt DR-Bootstrap ab, Gitea-Bundles sind off-host. Externe SSH-Brute-Force-Vektoren vermeiden. |
|
||||
|
||||
### UPnP / Selbstständige Portfreigaben
|
||||
|
||||
| Geraet | UPnP-Selbstfreigabe-Recht | Begruendung |
|
||||
|---|---|---|
|
||||
| `Kallilabcore` (192.168.178.58) | nicht erlaubt | Repo-managed; alle benoetigten Public-Ports sind explizite Freigaben |
|
||||
| `PC-192-168-178-71` / VONETS-Adapter (192.168.178.71, MAC 00:17:13:2F:61:96) | **2026-05-28 deaktiviert** | wahrscheinlich VONETS-WiFi-Bridge fuer SolarEdge-Wechselrichter; SolarEdge-Cloud-Sync ist ausschliesslich outbound, eingehende Ports sind nicht erforderlich |
|
||||
|
||||
Sollten neue Geraete UPnP-Selbstfreigaben anfordern, wird das in `docs/MIGRATION_LOG.md` und hier als bewusste Ausnahme dokumentiert oder pro Geraet wieder deaktiviert.
|
||||
|
||||
Aktueller UI-Befund vom 2026-05-27 (`Internet -> Freigaben -> Kallilabcore`):
|
||||
|
||||
| Beobachtung | Bewertung |
|
||||
|---|---|
|
||||
| `HTTP-Server`, TCP, extern `80/tcp` auf `192.168.178.58` | Abweichung: fuer ACME/DNS-Challenge nicht erforderlich; sollte nach Operator-Freigabe entfernt werden, falls keine bewusste HTTP-WAN-Nutzung besteht |
|
||||
| `HTTPS-Server`, TCP, extern `443/tcp` auf `192.168.178.58` | entspricht Repo-Soll |
|
||||
| Keine `222/tcp`-Freigabe sichtbar | Abweichung: Gitea-SSH ist extern nicht gemaess Soll erreichbar; nur anlegen, wenn Git-SSH aus dem Internet weiterhin gewuenscht ist |
|
||||
| Kallilabcore: keine selbststaendige Portfreigabe, kein IPv4-/IPv6-Exposed-Host sichtbar | entspricht Sicherheitsziel |
|
||||
| `PC-192-168-178-71`: selbststaendige Portfreigabe erlaubt, `0 aktiv` | keine aktive Freigabe, aber UPnP/PCP-Erlaubnis sollte bei Gelegenheit deaktiviert werden |
|
||||
|
||||
### Host (lokal beobachtbar)
|
||||
|
||||
| Port | Ziel | Zweck | Bewertung |
|
||||
|---:|---|---|---|
|
||||
| 80/tcp | Traefik | HTTP->HTTPS / ACME | erwartet |
|
||||
| 443/tcp | Traefik | HTTPS | erwartet |
|
||||
| 222/tcp | Gitea SSH | Git SSH | dokumentierte Ausnahme |
|
||||
| 53/tcp+udp | AdGuard | DNS | dokumentierte Ausnahme |
|
||||
| 8082/tcp | AdGuard Admin | Admin UI | Repo-Soll: nur `100.80.98.33:8082`, DNS-Port 53 unveraendert |
|
||||
| 80/tcp | Traefik | HTTP->HTTPS / ACME | nur LAN, keine WAN-Freigabe noetig |
|
||||
| 443/tcp | Traefik | HTTPS | WAN-Freigabe in FRITZ!Box erwartet |
|
||||
| 222/tcp | Gitea SSH | Git SSH | WAN-Freigabe in FRITZ!Box erwartet, dokumentierte Ausnahme |
|
||||
| 53/tcp+udp | AdGuard | DNS | LAN-only, dokumentierte Ausnahme |
|
||||
| 8082/tcp | AdGuard Admin | Admin UI | Bind nur `100.80.98.33:8082` (Tailscale), nicht im LAN exponiert |
|
||||
| 8181/tcp | InfluxDB 3 Core | LAN Writer fuer Home Assistant | LAN-only, Bind-IP pruefen |
|
||||
|
||||
Pruefkommando:
|
||||
@@ -70,11 +118,12 @@ docker ps --format "{{.Names}}: {{.Ports}}" | sort
|
||||
|
||||
| Netz | Status | Bemerkung |
|
||||
|---|---|---|
|
||||
| LAN | 192.168.178.0/24 | Hauptnetz, Host `192.168.178.58` |
|
||||
| Gast-WLAN | TBD | Zugriff auf AdGuard Admin muss ausgeschlossen sein |
|
||||
| IoT-Netz | TBD | Zugriff auf AdGuard Admin muss ausgeschlossen sein |
|
||||
| LAN | 192.168.178.0/24 | Hauptnetz, Host `192.168.178.58`, FRITZ!Box meldet 36 aktive Geraete |
|
||||
| WLAN 2,4 / 5 GHz | aktiv, SSID `Fritzi` | Standard-WLAN, im LAN-Adressbereich, kein eigener Adressraum |
|
||||
| Gast-WLAN | **inaktiv** (FRITZ!Box-UI) | Solange inaktiv: kein Gast-Pfad zu LAN-Diensten; AdGuard-Admin-Trennung primaer ueber Tailscale-Bind statt Netzsegmentierung |
|
||||
| IoT-Netz | nicht existent | Keine VLAN-Trennung dokumentiert |
|
||||
| Tailscale | aktiv | Operator-Zugang, Host-IP `100.80.98.33` |
|
||||
| VLANs | TBD | Router-/Switch-Faehigkeit pruefen |
|
||||
| VLANs | nicht in Nutzung | FRITZ!Box 7590 kann VLAN-Tagging an einzelnen LAN-Ports; aktuell nicht konfiguriert |
|
||||
|
||||
## Docker-Netze
|
||||
|
||||
@@ -101,6 +150,9 @@ docker network inspect backend_net | jq '.[0].Internal'
|
||||
| Thema | Status | Naechster Schritt |
|
||||
|---|---|---|
|
||||
| AdGuard Admin nur via Tailscale | live validiert 2026-05-26 | Compose bindet Admin-Port auf `100.80.98.33:8082`; DNS auf Port 53 funktioniert, LAN-Zugriff auf `192.168.178.58:8082` schlaegt fehl |
|
||||
| Gast-/IoT-Zugriff auf Admin-Ports | offen | Router-Regeln pruefen |
|
||||
| IPv6 Exposure | offen | Router und Traefik/Cloudflare pruefen |
|
||||
| FRITZ!Box-Portfreigaben mit Repo-Soll abgleichen | **erledigt 2026-05-28** | Bereinigt: `80/tcp` entfernt (Cloudflare-DNS-Challenge ersetzt HTTP-01; Mobilfunk-Test bestaetigt Timeout auf `http://`, `https://` weiter ok). `222/tcp` bleibt bewusst nicht eingerichtet (Tailscale-only-Linie). UPnP-Selbstfreigabe-Recht fuer VONETS-Bridge (SolarEdge) deaktiviert. Aktiver Soll-Stand: ausschliesslich `443/tcp -> 192.168.178.58`. |
|
||||
| FRITZ!OS 8.21 Update | gemeldet | Operator-Aufgabe; vor Update kurzes Service-Fenster planen, weil Reboot WAN/Tailscale-Aufbau unterbricht |
|
||||
| Gast-/IoT-Zugriff auf Admin-Ports | aktuell entschaerft | Gast-WLAN ist inaktiv; bei Aktivierung muessen `192.168.178.58:8082`, `192.168.178.58:8181` und ggf. weitere LAN-Ports per FRITZ!Box-Kindersicherung/Netzwerk-Filter abgesichert werden |
|
||||
| IPv6 Exposure | offen | Router und Traefik/Cloudflare pruefen; Telekom-DSL liefert in der Regel IPv6, FRITZ!Box-Standard-Verhalten klaeren |
|
||||
| WAN-Ausfallschutz | bewusst nicht eingerichtet | Mobilfunk-Stick-Failover an FRITZ!Box ist nicht aktiv; Internet-Ausfall = ACME/DDNS pausieren, lokale Apps laufen weiter |
|
||||
| Home Assistant InfluxDB Bind | offen | Effektive Listener-Adresse pruefen |
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
# Zweites Offsite-Backup-Ziel - Entscheidungsvorlage
|
||||
|
||||
Status: **Operator-Entscheidung 2026-05-28: bewusst KEIN zweites Off-site.** Doku bleibt als Begruendungs-Anker und fuer zukuenftige Reviews.
|
||||
Audit-Bezug: `docs/AUDIT_2026-05-25.md` Finding **F-03** und `docs/AUDIT_2026-05-25_TODO.md` Sprint 7.
|
||||
|
||||
## Beschluss 2026-05-28
|
||||
|
||||
Aktuelle Backup-Topologie:
|
||||
|
||||
| Kopie | Wo | Medium | Standort |
|
||||
|---|---|---|---|
|
||||
| 1 | Live-Daten Unraid (Cache + Disk1) | NVMe + HDD | Heim |
|
||||
| 2 | Borg-Repo lokal `/mnt/user/backups/borg/` | HDD (Disk1) | Heim |
|
||||
| 3 | Borg-Repo Hetzner Storagebox | Hetzner | Off-site (DE) |
|
||||
| 4 | H:/ Nearline-Pull am Windows-PC | externe HDD | Heim |
|
||||
|
||||
**3-2-1-Regel ist erfuellt:** 4 Kopien, 3 Medien, 1 Off-site (Hetzner).
|
||||
|
||||
Ein zweites Off-site wuerde **ausschliesslich** das Szenario "Hetzner-Account verloren" zusaetzlich abdecken. Operator-Bewertung: Wahrscheinlichkeit niedrig (etablierter deutscher Anbieter mit Zahlungsweg), Aufwand und laufende Kosten unverhaeltnismaessig zur Risikoreduktion fuer ein privates Familien-Homelab.
|
||||
|
||||
Statt zweitem Off-site werden drei Hetzner-Account-Haertungen als Folge-TODOs gefuehrt:
|
||||
|
||||
1. Hetzner-Account: starkes, einzigartiges Passwort in Vaultwarden + Backup-Zahlungsweg (zweite Karte/SEPA) + Login-Benachrichtigungen per E-Mail. **Bewusst keine 2FA** (Operator-Entscheidung 2026-05-28 analog zur USV-Risiko-Akzeptanz; 2FA-Aufwand ueberwiegt die fuer ein Familien-Homelab realistische Risiko-Reduktion).
|
||||
2. Borg `--append-only`-Mode pruefen (Repo `appdata-critical` laeuft aktuell im Mode `full`). Setup erfolgt server-seitig in Hetzner `~/.ssh/authorized_keys` mit `command="borg serve --append-only"` fuer den Backup-Key. Schuetzt gegen client-seitige Ransomware, die das Repo loeschen wuerde.
|
||||
3. H:/ Pull als Windows Scheduled Task aktivieren (Anker `docs/H_DRIVE_NEARLINE_PULL.md`).
|
||||
|
||||
Diese drei Massnahmen zusammen schliessen den gleichen Risikoraum wie ein zweites Off-site, ohne neue Provider/Hardware/laufende Kosten.
|
||||
|
||||
## Review-Trigger
|
||||
|
||||
Diese Entscheidung wird neu bewertet, wenn:
|
||||
|
||||
- Hetzner-Account-Probleme tatsaechlich auftreten (Payment-Issue, Login-Sperre)
|
||||
- Hetzner als Anbieter strukturelle Probleme zeigt (Insolvenz-Geruechte, AGB-Aenderungen)
|
||||
- Im Homelab Daten mit deutlich hoeherem Wiederbeschaffungs-Aufwand dazukommen (z. B. Firefly-III-Finanz-Daten, Smart-Home-Langzeitdaten)
|
||||
- Operator-Praeferenzen sich aendern
|
||||
|
||||
Wenn dieser Trigger eintritt, sind die drei Optionen rsync.net / BorgBase EU2 / Cold-Wechselplatte weiter unten dokumentiert.
|
||||
|
||||
---
|
||||
|
||||
## Zweck
|
||||
|
||||
Aktuell laeuft das Off-site-Backup bei genau einem Anbieter (**Hetzner Storage Box**). Diese Vorlage stellt drei realistische Optionen fuer ein **zweites** Off-site-Ziel gegenueber. Die Entscheidung trifft der Operator; dieses Dokument trifft sie nicht.
|
||||
|
||||
`H:/` am Windows-PC bleibt **kein** Offsite-Ersatz (siehe `docs/CAPACITY_AND_LIFECYCLE.md`). H:/ ist Nearline-Kopie am gleichen Standort.
|
||||
|
||||
## Anforderungen an das zweite Ziel
|
||||
|
||||
| Anforderung | Begruendung |
|
||||
|---|---|
|
||||
| **anderer Anbieter als Hetzner** | Provider-Risiko (Account-Sperre, Insolvenz, Region-Outage) wird sonst nicht reduziert |
|
||||
| **anderer physischer Standort** als Unraid-Host | Brand-/Diebstahl-/Wasser-Schutz |
|
||||
| **Borg-kompatibel** (SSH + Repo-Layout) | bestehendes Borg-UI-Setup soll wiederverwendbar bleiben |
|
||||
| **bezahlbar im Privatrahmen** | grobe Zielgroesse: < 10 EUR / Monat fuer ~1 TB |
|
||||
| **stabil ueber Monate** | wenig Eingriff, keine taeglichen Quirks |
|
||||
| **keine Konto-Komplexitaet** | 2FA-Recovery muss sauber, ohne Telefon-Nummer-Hacks etc. moeglich sein |
|
||||
|
||||
## Drei Optionen
|
||||
|
||||
### Option A - rsync.net Borg-Plan
|
||||
|
||||
- **Was:** seit Jahren etablierter Anbieter mit dediziertem Borg-Plan (eingebaute `borg`-Binary, SSH-Account, Snapshot-Schutz).
|
||||
- **Zielanbindung:** `borg init --encryption=repokey-blake2 user@hostname:repo`.
|
||||
- **Standort:** Schweiz/USA (je nach Konto-Region).
|
||||
- **Preis (Stand 2026, grob):** ~10-15 USD pro Monat fuer 1 TB; Mindestbestelldauer keine.
|
||||
- **Vorteile:** Borg-First, ZFS-Snapshots als zusaetzlicher Schutz vor Repo-Loeschung, sehr lange Track-Record, keine SMB-/CIFS-Quirks.
|
||||
- **Nachteile:** USD-Preis, Standort ausserhalb EU je nach Konto, etwas teurer als reine Storage Boxes.
|
||||
- **Konto-Risiko:** unabhaengig von Hetzner-Konto.
|
||||
|
||||
### Option B - BorgBase EU2 (zweite Region beim gleichen Borg-Spezialisten)
|
||||
|
||||
- **Was:** BorgBase ist Borg-as-a-Service mit mehreren Regionen.
|
||||
- **Zielanbindung:** identisch zu bestehendem Borg-UI-Pfad.
|
||||
- **Standort:** zweite EU-Region als EU1.
|
||||
- **Preis (Stand 2026, grob):** ~10 EUR / Monat fuer ~1 TB.
|
||||
- **Vorteile:** identisches Borg-Modell, geringer Lernaufwand, einfache Web-UI.
|
||||
- **Nachteile:** Anbieter-Risiko nicht vollstaendig getrennt (gleicher Anbieter, andere Region). Bei Account-Sperre/Insolvenz waeren beide Ziele gleichzeitig betroffen.
|
||||
- **Konto-Risiko:** **gleicher** Anbieter, **anderes** Geo-Risiko.
|
||||
|
||||
### Option C - Rotierende Cold-Wechselplatte ausser Haus
|
||||
|
||||
- **Was:** zweite externe HDD (z. B. 2x WD Elements 4 TB), die im monatlichen Wechsel zwischen Heim-LAN und vertrauter dritter Person (Familie, Schliessfach, Buero) rotiert.
|
||||
- **Zielanbindung:** Borg-Repo lokal auf der eingesteckten Platte; Backup-Lauf nur wenn die Platte gerade vor Ort ist.
|
||||
- **Standort:** echtes Ausserhaus, dazwischen offline (Air-Gap).
|
||||
- **Preis (einmalig):** zwei Platten ~250 EUR, keine laufenden Kosten.
|
||||
- **Vorteile:** echtes Air-Gap, keine Provider-Abhaengigkeit, keine Bandbreitenfrage, keine Konto-Risiken.
|
||||
- **Nachteile:** manuelle Disziplin noetig, Recovery-Zeit haengt davon ab, dass die Platte gerade erreichbar ist. Nicht so taggenau wie Cloud-Borg.
|
||||
- **Konto-Risiko:** keines (keine Provider-Bindung).
|
||||
|
||||
## Bewertung gegen die Anforderungen
|
||||
|
||||
| Anforderung | Hetzner (Ist) | Option A rsync.net | Option B BorgBase EU2 | Option C Cold-Platte |
|
||||
|---|---|---|---|---|
|
||||
| Anderer Anbieter | - | ja | nein (gleicher) | ja (keiner) |
|
||||
| Anderer Standort | - | ja | ja | ja |
|
||||
| Borg-kompatibel | ja | ja | ja | ja |
|
||||
| Preis < 10 EUR/Monat | ja | grenzwertig | ja | einmalig ~250 EUR |
|
||||
| Stabilitaet | hoch | hoch | hoch | Operator-Disziplin abhaengig |
|
||||
| 2FA/Konto-Recovery | OK | OK | OK | n/a |
|
||||
|
||||
## Empfehlung (nicht Entscheidung)
|
||||
|
||||
Wenn der Operator die geringste Bedienung bei maximalem Provider-getrennten Schutz will: **Option A rsync.net**. Echte Anbieter-Trennung gegenueber Hetzner, Borg-First-Anbieter, ZFS-Snapshot-Schutz, keine zusaetzliche Hardware noetig.
|
||||
|
||||
Wenn Air-Gap und Null-Provider-Abhaengigkeit am wichtigsten sind und der Operator die monatliche Rotation tatsaechlich diszipliniert macht: **Option C Cold-Platte**.
|
||||
|
||||
**Option B (BorgBase EU2) wird nicht empfohlen** als zweites Ziel, weil das das Provider-Risiko nicht reduziert.
|
||||
|
||||
## Was vor einer Buchung zu tun ist
|
||||
|
||||
1. Operator-Entscheidung Option A vs. C dokumentieren (kein Provider-Kontakt vor Entscheidung).
|
||||
2. Falls Option A:
|
||||
- Konto bei rsync.net anlegen, Borg-Plan waehlen.
|
||||
- SSH-Key vom Borg-UI-Container exportieren bzw. dediziertes Key-Pair erzeugen.
|
||||
- `borg init` gegen das neue Repo durchfuehren (separate Passphrase oder gleiche - bewusst entscheiden).
|
||||
- Borg-UI um zweites Repository erweitern (separater Eintrag, kein Replace).
|
||||
- Schedule pruefen: erstes Vollbackup als One-Shot, danach inkrementell.
|
||||
- `docs/SECRETS_MAP.md` und `docs/EXTERNAL_DEPENDENCIES.md` um neuen Provider ergaenzen.
|
||||
- `docs/RESTORE_MATRIX.md` und `docs/STORAGE_LAYOUT.md` Backup-Ziel-Liste aktualisieren.
|
||||
3. Falls Option C:
|
||||
- zwei Platten beschaffen, Filesystem XFS oder ext4 (kein NTFS).
|
||||
- Rotations-Plan in `docs/STORAGE_LAYOUT.md` §8.1 ergaenzen.
|
||||
- Borg-Repo-Init pro Platte; Borg-UI um lokales Repo erweitern (nur aktiv wenn Platte eingesteckt).
|
||||
- Operator-Disziplin: monatliche Rotation als Kalender-Reminder.
|
||||
|
||||
## Was bewusst NICHT in dieser Vorlage steht
|
||||
|
||||
- Konkrete Hetzner-Konto-Daten, rsync.net-Accountnamen, IBANs, Telefonnummern. Diese Daten gehoeren nirgendwo in dieses Repo.
|
||||
- Borg-Passphrasen. Bleiben ausserhalb des Repos.
|
||||
- "Migrieren weg von Hetzner" als Option. Hetzner bleibt; das zweite Ziel ist Ergaenzung, nicht Ablosung.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
| Status | Punkt | Naechster Schritt |
|
||||
|---|---|---|
|
||||
| offen | Entscheidung Option A vs. C | Operator |
|
||||
| offen | Budget-Freigabe | Operator |
|
||||
| offen | Provider-/Hardware-Beschaffung | nach Entscheidung |
|
||||
| offen | Schedule-Anpassung in `ops/borg-ui` | nach Provider-Bereitstellung |
|
||||
@@ -7,7 +7,7 @@ Zweck: Startpunkt fuer einen neuen Chat, ohne das komplette Repo erneut zu lesen
|
||||
- Incident: NTFS-Cache-Vorfall ab 2026-05-11.
|
||||
- Host: Unraid `Kallilabcore`, SSH `root@192.168.178.58`.
|
||||
- Root Cause: Cache war NTFS/ntfs3; Disk1 ist noch NTFS/ntfs3 und wird spaeter separat migriert.
|
||||
- Recovery-Prinzip: `docs/STORAGE_LAYOUT.draft.md` ist fuer diesen Restore bindend, obwohl die Datei noch `.draft` heisst.
|
||||
- Recovery-Prinzip: `docs/STORAGE_LAYOUT.md` ist bindend. Zum Zeitpunkt dieses Handoffs hiess die Datei noch `docs/STORAGE_LAYOUT.draft.md`.
|
||||
- Keine Stacks starten, wenn ein Pfad/Setting gegen Storage Layout, Restore Matrix oder Architecture Master verstoesst.
|
||||
|
||||
## Host-Zustand
|
||||
@@ -86,4 +86,4 @@ Verifikation:
|
||||
|
||||
## Startprompt fuer neuen Chat
|
||||
|
||||
Lies zuerst `docs/RECOVERY_HANDOFF_2026-05-15.md`, dann `docs/STORAGE_LAYOUT.draft.md`, `docs/RESTORE_MATRIX.md` und nur die Compose-Dateien des naechsten betroffenen Stacks. Fuehre den KalliLab-CORE-Restore token-sparend fort. Nichts erfinden, keine Container starten, wenn etwas gegen Storage Layout verstoesst. Backrest und WD MyBookLive Duo sind entfernt und duerfen nicht wieder ins Setup.
|
||||
Lies zuerst `docs/RECOVERY_HANDOFF_2026-05-15.md`, dann `docs/STORAGE_LAYOUT.md`, `docs/RESTORE_MATRIX.md` und nur die Compose-Dateien des naechsten betroffenen Stacks. Fuehre den KalliLab-CORE-Restore token-sparend fort. Nichts erfinden, keine Container starten, wenn etwas gegen Storage Layout verstoesst. Backrest und WD MyBookLive Duo sind entfernt und duerfen nicht wieder ins Setup.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Renovate Bot - Self-hosted gegen Gitea
|
||||
|
||||
Status: **vorbereitet 2026-05-29**; PAT-Setup und Cron-Aktivierung sind Operator-Schritte.
|
||||
Audit-Bezug: `docs/AUDIT_2026-05-25.md` Finding **F-12**.
|
||||
|
||||
## Zweck
|
||||
|
||||
Wir pinnen Image-Versionen und Digests konsequent (siehe `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13, "Reproduzierbare Deployments"). Das macht das Setup stabil, aber jede Image-Aktualisierung musste bisher manuell laufen. Renovate uebernimmt das in Zukunft: scant das Repo periodisch, oeffnet Pull-Requests in Gitea fuer Image-/Digest-Updates, gruppiert sinnvoll, laesst Operator entscheiden.
|
||||
|
||||
Bewusst kein Auto-Merge: jede PR braucht eine Operator-Sichtpruefung und einen Merge-Click. Komodo deployt danach automatisch ueber den Standard-Webhook-Pfad.
|
||||
|
||||
## Architektur
|
||||
|
||||
- **Image:** `renovate/renovate:41` (versioniert, kein latest)
|
||||
- **Lauf:** ein-shot pro Cron-Tick, danach beendet sich der Container; persistente State liegt in `/mnt/user/services/renovate/state/`
|
||||
- **Schedule:** alle 6 Stunden (User-Script `renovate-six-hourly`)
|
||||
- **Plattform:** Gitea via `https://git.kaleschke.info/api/v1`
|
||||
- **Authentifizierung:** Gitea-PAT als Host-Secret-Datei
|
||||
- **Konfiguration:** `renovate.json` im Repo-Root
|
||||
|
||||
## Operator-Setup (einmalig, ~10 Minuten)
|
||||
|
||||
### Schritt 1 - Service-Account in Gitea
|
||||
|
||||
1. Als Admin in Gitea einloggen.
|
||||
2. Neuen User anlegen:
|
||||
- Username: `renovate`
|
||||
- E-Mail: ein gueltiges Postfach (Renovate sendet keine Mails, aber Gitea braucht eine Adresse)
|
||||
- Passwort: zufaellig, in Vaultwarden speichern
|
||||
3. Diesem User Schreibrechte fuer das Repo geben, das Renovate scannen soll: Repo `homelab-infra` -> Einstellungen -> Mitarbeiter -> `renovate` mit Permission `Schreibrechte` hinzufuegen.
|
||||
|
||||
**Wichtig:** Den Collaborator immer ueber die Gitea-UI/API hinzufuegen, nicht ueber direkten SQL-Insert. Die UI/API loest einen Permissions-Cache-Refresh aus; ein DB-Insert tut das nicht und fuehrt dazu, dass Renovate spaeter "Repository does not permit pull or push" meldet, obwohl die DB den Write-Mode kennt (Befund am 2026-05-29).
|
||||
|
||||
### Schritt 2 - Access-Token erzeugen
|
||||
|
||||
1. Als `renovate`-User in Gitea einloggen.
|
||||
2. Profile -> Settings -> Applications -> Generate New Token.
|
||||
3. Token-Name: `renovate-bot`.
|
||||
4. Scopes:
|
||||
- `read:user`
|
||||
- `write:repository`
|
||||
- `write:issue` (Renovate setzt Labels und kann den Dependency Dashboard erstellen)
|
||||
5. Token kopieren (wird nur einmal angezeigt).
|
||||
|
||||
### Schritt 3 - Token als Host-Secret ablegen
|
||||
|
||||
Am Unraid-Host:
|
||||
|
||||
```bash
|
||||
TOKEN='hier-das-token-einfuegen'
|
||||
echo -n "$TOKEN" > /mnt/user/appdata/secrets/renovate_token.txt
|
||||
chmod 600 /mnt/user/appdata/secrets/renovate_token.txt
|
||||
chown root:root /mnt/user/appdata/secrets/renovate_token.txt
|
||||
```
|
||||
|
||||
Token-Wert nicht in dieses Repo, nicht in Logs, nicht in Issues.
|
||||
|
||||
### Schritt 4 - Erstlauf manuell
|
||||
|
||||
```bash
|
||||
bash /mnt/user/services/homelab-infra/ops/renovate/run-renovate.sh
|
||||
```
|
||||
|
||||
Erwartete Ausgabe: Renovate verbindet sich mit Gitea, scant Repos unter `Micha/*` und entweder
|
||||
|
||||
- erstellt Pull-Requests, falls Updates verfuegbar sind, **oder**
|
||||
- erstellt nur die "Renovate Dependency Dashboard"-Issue im Repo (Onboarding-PR ist via `onboarding: false` deaktiviert)
|
||||
|
||||
Log liegt unter `/mnt/user/services/renovate/logs/renovate-<timestamp>.log` und symlinkt auf `latest.log`.
|
||||
|
||||
### Schritt 5 - User-Script aktivieren
|
||||
|
||||
Unraid User Scripts:
|
||||
|
||||
```
|
||||
Name: renovate-six-hourly
|
||||
Description: Run Renovate against Gitea every 6 hours.
|
||||
Schedule: 20 */6 * * *
|
||||
Script: bash /mnt/user/services/homelab-infra/ops/renovate/run-renovate.sh
|
||||
```
|
||||
|
||||
20 Minuten nach jeder vollen Stunde, damit es nicht mit `gitea-bundle-mirror-6h` (Minute 10) kollidiert.
|
||||
|
||||
## Was Renovate macht und was nicht
|
||||
|
||||
| Verhalten | Renovate-Konfig | Wirkung |
|
||||
|---|---|---|
|
||||
| Major-Updates | `groupName: major-updates`, `automerge: false` | Eine gesammelte PR pro Lauf mit allen Major-Updates, manueller Merge |
|
||||
| Minor + Patch + Digest fuer Docker-Compose | `groupName: minor-and-patch-updates`, `automerge: false` | Eine gesammelte PR; Operator merged manuell |
|
||||
| Tier-1-Datenhalter (Postgres, Mongo, Redis, pgvecto-rs) | `groupName: null`, eigener Label | Einzelne PRs ohne Group, hoehere Sichtbarkeit |
|
||||
| Komodo-Major-Updates | `enabled: false` fuer matchPackageNames + matchUpdateTypes major | Komodo bleibt auf `:2`, wird nicht versehentlich auf `:3` migriert |
|
||||
| Lock-File-Maintenance | `lockFileMaintenance.enabled: false` | Renovate macht keine reinen Lock-File-Refreshs |
|
||||
| Schedule | `extends ["schedule:weekly"]` | Renovate-Engine prueft, aber PRs/Updates folgen Wochen-Profilen wo sinnvoll |
|
||||
| Dependency Dashboard | aktiv | Gitea-Issue, die alle ausstehenden Updates auflistet |
|
||||
| Onboarding-PR | `onboarding: false` | Keine `Configure Renovate`-Onboarding-PR; wir nutzen die Repo-`renovate.json` direkt |
|
||||
| Ignore-Pfade | `_archive`, `ops/grafana-influxdb`, `ops/loki` | Renovate scant alte/abgeloeste Stacks nicht |
|
||||
|
||||
## Erwartete erste PRs
|
||||
|
||||
Beim Erstlauf wird Renovate vermutlich PRs fuer einige der digest-gepinnten Images oeffnen, weil diese Digests seit Wochen nicht erneuert wurden. Reihenfolge der Sichtpruefung:
|
||||
|
||||
1. **Stateful Tier-1 zuerst, einzeln**: Postgres, Redis, Mongo, pgvecto-rs - jeder eigener PR, einzeln pruefen und mergen. Smoke-Test nach Merge ueber Komodo-Webhook-Deploy beobachten.
|
||||
2. **Gruppe minor-and-patch-updates**: Alle anderen Docker-Compose-Images zusammen. Wenn der Diff vernuenftig aussieht, mergen.
|
||||
3. **Gruppe major-updates**: Erst nach Operator-Sichtpruefung pro Image, ggf. zurueckstellen oder manuell entscheiden.
|
||||
|
||||
## Notfall-Stop
|
||||
|
||||
Wenn Renovate aus irgendeinem Grund zu aggressiv wird oder ungewollte PRs oeffnet:
|
||||
|
||||
```bash
|
||||
# 1. User-Script disablen
|
||||
# Unraid UI: User Scripts -> renovate-six-hourly -> Schedule -> Disabled
|
||||
|
||||
# 2. Im Worst Case: Token sofort widerrufen
|
||||
# Gitea -> Login als renovate -> Settings -> Applications -> Token loeschen
|
||||
|
||||
# 3. Offene PRs schliessen ohne mergen
|
||||
```
|
||||
|
||||
## Was bewusst NICHT enthalten ist
|
||||
|
||||
- **Auto-Merge**: keine PR wird ohne Operator-Click ausgerollt. Auto-Merge waere bei einem GitOps-Setup mit live-Webhooks ein zu grosses Risiko.
|
||||
- **Renovate-UI**: kein Mend.io-Cloud-Account, kein zusaetzlicher Service; lokal genutzte CLI im Docker-Container.
|
||||
- **Slack/E-Mail-Benachrichtigungen**: Renovate signalisiert ueber Gitea-PRs und das Dependency Dashboard.
|
||||
- **Self-hosted Renovate-Runner-Cluster**: ein einzelner User-Script-Lauf reicht fuer den Homelab-Scope.
|
||||
|
||||
## Verwandte Doku
|
||||
|
||||
- `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13 ("Reproduzierbare Deployments", Digest-Pinning)
|
||||
- `docs/WORKFLOW.md` Image-Versionierungs-Regel
|
||||
- `docs/SECRETS_MAP.md` (Renovate-Token wird dort nach Aktivierung ergaenzt)
|
||||
+11
-3
@@ -32,11 +32,18 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
||||
| `docs/DISASTER_RECOVERY.md` | Wiederanlauf nach Host-/Systemausfall |
|
||||
| `docs/RESTORE_MATRIX.md` | Restore-Quellen, Dump-Artefakte und Smoke-Tests je Dienst |
|
||||
| `docs/SERVICES_RECOVERY.md` | Recovery-kritische `/mnt/user/services`-Pfade, Gitea-Mirror und Komodo-Bootstrap |
|
||||
| `docs/STORAGE_LAYOUT.md` | verbindliche Storage-/Share-/Pfad-Konstitution |
|
||||
| `docs/HARDWARE_INVENTORY.md` | Hardware-, Disk-, SMART-, USV- und Strom-Inventar |
|
||||
| `docs/NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netztrennung |
|
||||
| `docs/EXTERNAL_DEPENDENCIES.md` | Externe Provider, Konten, Ausfall-Szenarien und kritische Off-Repo-Abhaengigkeiten |
|
||||
| `docs/CAPACITY_AND_LIFECYCLE.md` | Capacity-Schwellen, Wachstum, Upgrade-Trigger und Restore-Zeitziele |
|
||||
| `docs/FAMILY_ONBOARDING.md` | Familienorientierte Nutzungsdoku ohne Operator-Details |
|
||||
| `docs/FAMILY_VIEW_DASHBOARD.md` | Spezifikation fuer das Grafana Family-View-Dashboard (Doku-only, kein JSON) |
|
||||
| `docs/RESTORE_DRILL_ROUTINE.md` | Quartalsweise Restore-Drill-Routine, Tier-Belegung, DR-Sanity-Check |
|
||||
| `docs/IMMICH_RESTORE_TEST.md` | Operator-Overview Immich-Restore-Test, Erstlauf 2026-05-27 erfolgreich |
|
||||
| `docs/RENOVATE.md` | Self-hosted Renovate gegen Gitea (Setup + Wartung) |
|
||||
| `docs/FRITZBOX_PORT_CORRECTION_PLAN.md` | Vorbereitungs-Doku fuer FRITZ!Box-Portfreigaben-Korrektur (kein Router-Eingriff) |
|
||||
| `docs/OFFSITE_BACKUP_OPTIONS.md` | Entscheidungsvorlage zweites Offsite-Backup-Ziel (rsync.net vs. BorgBase EU2 vs. Cold-Platte) |
|
||||
| `docs/AUDIT_2026-05-25_TODO.md` | Operative Arbeitsliste aus dem Audit vom 2026-05-25; Authelia-2FA bewusst geparkt |
|
||||
| `docs/ALERTING_MAP.md` | ntfy Topic-Konvention und Sender-Mapping fuer Homelab-Alerts |
|
||||
| `docs/ROLLBACK.md` | Rueckweg bei Fehlern im GitOps-Betrieb |
|
||||
@@ -59,9 +66,10 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
||||
| `monitoring/grafana/provisioning/*` | Grafana Datasource-/Dashboard-Provisioning fuer Prometheus und Loki |
|
||||
| `ops/glance/config/glance.yml` | Glance Dashboard-Konfiguration fuer Homelab-Monitore, Internet-/DNS-/VPN-Widgets, Community-Widgets, Docker-Containergruppen, Zeitfortschritt, Host-Snapshot, Bookmarks und zweite Infrastruktur-Seite |
|
||||
| `ops/borg-ui/scripts/pre-backup-dumps.sh` | Host-seitiges Dump-Skript fuer PostgreSQL, SQLite-Container-Dumps und Komodo Mongo |
|
||||
| `services/posture-check/posture-check.sh` | Host-seitiger Posture-Check fuer Filesystem, Mover-Drift, NVMe-SMART, Fuellstand und ntfy-Alarmierung |
|
||||
| `services/posture-check/posture-check.sh` | Host-seitiger Posture-Check fuer Filesystem, Mover-Drift, NVMe-SMART, Fuellstand, Authelia-Repo<->Host-Drift und ntfy-Alarmierung |
|
||||
| `services/posture-check/docker-critical-events.sh` | Host-seitiger Docker-Event-Watcher fuer kritische ntfy-Alarme |
|
||||
| `services/posture-check/posture_check.sh` | Kompatibilitaets-Wrapper fuer die Schreibweise aus `STORAGE_LAYOUT.draft.md` |
|
||||
| `services/posture-check/posture_check.sh` | Kompatibilitaets-Wrapper fuer die historische Schreibweise aus `STORAGE_LAYOUT.draft.md` |
|
||||
| `services/authelia-diff.sh` | Vergleicht `access_control:`-Sektion zwischen Repo-Baseline und Host-Datei; wird vom Posture-Check als Check `authelia_config_drift` aufgerufen |
|
||||
| `ops/hermes-agent/config/hermes/config.yaml` | Hermes Agent Konfiguration mit Env-Platzhaltern |
|
||||
| `ops/hermes-agent/hermes.env.example` | Beispiel fuer Hermes `.env`; echte Datei liegt auf Host-Appdata |
|
||||
| `ops/hermes-agent/stack.env.example` | Beispiel fuer Hermes Stack-ENV; echte `stack.env` bleibt host-/komodoseitig und ist per `.gitignore` ausgeschlossen |
|
||||
@@ -93,7 +101,7 @@ Secret-Werte werden hier nicht dokumentiert. Aufgefuehrt werden nur Variablennam
|
||||
| Vaultwarden | `security/vaultwarden/docker-compose.yml` | `vaultwarden` -> `vaultwarden/server:latest@sha256:...` | `vault.kaleschke.info` | `frontend_net` | keine | Datei-Persistenz, `ADMIN_TOKEN_FILE` |
|
||||
| ddns-updater | `infra/ddns-updater/docker-compose.yml` | `ddns-updater` -> `ghcr.io/qdm12/ddns-updater:latest@sha256:...` | keine | `frontend_net` | keine | Cloudflare/API-Internetbedarf |
|
||||
| PostgreSQL 17 | `infra/postgresql17/docker-compose.yml` | `postgresql17` -> `postgres:17.9@sha256:...` | keine | `backend_net` | keine | shared DB-Cluster |
|
||||
| Redis | `infra/redis/docker-compose.yml` | `Redis` -> `redis:7.4-alpine@sha256:...` | keine | `backend_net` | keine | shared Cache, Passwort-Datei |
|
||||
| Redis | `infra/redis/docker-compose.yml` | `Redis` -> `redis:7.4-alpine@sha256:...` | keine | `backend_net` | keine | primaer Paperless-Redis (App-Cache); historisch als "shared" angelegt, faktisch nur von Paperless genutzt; Passwort-Datei |
|
||||
|
||||
### Host Services
|
||||
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# Restore-Drill Routine - KalliLab CORE
|
||||
|
||||
Status: **verbindliche Routine (Doku-only)**, 2026-05-27.
|
||||
Audit-Bezug: `docs/AUDIT_2026-05-25.md` Sprint 7 "Restore-Lab-Drill quartalsweise dokumentieren".
|
||||
Verwandte Docs: `docs/RESTORE_MATRIX.md`, `docs/RESTORE_HANDBOOK.md`, `docs/DISASTER_RECOVERY.md`, `ops/restore-tests/schedule.md`.
|
||||
|
||||
## Ziel
|
||||
|
||||
Sicherstellen, dass die Backup-Kette nicht nur weiter laeuft, sondern auch **wiederherstellbar** ist. Restore-Tests werden nicht ad-hoc gemacht, wenn ein Problem auftritt, sondern in einer planbaren Kadenz, damit das Vertrauen ueber die Zeit waechst und Drift fruehzeitig auffaellt.
|
||||
|
||||
Diese Datei beschreibt nur die **Routine** (wann, was, wie pruefen). Die operativen Anleitungen pro Dienst stehen in `docs/RESTORE_HANDBOOK.md` und den dienstspezifischen Runbooks unter `ops/restore-tests/<dienst>-runbook.md`.
|
||||
|
||||
## Drei-Stufen-Modell
|
||||
|
||||
| Stufe | Frequenz | Aufwand | Ziel |
|
||||
|---|---|---|---|
|
||||
| Freshness-Check | woechentlich | Minuten | Backup-Artefakte sind frisch, Reports lesbar |
|
||||
| Mini-Restore | monatlich/quartalsweise | 15-60 Min | Ein konkreter Dienst wird in isoliertem Test-Lab restauriert |
|
||||
| DR-Sanity-Check | quartalsweise | 1-2 h | Reihenfolge, Doku, Tier-Reihenfolge in der DR-Doku gegen Realitaet pruefen |
|
||||
|
||||
## Bestaetigte Mini-Restores
|
||||
|
||||
Wenn ein Mini-Restore zum ersten Mal sauber durchlaeuft, wird er hier als Referenz gefuehrt. Der Eintrag wird **nicht** entfernt, wenn er wiederholt wird; stattdessen aendert sich der Datums-Stand.
|
||||
|
||||
| Dienst | Erster bestaetigter Lauf | Letzter Erfolg | Report | Repo-Skript |
|
||||
|---|---|---|---|---|
|
||||
| Vaultwarden | 2026-05-07 | 2026-05-07 | `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md` | `ops/restore-tests/vaultwarden-restore-test.sh` |
|
||||
| Gitea | 2026-05-07 | 2026-05-07 | `/mnt/user/backups/restore-reports/gitea-2026-05-07.md` | `ops/restore-tests/gitea-restore-test.sh` |
|
||||
| Paperless | 2026-05-07 | 2026-05-07 | `/mnt/user/backups/restore-reports/paperless-2026-05-07.md` | `ops/restore-tests/paperless-restore-test.sh` |
|
||||
| Immich | **2026-05-27** | **2026-05-27** | `/mnt/user/backups/restore-reports/immich-2026-05-27.md` | `ops/restore-tests/immich-restore-test.sh` |
|
||||
|
||||
Bei jedem weiteren Lauf wird die Spalte "Letzter Erfolg" aktualisiert.
|
||||
|
||||
## Quartals-Kadenz
|
||||
|
||||
Ein Kalenderjahr enthaelt vier Quartals-Drills. Jeder Quartals-Drill besteht aus dem Mini-Restore eines anderen Tier-2-Dienstes plus einem DR-Sanity-Check der Tier-1-Dienste.
|
||||
|
||||
| Quartal | Mini-Restore | DR-Sanity-Check Fokus |
|
||||
|---|---|---|
|
||||
| Q1 (Januar-Maerz) | `paperless` | Tier-1-Reihenfolge, Posture-Check-Status, Borg-Frische-Alerts |
|
||||
| Q2 (April-Juni) | `immich` | Komodo-Bootstrap-Pfad, Gitea-Bundles, Secrets-Pfad-Inventur |
|
||||
| Q3 (Juli-September) | `mealie` oder `nextcloud` (Operator-Wahl) | DNS-Pfad (AdGuard/Unbound/Tailscale), Cert-Expiry-Sicht |
|
||||
| Q4 (Oktober-Dezember) | `vaultwarden` oder `gitea` (Operator-Wahl) | Externe Abhaengigkeiten (Cloudflare, Hetzner, GitHub-Mirror), Off-site-Zweitziel-Diskussion |
|
||||
|
||||
Diese Liste ist bewusst auf Tier-2 und Tier-1-Dienste fokussiert. Tier-3-Dienste (Filebrowser, Glances, Scrutiny, Speedtest, Glance) werden im Drill nicht explizit ausgefuehrt, weil sie rebuildbar sind oder keinen kritischen Datenbestand haben.
|
||||
|
||||
### Q2 2026 - Konkrete Belegung
|
||||
|
||||
- Mini-Restore: **Immich (erledigt 2026-05-27)**.
|
||||
- DR-Sanity-Check (teilweise erledigt, Rest vor Quartalsende 2026-06-30):
|
||||
- Komodo-Bootstrap-Pfad: **erledigt 2026-05-30** durch echten Trockenlauf via `ops/restore-tests/komodo-bootstrap-test.sh --keep-data`, Report `/mnt/user/backups/restore-reports/komodo-bootstrap-2026-05-30.md`, `ops/komodo/docker-compose.yml` als Recovery-Anker belegt.
|
||||
- Gitea-Bundles ueber `ops/borg-ui/scripts/gitea-bundle-mirror.sh` auf Frische und Bundle-Klonbarkeit pruefen: offen.
|
||||
- Secrets-Inventur gegen `docs/SECRETS_MAP.md` abgleichen: offen.
|
||||
|
||||
### Wer schiebt das an?
|
||||
|
||||
- **Operator** loest jeden Drill manuell aus, idealerweise am 2. Wochenende des ersten Monats im Quartal.
|
||||
- Es gibt **keinen** automatischen Host-Schedule fuer den Quartals-Drill. Die woechentliche Freshness-Pruefung und die monatlichen Mini-Restores in `ops/restore-tests/schedule.md` laufen separat.
|
||||
- Bei akuten Aenderungen (Major-Upgrade eines Dienstes, FS-Migration, Repo-Strukturaenderung): zusaetzlichen Ad-hoc-Drill ausserhalb der Quartals-Kadenz einplanen.
|
||||
|
||||
## Freshness-Check (woechentlich)
|
||||
|
||||
- Skript: `ops/restore-tests/check-restore-freshness.sh` (Host-Bash) bzw. `ops/restore-tests/check-restore-freshness.ps1` (Windows-Operator).
|
||||
- Erwartete Pruefungen:
|
||||
- Letzter Borg-Archiv-Stand juenger als die Schwellwerte aus `docs/STORAGE_LAYOUT.md` §11.
|
||||
- Kanonische Dump-Artefakte unter `/mnt/user/backups/borg/dumps/latest/` vorhanden und juenger als 26 h.
|
||||
- Letzte Report-Dateien unter `/mnt/user/backups/restore-reports/` lesbar.
|
||||
- Gitea-Bundles unter `/mnt/user/backups/git-bundles/gitea/` plausibel aktuell.
|
||||
|
||||
Ergebnis ist ein kurzes Konsolen-Log; bei Fehler greift die ntfy-Alarmierung aus `docs/ALERTING_MAP.md`.
|
||||
|
||||
## Mini-Restore (monatlich / bimonatlich)
|
||||
|
||||
Skripte folgen alle demselben Muster:
|
||||
|
||||
- isoliertes Test-Lab unter `/mnt/user/backups/restore-lab/<dienst>`
|
||||
- isolierte Test-Container `restoretest-*`
|
||||
- nur `127.0.0.1`-Ports, keine Traefik-Labels, keine produktive Domain
|
||||
- Smoke-Test mit Erfolgsregel "Container laeuft reicht nicht"
|
||||
- Report unter `/mnt/user/backups/restore-reports/<dienst>-YYYY-MM-DD.md`
|
||||
|
||||
Operative Anleitungen je Dienst:
|
||||
|
||||
- `ops/restore-tests/vaultwarden-runbook.md`
|
||||
- `ops/restore-tests/gitea-runbook.md`
|
||||
- `ops/restore-tests/paperless-runbook.md`
|
||||
- `ops/restore-tests/immich-runbook.md`
|
||||
|
||||
## DR-Sanity-Check (quartalsweise)
|
||||
|
||||
Der Sanity-Check ist **kein** echter Restore. Er ist eine Doku-/Konsistenz-Pruefung mit zehn Punkten:
|
||||
|
||||
1. `docs/DISASTER_RECOVERY.md` Phase 1-5 noch konsistent mit Repo und Live-Stand?
|
||||
2. `docs/RESTORE_MATRIX.md` Tier-Klassifizierung pro Dienst aktuell?
|
||||
3. `docs/SECRETS_MAP.md` Pfade existieren, Stack-ENV-only-Liste aktuell?
|
||||
4. `docs/SERVICES_RECOVERY.md` Komodo-Bootstrap-Pfad noch in Stufen A-F konsistent?
|
||||
5. Gitea-Bundle-Mechanik laeuft und letzter Bundle-Stand klonbar (`git clone .../homelab-infra.bundle /tmp/restore-test`)?
|
||||
6. Externe Mirrors (`michaelkaleschke-spec/homelab-infra` auf GitHub) gemaess `docs/EXTERNAL_DEPENDENCIES.md` noch erreichbar und aktuell?
|
||||
7. ntfy-Push-Pfad noch erreichbar? (Test-Nachricht an `homelab-info`.)
|
||||
8. Letzte vier Quartals-Mini-Restores im Report-Verzeichnis vorhanden?
|
||||
9. Borg-Repo-Passphrase Offline-Sicherung noch auffindbar? (Pruefung durch Operator, nicht durch Skript, kein Wert ablegen.)
|
||||
10. Capacity-Stand gegen Schwellen aus `docs/CAPACITY_AND_LIFECYCLE.md` abgeglichen?
|
||||
|
||||
Jeder Punkt wird in einem kurzen Quartals-Eintrag in `docs/MIGRATION_LOG.md` als `ok` / `Abweichung` / `Folgeaufgabe` festgehalten.
|
||||
|
||||
## Abbruch-Regeln
|
||||
|
||||
Wenn ein Drill fehlschlaegt, gilt die Stop-Regel aus `docs/WORKFLOW.md`:
|
||||
|
||||
- nach zwei fehlgeschlagenen Reparaturversuchen nicht weiterschreiben
|
||||
- stattdessen Pflichtmatrix aus `docs/GITOPS_DRIFT_RUNBOOK.md` durchgehen
|
||||
- Befund dokumentieren, naechsten Schritt mit dem Operator klaeren
|
||||
- erst danach den Drill erneut starten oder das Quartal als "Drill incomplete" markieren
|
||||
|
||||
## Berichte
|
||||
|
||||
- Mini-Restore-Reports liegen unter `/mnt/user/backups/restore-reports/<dienst>-YYYY-MM-DD.md`.
|
||||
- Quartals-Sanity-Checks landen als kurzer Block in `docs/MIGRATION_LOG.md`, nicht als eigenes Dokument.
|
||||
- Reports werden nicht aus dem Repo verlinkt, weil sie nicht im Repo liegen. Operator dokumentiert nur Vorhanden/Erfolg/Datum.
|
||||
|
||||
## Geltungsdauer
|
||||
|
||||
Diese Routine gilt ab Q2 2026. Bei groesseren Aenderungen (zweites Off-site, Authelia-OIDC-Aktivierung, Hardware-Migration) wird die Liste pro Quartal angepasst.
|
||||
@@ -33,7 +33,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
||||
| PostgreSQL 17 | Share + Dumps | `/mnt/user/appdata/postgresql17` | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt` | `backend_net` | DB startet, Ziel-Datenbanken vorhanden |
|
||||
| Redis | Share / Host | `/mnt/user/appdata/redis` | keine | `redis_password.txt` | `backend_net` | Redis startet, Apps verbinden sich |
|
||||
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 17, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
|
||||
| Gitea | GitHub-Mirror fuer Repo-Bootstrap, Borg + Dump fuer Gitea-Appstate | `/mnt/user/services/gitea/data` | `gitea.sqlite.dump` | `borg_repo_passphrase.txt` fuer Restore-Tests; GitHub-Push-Mirror-PAT liegt nur in Gitea-Mirror-Settings | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; GitHub-Push-Mirror synchronisiert ohne `last_error`; Mini-Restore nach `/mnt/user/backups/restore-lab/gitea` am 2026-05-07 erfolgreich validiert |
|
||||
| Gitea | GitHub-Mirror + Gitea-Bundles fuer Repo-Bootstrap, Borg + Dump fuer Gitea-Appstate | `/mnt/user/services/gitea/data`, `/mnt/user/backups/git-bundles/gitea` | `gitea.sqlite.dump`, Bundle-Report `latest-report.md` | `borg_repo_passphrase.txt` fuer Restore-Tests; GitHub-Push-Mirror-PAT liegt nur in Gitea-Mirror-Settings | Traefik | Web-UI erreichbar, Repo sichtbar, SSH-Port reagiert; Bundle laesst sich klonen und `git fsck` ist sauber; GitHub-Push-Mirror synchronisiert ohne `last_error`; Mini-Restore nach `/mnt/user/backups/restore-lab/gitea` am 2026-05-07 erfolgreich validiert |
|
||||
| Komodo | Borg / Share | `/mnt/user/appdata/komodo/core`, `/mnt/user/appdata/komodo/periphery`, `/mnt/user/services/stacks` | `komodo-mongo.archive.gz` falls verifiziert | `komodo_mongo_password.txt`, `KOMODO_*` Stack ENV | Traefik, Mongo, Gitea | UI erreichbar, Periphery verbunden |
|
||||
| GitOps Host Automation | Borg / Git | `/mnt/user/services/homelab-infra`, `/mnt/user/services/posture-check` | keine eigene DB | keine | Gitea, Komodo, Unraid User Scripts | `posture-check` laeuft vom Host-Pfad und liefert `warning_count: 0` im bekannten Uebergangszustand |
|
||||
| Vaultwarden | Borg + Dump | `/mnt/user/appdata/vaultwarden` | `vaultwarden.sqlite.dump` | `vaultwarden_admin_token.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar; Mini-Restore nach `/mnt/user/backups/restore-lab/vaultwarden` am 2026-05-07 erfolgreich validiert |
|
||||
@@ -46,7 +46,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
||||
|---|---|---|---|---|---|---|
|
||||
| Paperless-ngx | Borg + Dumps | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/paperless/export`, `/mnt/user/documents/scans_inbox` | `postgresql17-paperless.dump` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS`, `borg_repo_passphrase.txt` fuer Restore-Tests | PostgreSQL 17, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Mini-Restore nach `/mnt/user/backups/restore-lab/paperless` am 2026-05-07 erfolgreich validiert |
|
||||
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, optional `/mnt/user/appdata/mealie/postgres` bei lokalem Share-Weiterbetrieb | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
||||
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt` | `immich_postgres`, `immich_redis`, Traefik | UI startet, Medienbibliothek sichtbar |
|
||||
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | `immich.dump` | `IMMICH_DB_PASSWORD`, `immich_postgres_password.txt`, `borg_repo_passphrase.txt` fuer Restore-Tests | `immich_postgres`, `immich_redis`, Traefik | DB- und UI-Smoke gegen produktives Borg-Archiv am 2026-05-27 erfolgreich validiert: `immich.dump` extrahiert, isolierter pgvecto-rs-Postgres importiert, Immich-Loginseite HTTP 200, `11977` Assets und `1` User im Test-DB-Check; Report `/mnt/user/backups/restore-reports/immich-2026-05-27.md`. Voll-Restore der Foto-Dateien bleibt separater DR-Drill |
|
||||
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 17, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
||||
| Nextcloud | Borg + Dump | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | `nextcloud.dump` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar |
|
||||
| Glance | Git / Borg-Repo | Repo-Konfiguration unter `ops/glance/config/glance.yml`; keine kritische Datenpersistenz | keine | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Traefik, Authelia, optional interne API-Ziele | Dashboard startet, Widgets laden, Docker-Status laeuft nur ueber `glance-docker-socket-proxy` |
|
||||
|
||||
+37
-1
@@ -52,6 +52,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
||||
| Monitoring Grafana | Admin Password | `/mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt` -> Docker Secret `/run/secrets/monitoring_grafana_admin_password` -> `GF_SECURITY_ADMIN_PASSWORD__FILE` | aktiv |
|
||||
| Monitoring Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/monitoring_grafana_influxdb_token` | aktiv |
|
||||
| Home Assistant -> InfluxDB | HA InfluxDB Token | `/homeassistant/secrets.yaml` -> `influxdb3_homeassistant_token` | geplant |
|
||||
| Renovate Bot | Gitea Service-Account PAT | `/mnt/user/appdata/secrets/renovate_token.txt` -> Host-Datei (chmod 600), gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_TOKEN` weitergegeben | aktiv nach Operator-Setup (siehe `docs/RENOVATE.md`) |
|
||||
|
||||
---
|
||||
|
||||
@@ -97,14 +98,49 @@ Weitere dokumentierte Secret-Pfade:
|
||||
- `/mnt/user/appdata/secrets/hermes_runner_id_ed25519`
|
||||
- `/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token`
|
||||
- Borg UI verwaltet Session-Secret, Admin-Login, SSH-Keys und Repo-Credentials in seiner persistenten `/data`-Struktur. Diese Daten liegen nicht im Git, muessen aber gesichert werden.
|
||||
- Die Borg-Repo-Passphrase liegt zusaetzlich als Host-Secret-Datei fuer Restore-Tests und Notfallzugriff vor; der Wert muss ausserhalb des Homelabs analog gesichert werden.
|
||||
- Die Borg-Repo-Passphrase liegt zusaetzlich als Host-Secret-Datei fuer Restore-Tests und Notfallzugriff vor. Der Wert ist laut Operator-Bestaetigung vom 2026-05-26 offline gesichert; Ablageort und Wert werden nicht im Repo dokumentiert.
|
||||
- Gitea verwaltet den GitHub-Push-Mirror-PAT in den Repository-Mirror-Settings. Der Wert wird nicht dokumentiert und nicht in Dateien unter `docs/` oder `core/gitea/` geschrieben.
|
||||
- `paperless-ngx` ist eine bewusste Ausnahme: DB-Passwort und Redis-URL bleiben aktuell als Komodo Stack Environment Variables hinterlegt, um den stabil laufenden Produktionsstand nicht fuer eine reine Secret-Mechanik-Migration zu riskieren.
|
||||
|
||||
---
|
||||
|
||||
## Stack-ENV-only Secrets - Restore-Wege
|
||||
|
||||
Einige Secrets liegen bewusst nur als Komodo Stack Environment Variables vor, weil das Image kein `_FILE` unterstuetzt oder ein laufender stabiler Produktionsstand nicht fuer eine reine Mechanik-Migration geopfert werden soll. Diese Werte existieren **ausschliesslich** an folgenden Stellen:
|
||||
|
||||
1. **Komodo Mongo** (Runtime und Backup-Dump `komodo-mongo.archive.gz` unter `/mnt/user/backups/borg/dumps/latest/`).
|
||||
2. **Vaultwarden** (Operator-Eintrag pro Stack, sofern dort gepflegt).
|
||||
3. **Externe Operator-Notiz** (analoge Sicherung, vergleichbar mit der Borg-Passphrase).
|
||||
|
||||
**Bei Komodo-Restore aus kaltem Zustand wird immer in dieser Reihenfolge gesucht.** Konkrete Werte werden im Repo, in Logs, in Doku-Kommentaren und in ntfy-Meldungen niemals wiedergegeben.
|
||||
|
||||
### Stacks und ihre Stack-ENV-only Secrets
|
||||
|
||||
| Stack | Stack-ENV-Variablen | Restore-Quelle (Reihenfolge) | Folgen bei Verlust aller Quellen |
|
||||
|---|---|---|---|
|
||||
| `paperless-ngx` | `PAPERLESS_DBPASS`, `PAPERLESS_REDIS` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | App-DB ist im Postgres-Cluster, Passwort muss in Postgres und Stack-ENV synchron neu gesetzt werden; Redis-URL ist deterministisch rekonstruierbar (Host, Port, Passwort), wenn Redis-Passwort-Datei vorliegt |
|
||||
| `immich-server` | `IMMICH_DB_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | analog Paperless: Postgres-User-Passwort in `immich_postgres` und Stack-ENV gemeinsam zuruecksetzen |
|
||||
| `mail-archiver` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | DB-Connection-String enthaelt Postgres-Pass; App-Auth-Password fuer Web-UI |
|
||||
| `speedtest-tracker` | `APP_KEY`, `ADMIN_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | `APP_KEY` ist verschluesselungsrelevant; bei echtem Verlust App-State frisch initialisieren |
|
||||
| `komodo-core` | `KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY` | Vaultwarden -> externe Notiz (Henne-Ei: Komodo-Mongo-Dump ist hier **nicht** Restore-Quelle, weil Komodo dafuer schon laufen muesste) | siehe `docs/SERVICES_RECOVERY.md` Komodo-Bootstrap; ohne diese Werte ist der Self-Stack nicht reproduzierbar |
|
||||
| `hermes-agent` | `HERMES_DASHBOARD_HOST` plus Provider-/API-/Home-Assistant-Tokens in Host-`.env` | Vaultwarden -> externe Notiz | Stack ist aktuell geparkt (Review 2026-07-25); ohne Werte bleibt der Stack deaktiviert, kein Schaden am Rest |
|
||||
| `glance` | `GLANCE_IMMICH_API_KEY`, `GLANCE_ADGUARD_USERNAME`, `GLANCE_ADGUARD_PASSWORD`, `GLANCE_SPEEDTEST_API_KEY` | Provider-UIs (Immich, AdGuard, Speedtest-Tracker) neu erzeugen | rebuildbar; Widgets bleiben leer bis Tokens neu erzeugt sind, kein kritischer Datentopf |
|
||||
|
||||
### Komodo-Sonderfall
|
||||
|
||||
Komodos eigene Secrets (`KOMODO_*`) sind die kritischste Untermenge dieser Liste, weil sie nicht aus dem eigenen Mongo-Dump rekonstruierbar sind, solange Komodo nicht laeuft. Sie gehoeren entweder
|
||||
|
||||
- in Vaultwarden (sobald Vaultwarden produktiv ist) **und**
|
||||
- in eine analoge Operator-Notiz neben der Borg-Passphrase.
|
||||
|
||||
Details und Bootstrap-Reihenfolge stehen in `docs/SERVICES_RECOVERY.md` und werden in `docs/DISASTER_RECOVERY.md` Abschnitt 6.2.1 als Pflicht-Pruefung vor Phase 4 Stufe 4 referenziert.
|
||||
|
||||
---
|
||||
|
||||
## Regel
|
||||
|
||||
Wenn `_FILE` nicht unterstuetzt wird -> Stack Environment Variable in Komodo verwenden.
|
||||
|
||||
Secrets niemals direkt in die Compose-Datei schreiben.
|
||||
|
||||
Stack-ENV-Werte niemals im Repo, in Logs oder in Doku-Kommentaren ablegen — nur Variablennamen und Restore-Quellen.
|
||||
|
||||
+132
-21
@@ -1,7 +1,7 @@
|
||||
# Services Recovery - KalliLab CORE
|
||||
|
||||
Status: Initiale Recovery-Baseline 2026-05-26, aus dem Audit 2026-05-25 abgeleitet.
|
||||
Verwandte Docs: `docs/DISASTER_RECOVERY.md`, `docs/RESTORE_MATRIX.md`, `docs/STORAGE_LAYOUT.draft.md`, `docs/SECRETS_MAP.md`
|
||||
Verwandte Docs: `docs/DISASTER_RECOVERY.md`, `docs/RESTORE_MATRIX.md`, `docs/STORAGE_LAYOUT.md`, `docs/SECRETS_MAP.md`
|
||||
|
||||
## Zweck
|
||||
|
||||
@@ -31,11 +31,13 @@ Optionen:
|
||||
|
||||
Empfohlener Start:
|
||||
|
||||
1. `git bundle`-Job fuer alle Gitea-Repositories definieren.
|
||||
2. Ziel auf zweitem physischen Medium oder separatem Off-site-Ziel ablegen.
|
||||
3. Job alle 6 Stunden ausfuehren.
|
||||
1. `ops/borg-ui/scripts/gitea-bundle-mirror.sh` auf dem Host ausfuehren.
|
||||
2. Ziel `/mnt/user/backups/git-bundles/gitea` in Borg/off-site Scope aufnehmen.
|
||||
3. Job alle 6 Stunden oder mindestens vor Borg ausfuehren.
|
||||
4. Stichprobe: ein Bundle in Wegwerfpfad klonen.
|
||||
|
||||
Erstlauf 2026-05-26: 4 Gitea-Bundles erzeugt, Checksums OK, `homelab-infra.bundle` in Restore-Lab geklont und `git fsck` sauber. Offen bleibt die dauerhafte Host-Zeitplanung.
|
||||
|
||||
Erfolgskriterium:
|
||||
|
||||
```bash
|
||||
@@ -45,30 +47,137 @@ git -C /tmp/repo-restore-test fsck
|
||||
|
||||
## Komodo Bootstrap
|
||||
|
||||
Problem: Komodo verwaltet Stacks, ist aber selbst Teil des Recovery-Pfads. Ein kalter Host darf nicht voraussetzen, dass Komodo schon laeuft.
|
||||
### Problemstellung
|
||||
|
||||
Komodo ist deshalb bewusst kein normaler Auto-Deploy-Stack: der `komodo`-Self-Stack hat keinen aktiven Gitea-Webhook. Recovery und Aenderungen laufen ueber den dokumentierten Bootstrap-Pfad und muessen nach dem Start in Komodo validiert werden.
|
||||
Komodo verwaltet alle Stacks per GitOps, ist aber selbst Teil des Recovery-Pfads. Ein kalter Host darf **nicht** voraussetzen, dass Komodo schon laeuft. Das ist das klassische Henne-Ei-Problem: Komodo darf sich nicht selbst aus dem Repo holen muessen, bevor es laufen kann.
|
||||
|
||||
Minimaler Wiederanlauf:
|
||||
### Recovery-Anker (verbindlich)
|
||||
|
||||
1. Docker und externe Netze herstellen (`frontend_net`, `backend_net`, ggf. weitere dokumentierte Netze).
|
||||
2. Repo aus Gitea/GitHub Mirror klonen.
|
||||
3. Komodo Compose aus `ops/komodo/docker-compose.yml` oder einem spaeteren Bootstrap-Pfad starten.
|
||||
4. Erforderliche `.env`/Secrets aus Host-Secret-Backup wiederherstellen.
|
||||
5. Komodo-Core, Periphery und Mongo starten.
|
||||
6. Web-UI und Periphery-Verbindung pruefen.
|
||||
**Anker:** `ops/komodo/docker-compose.yml` aus dem Repo.
|
||||
|
||||
Offene Aufgabe:
|
||||
Dieses Compose-File ist die einzige Quelle, aus der Komodo nach einem Kaltstart hochgefahren wird. Es wird nicht ueber Komodos eigenen Auto-Deploy-Pfad konsumiert.
|
||||
|
||||
- Entscheidung 2026-05-26: `ops/komodo/docker-compose.yml` bleibt die verbindliche Bootstrap-Quelle. Der `komodo`-Self-Stack hat keinen aktiven Gitea-Webhook und ist nicht der Recovery-Anker.
|
||||
- Offen bleibt nur ein spaeterer Trockenlauf, bei dem die Komodo-Startkommandos gegen echte Restore-Pfade getestet werden.
|
||||
**Was der Anker bewusst NICHT ist:**
|
||||
|
||||
Validierung:
|
||||
- nicht der Komodo-Self-Stack (Komodo darf sich nicht selbst deployen).
|
||||
- nicht der laufende Komodo-Workspace unter `/mnt/user/services/stacks/komodo/compose.yaml` (kann driften, siehe `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13, Drift-Recovery 2026-05-04).
|
||||
- nicht ein Gitea-Webhook (`komodo`-Stack hat bewusst `webhook_enabled: false`).
|
||||
|
||||
**Quelle der Compose-Datei:**
|
||||
|
||||
1. Vorzug: lokaler Repo-Clone auf dem Operator-Windows-PC (`G:\Gitea_Clone\homelab-infra\`).
|
||||
2. Fallback: GitHub-Mirror `michaelkaleschke-spec/homelab-infra` (siehe `docs/EXTERNAL_DEPENDENCIES.md`).
|
||||
3. Letzter Fallback: Gitea-Bundles unter `/mnt/user/backups/git-bundles/gitea/homelab-infra.bundle` (siehe Mirror-Abschnitt oben).
|
||||
|
||||
Wenn alle drei Quellen down sind, ist Recovery blockiert und das Problem ist nicht Komodo, sondern Repo-Verlust.
|
||||
|
||||
### Kaltstart-Schritte
|
||||
|
||||
Der Wiederanlauf-Pfad ist linear; jeder Schritt hat ein eindeutiges Erfolgskriterium, bevor der naechste laeuft.
|
||||
|
||||
**Stufe A - Host und Docker-Grundlage**
|
||||
|
||||
1. Unraid bootet; Array ist online; Shares `/mnt/user/appdata`, `/mnt/user/services`, `/mnt/user/backups` sichtbar.
|
||||
2. Docker-Daemon laeuft (`docker info` antwortet).
|
||||
3. 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`.
|
||||
|
||||
Erfolgskriterium: `docker network ls` zeigt `frontend_net` und `backend_net`.
|
||||
|
||||
**Stufe B - Repo-Quelle bereitstellen**
|
||||
|
||||
1. Repo-Clone aus dem bevorzugten Pfad bereithalten:
|
||||
- lokaler Operator-Clone, oder
|
||||
- frischer Clone aus GitHub-Mirror, oder
|
||||
- Bundle-Restore aus `/mnt/user/backups/git-bundles/gitea/homelab-infra.bundle` (`git clone homelab-infra.bundle homelab-infra`).
|
||||
2. Repo-Stand verifizieren: `git -C <pfad> log --oneline -1` zeigt einen plausibel aktuellen Commit.
|
||||
|
||||
Erfolgskriterium: `ops/komodo/docker-compose.yml` ist auf dem Host lesbar.
|
||||
|
||||
**Stufe C - Komodo-Secrets bereitstellen**
|
||||
|
||||
Komodo braucht beim Start mehrere Secrets, die **nicht** aus dem Repo kommen. Restore-Reihenfolge gemaess `docs/SECRETS_MAP.md`:
|
||||
|
||||
1. Host-Secrets unter `/mnt/user/appdata/secrets/` wiederherstellen (aus Borg oder analog gesicherter Quelle).
|
||||
2. Datei `/mnt/user/appdata/secrets/komodo_mongo_password.txt` ist Pflicht (Mongo-Initialisierung).
|
||||
3. Stack-ENV-Werte `KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY` muessen als Host-`.env` neben dem Compose vorliegen. Quelle in dieser Reihenfolge: Vaultwarden (sobald restauriert), externe Operator-Notiz, oder Komodo-Mongo-Dump (nur wenn Mongo separat bereits gestartet und die `stack`-Collection lesbar ist).
|
||||
|
||||
Erfolgskriterium: Compose-Validierung laeuft ohne fehlende Variablen.
|
||||
|
||||
```bash
|
||||
docker compose -f ops/komodo/docker-compose.yml config >/dev/null
|
||||
```
|
||||
|
||||
**Stufe D - Komodo starten**
|
||||
|
||||
1. Compose hochfahren:
|
||||
|
||||
```bash
|
||||
docker compose -f ops/komodo/docker-compose.yml config
|
||||
docker compose -f ops/komodo/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
2. Reihenfolge intern: `komodo-mongo` zuerst healthy, dann `komodo-core`, dann `komodo-periphery`.
|
||||
3. Status pruefen:
|
||||
|
||||
```bash
|
||||
docker ps --filter "name=komodo"
|
||||
docker logs --tail 50 komodo-core
|
||||
docker logs --tail 50 komodo-periphery
|
||||
```
|
||||
|
||||
Erfolgskriterium: alle drei Container laufen; Komodo-Core meldet Bind auf Port `9120`; Periphery meldet erfolgreiche Verbindung zu Core.
|
||||
|
||||
**Stufe E - Web-UI und GitOps validieren**
|
||||
|
||||
1. `https://komodo.kaleschke.info` ist erreichbar (Authelia-Bypass dokumentiert, native Komodo-Auth aktiv).
|
||||
2. Komodo zeigt im Web-UI die bekannten Stacks aus Gitea (sobald Gitea ebenfalls laeuft; siehe `docs/DISASTER_RECOVERY.md` Phase 4 Stufe 2 vor Stufe 3).
|
||||
3. Gitea-Webhooks gegen Komodo werden separat in der Phase-4-Reihenfolge geprueft, **nicht** als Teil des Komodo-Bootstraps.
|
||||
|
||||
Erfolgskriterium: Komodo-UI laedt, Periphery `Online`, mindestens ein Stack aus Gitea sichtbar.
|
||||
|
||||
**Stufe F - Stacks in Tier-Reihenfolge aufnehmen**
|
||||
|
||||
Erst nach erfolgreichem Komodo-Bootstrap werden produktive Stacks ueber den dokumentierten Stufenpfad in `docs/DISASTER_RECOVERY.md` Phase 4 hochgefahren (Traefik, AdGuard, Tailscale, dann PostgreSQL, Authelia, Redis, Gitea, dann Apps).
|
||||
|
||||
### Trockenlauf (als Repo-Skript, bestaetigt)
|
||||
|
||||
Trockenlauf gegen Wegwerf-Pfade ist seit 2026-05-29 als Repo-Skript abgelegt: `ops/restore-tests/komodo-bootstrap-{compose.test.yml,test.sh,plan.md,runbook.md}`. Aufruf:
|
||||
|
||||
```bash
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/komodo-bootstrap-test.sh --what-if # nur Plan
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/komodo-bootstrap-test.sh --keep-data # echter Lauf
|
||||
```
|
||||
|
||||
Erstlauf 2026-05-30 erfolgreich: `SUCCESS`, alle 5 Checks gruen (compose config valid, Mongo healthy, Mongo authenticated ping ok, Komodo Core HTTP `200`, Test-Periphery `running`). Report unter `/mnt/user/backups/restore-reports/komodo-bootstrap-2026-05-30.md`. Produktive Komodo-Container, Mongo-Datadir und Secrets wurden nicht beruehrt.
|
||||
|
||||
Test-Isolation:
|
||||
|
||||
| Bereich | Wegwerf-Wert |
|
||||
|---|---|
|
||||
| Compose-Project | `restoretest-komodo` (isoliert von Produktions-Project `komodo`) |
|
||||
| Test-Mongo-Datadir | `/mnt/user/backups/restore-lab/komodo/mongo` |
|
||||
| Test-Port | `127.0.0.1:19120` (kein LAN, kein Traefik) |
|
||||
| Test-Periphery | ohne `docker.sock`-Mount, ohne `/mnt/user/services`-Mount |
|
||||
| `KOMODO_*`-Secrets | Wegwerf-Werte im Test-Compose, niemals produktive Werte |
|
||||
|
||||
Damit ist `ops/komodo/docker-compose.yml` als Recovery-Anker fuer Stufen A-F **belegt** tauglich, nicht nur angenommen tauglich.
|
||||
|
||||
### Validierungs-Kommandos (Snapshot)
|
||||
|
||||
```bash
|
||||
# Compose syntaktisch ok?
|
||||
docker compose -f ops/komodo/docker-compose.yml config >/dev/null
|
||||
|
||||
# Komodo-Container vorhanden und laufend?
|
||||
docker ps --filter "name=komodo" --format "table {{.Names}}\t{{.Status}}"
|
||||
|
||||
# Mongo Health?
|
||||
docker exec komodo-mongo mongosh --quiet --eval 'db.adminCommand({ping:1}).ok'
|
||||
|
||||
# Core API up?
|
||||
docker exec komodo-core sh -lc 'wget -q -O- http://127.0.0.1:9120/api/health || true'
|
||||
|
||||
# Periphery sichtbar?
|
||||
docker logs --tail 50 komodo-periphery 2>&1 | grep -i "connected\|periphery"
|
||||
```
|
||||
|
||||
## Secrets Recovery Reihenfolge
|
||||
@@ -88,13 +197,15 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Fuer den Kaltstart ist diese Reihenfolge
|
||||
- Keine Secret-Werte in Git oder Tickets kopieren.
|
||||
- Restore-Tests laufen in Wegwerfpfaden, nie direkt gegen produktive Pfade.
|
||||
- Wenn Gitea und Komodo beide down sind, gewinnt der externe GitHub-Mirror als Repo-Quelle.
|
||||
- Wenn Borg ohne Passphrase nicht entschluesselbar ist, ist Recovery blockiert. Deshalb ist die analoge Passphrase-Sicherung P0.
|
||||
- Wenn Borg ohne Passphrase nicht entschluesselbar ist, ist Recovery blockiert. Die Offline-Sicherung wurde am 2026-05-26 vom Operator bestaetigt; bei Reviews nur pruefen, dass sie weiterhin auffindbar und lesbar ist.
|
||||
|
||||
## Naechste Aufgaben
|
||||
|
||||
| Status | Aufgabe |
|
||||
|---|---|
|
||||
| offen | Gitea-Bundle- oder Mirror-Mechanik final entscheiden |
|
||||
| erledigt (Skript + Host-Test) | Gitea-Bundle- oder Mirror-Mechanik final entscheiden |
|
||||
| erledigt | Komodo-Bootstrap-Quelle finalisieren |
|
||||
| offen | Restore-Kommandos nach erstem Trockenlauf mit echten Pfaden ergaenzen |
|
||||
| erledigt (Doku) | Komodo-Kaltstart in linearen Stufen A-F dokumentieren |
|
||||
| erledigt 2026-05-29 | Komodo-Trockenlauf-Skript in `ops/restore-tests/` analog zu Immich vorbereiten |
|
||||
| erledigt 2026-05-30 | Restore-Kommandos nach erstem Trockenlauf mit echten Pfaden ergaenzen |
|
||||
| erledigt | Services-Recovery in `docs/DISASTER_RECOVERY.md` verlinken |
|
||||
|
||||
@@ -28,7 +28,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| `postgresql17` | shared PostgreSQL Cluster | `infra/postgresql17/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/postgresql17`, `postgres_password.txt` | Tier 1; Dumps unter `/mnt/user/backups/borg/dumps/latest` | nein | keine Host-Ports; raw DB nicht primaerer Restore-Weg |
|
||||
| `Redis` | shared Redis Cache | `infra/redis/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/redis`, `redis_password.txt` | transiente Daten, bewusst nicht kritisch | nein | Passwort-Datei; optional named volume offen |
|
||||
| `Redis` | primaer Paperless-Redis (App-Cache); historisch als "shared" angelegt, faktisch nur von Paperless genutzt | `infra/redis/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/redis`, `redis_password.txt` | transiente Daten, bewusst nicht kritisch | nein | Passwort-Datei; optional named volume offen. Immich, Nextcloud und Mealie nutzen jeweils eigene Redis-Instanzen; Authelia laeuft bewusst ohne Redis-Session-Backend. Bei Wegfall ist Paperless der einzige betroffene Stack. |
|
||||
| `ddns-updater` | Cloudflare/DDNS Aktualisierung | `infra/ddns-updater/docker-compose.yml` | intern | Internetzugang, `frontend_net` | `/mnt/user/appdata/ddns-updater` | rebuildbar | nein | bleibt bewusst in `frontend_net`, weil `backend_net` internal ist |
|
||||
|
||||
## Public / User Apps
|
||||
@@ -36,7 +36,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| `paperless-ngx` | Dokumentenmanagement | `apps/paperless/docker-compose.yml` | `https://paperless.kaleschke.info` | PostgreSQL 17, Redis, Traefik | `/mnt/user/appdata/paperless-ngx/data`, `/mnt/user/documents/paperless`, `/mnt/user/documents/scans_inbox` | Tier 2, Borg + `postgresql17-paperless.dump` | ja | DB/Redis Secrets bleiben bewusst Stack ENV |
|
||||
| `paperless-gpt` | KI-Ergaenzung fuer Paperless | `apps/paperless-gpt/docker-compose.yml` | `https://paperless-gpt.kaleschke.info` | Paperless API, LLM/Ollama, Traefik | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` | Tier 2 | ja + Authelia | API Token als Stack ENV; OCR/LLM-Konfig bei Aenderungen pruefen |
|
||||
| `paperless-gpt` | KI-Ergaenzung fuer Paperless | `apps/paperless-gpt/docker-compose.yml` | `https://paperless-gpt.kaleschke.info` | Paperless API, LLM/Ollama, Traefik | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` | Tier 2 | ja + Authelia | API Token als Stack ENV; OCR/LLM-Konfig bei Aenderungen pruefen. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv, auch wenn aktuell keine Traefik-Zugriffe in der Woche; Ablouseplanung erst mit Paperless-NGX 3.0 (eigene KI-Features erwartet) - dann neu bewerten. |
|
||||
| `immich_server` | Foto-/Video-App | `apps/immich/docker-compose.yml` | `https://immich.kaleschke.info` | Immich Postgres, Immich Redis, ML, Traefik | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive` | Tier 2, Borg + `immich.dump` | ja | native App-Auth; externes Fotoarchiv gemountet |
|
||||
| `immich_postgres` | Immich-Datenbank | `apps/immich/docker-compose.yml` | intern | `immich_default` | `/mnt/user/appdata/immich_postgres`, `immich_postgres_password.txt` | Dump `immich.dump` | nein | nie ins `frontend_net` |
|
||||
| `immich_redis` | Immich Cache | `apps/immich/docker-compose.yml` | intern | `immich_default` | kein kritischer Pfad dokumentiert | rebuildbar | nein | Architektur nennt anonymes Volume -> named volume als offenes Thema |
|
||||
@@ -47,9 +47,9 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
||||
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
||||
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB |
|
||||
| `nextcloud-redis` | Nextcloud Cache/Locking | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/redis` | Teil von Nextcloud-Restore | nein | interne Redis |
|
||||
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml` | Plex native / LAN / Remote je Plex-Konfiguration | Host-Netz | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | nein | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme, kein Traefik-Stack |
|
||||
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml` | Plex native, **LAN/Tailscale-only**, Remote Access deaktiviert | Host-Netz | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | nein | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme. Server geclaimt von `Xeridos` (Reclaim 2026-05-28 nach Preferences-Reset vom 18.05.). Smart-TVs greifen ueber WLAN-LAN per mDNS/Plex-GDM direkt zu. `PublishServerOnPlexOnlineKey=0` (Remote Access aus), `RelayEnabled` ebenfalls aus. |
|
||||
| `ntfy` | Push-Benachrichtigungen | `apps/ntfy/docker-compose.yml` | `https://ntfy.kaleschke.info` | Traefik, upstream mobile push | `/mnt/user/appdata/ntfy` | Tier 2 | ja | `NTFY_BEHIND_PROXY=true`; Problem-Alerts gehen gebuendelt an `homelab-alerts`, optionale Erfolgsmeldungen an `homelab-info` |
|
||||
| `bentopdf` | PDF-Tooling / Ersatz fuer Stirling-PDF | `apps/bentopdf/docker-compose.yml` | `https://pdf.kaleschke.info` | Traefik + Authelia | keine kritische Persistenz im Compose | Tier 3, rebuildbar | ja + Authelia | COOP/COEP per Middleware; fachliche Abnahme/Live-Status pruefen |
|
||||
| `bentopdf` | PDF-Tooling / Ersatz fuer Stirling-PDF | `apps/bentopdf/docker-compose.yml` | `https://pdf.kaleschke.info` | Traefik + Authelia | keine kritische Persistenz im Compose | Tier 3, rebuildbar | ja + Authelia | COOP/COEP per Middleware. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv als situatives Tool, auch wenn aktuell keine Traefik-Zugriffe in der Woche. Resource-Footprint vernachlaessigbar (~4 MB RAM). |
|
||||
|
||||
## Operations / Monitoring / Admin
|
||||
|
||||
@@ -82,7 +82,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
||||
|
||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| `posture-check` | Host-Posture-Audit fuer Filesystem, Mover-Drift, NVMe-SMART und Fuellstand | `services/posture-check/posture-check.sh` | Unraid User-Script / Cron / Borg Pre-Hook | `findmnt`, `df`, `nvme`, optional `curl` fuer ntfy | `/mnt/user/services/posture-check/last.json` | Repo-Skript + letzter JSON-Status | nein | Muss auf dem Unraid-Host bei Boot, stuendlich und vor Borg laufen; Disk1-NTFS ist nach Disk1 Phase 2 nicht mehr erlaubt (`ALLOW_DISK1_NTFS=0` Standard); Warning/Critical alarmieren via ntfy nur bei neuer Ursache oder nach `ALERT_REPEAT_SECONDS` |
|
||||
| `posture-check` | Host-Posture-Audit fuer Filesystem, Mover-Drift, NVMe-SMART, Fuellstand und Authelia-Repo<->Host-Drift | `services/posture-check/posture-check.sh` | Unraid User-Script / Cron / Borg Pre-Hook | `findmnt`, `df`, `nvme`, optional `curl` fuer ntfy; ruft `services/authelia-diff.sh` fuer `authelia_config_drift` auf | `/mnt/user/services/posture-check/last.json` | Repo-Skript + letzter JSON-Status | nein | Muss auf dem Unraid-Host bei Boot, stuendlich und vor Borg laufen; Disk1-NTFS ist nach Disk1 Phase 2 nicht mehr erlaubt (`ALLOW_DISK1_NTFS=0` Standard); Warning/Critical alarmieren via ntfy nur bei neuer Ursache oder nach `ALERT_REPEAT_SECONDS`. Authelia-Drift-Check braucht einen Repo-Spiegel unter `/mnt/user/services/homelab-infra/` (siehe `docs/WORKFLOW.md` Sektion "Ausnahme: Authelia configuration.yml") |
|
||||
| `docker-critical-events` | Live-Alarmierung fuer Docker `die`/`oom`/`kill` Events | `services/posture-check/docker-critical-events.sh` | Unraid User-Script / Hintergrundprozess | Docker CLI, ntfy | `/mnt/user/services/posture-check/docker-critical-events-last.log` | Repo-Skript + letzter Event-Log | nein | Optional als Unraid User-Script `at array start` starten; sendet nach `homelab-alerts` |
|
||||
|
||||
## Backup- und Restore-Hinweise
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
| Feld | Wert |
|
||||
|------|------|
|
||||
| Version | 1.3 |
|
||||
| Status | **Active** — bindend, commit-reif als `docs/STORAGE_LAYOUT.md` |
|
||||
| Datum | 2026-05-15 |
|
||||
| Version | 1.4 |
|
||||
| Status | **Active** — bindend als `docs/STORAGE_LAYOUT.md` |
|
||||
| Datum | 2026-05-27 |
|
||||
| Geltungsbereich | Unraid-Host `Kallilabcore`, alle Pools, Array-Disks, User-Shares, Appdata-Strukturen |
|
||||
| Verbindlichkeit | Bindend ab Inkraftsetzung. Abweichungen nur dokumentiert als Ausnahme (siehe Abschnitt 12) |
|
||||
| Vorgänger | keiner; entstanden aus Incident 2026-05-11 (NTFS-Cache-Korruption) |
|
||||
@@ -38,13 +38,13 @@ Es ist **vor** jeder Storage- oder Compose-Änderung zu lesen. Wenn ein neuer St
|
||||
|
||||
| Slot | Device | Filesystem | Größe | Zweck | Status nach Recovery |
|
||||
|------|--------|------------|-------|-------|----------------------|
|
||||
| Cache (Pool) | Samsung 970 EVO Plus, NVMe | **XFS** | 2 TB | Appdata, system, domains | Reformat von NTFS auf XFS, Phase 1 |
|
||||
| Disk1 (Array) | HDD (Modell TBD) | **XFS** | TBD | Nutzdaten, Backups, Services | NTFS-zu-XFS-Migration Phase 2 abgeschlossen am 2026-05-25 |
|
||||
| Parity | HDD (Modell TBD) | — (keine FS) | TBD | Redundanz für Array | Unverändert |
|
||||
| Boot | USB-Stick | FAT32 | klein | Unraid-OS, Konfiguration | Unverändert, regelmäßig per Flash-Backup gesichert |
|
||||
| Externe Backup-Platte | Wechselplatte (Modell TBD) | XFS oder ext4 | ~8 TB | Ausgelagertes Backup-Ziel, Recovery-Material | NEU, ersetzt WD MyBookLive |
|
||||
| Cache (Pool) | Samsung SSD 970 EVO Plus 2TB, NVMe (`S4J4NM0W609649H`) | **XFS** | 1.8T nutzbar | Appdata, system, domains | Reformat von NTFS auf XFS, Phase 1 |
|
||||
| Disk1 (Array) | WDC WD60EFAX-68JH4N1 (`WD-WX32D90PC0V0`) | **XFS** auf `md1p1` | 5.5T nutzbar | Nutzdaten, Backups, Services | NTFS-zu-XFS-Migration Phase 2 abgeschlossen am 2026-05-25 |
|
||||
| Parity | TOSHIBA HDWG480 (`2460A03VFA3H`) | — (keine FS) | 7.3T | Redundanz für Array | Unverändert |
|
||||
| Boot | Samsung Flash Drive (`0375125090000587`) | FAT32 | 59.8G | Unraid-OS, Konfiguration | Regelmäßig per Flash-Backup gesichert |
|
||||
| Externe Backup-Platte | H:/ `Externe HDD` am Windows-PC | NTFS | 8.0T | Nearline-Pull-Ziel für kritische Restore-Artefakte | Kein Off-site-/Airgap-Ersatz; Pull-Workflow in `docs/H_DRIVE_NEARLINE_PULL.md` |
|
||||
|
||||
**TBD-Felder werden nachgetragen** sobald `lsblk -o NAME,SIZE,MODEL,SERIAL,VENDOR /dev/sd?` und `smartctl -i` für jeden Slot einmal ausgeführt wurden. Vorschlag: dieser Detection-Lauf ist Teil der Posture-Check-Erstinstallation (§11) und das Ergebnis wird automatisch in dieses Dokument eingetragen. Kein Blocker für die Inkraftsetzung — die Festsetzung „XFS auf Cache, XFS auf Disk1" steht unabhängig von Modell/Größe.
|
||||
Physikalische Basisdaten sind aus `docs/HARDWARE_INVENTORY.md` und dem Host-Readout vom 2026-05-27 übernommen. Detailwerte zu SMART/Health bleiben dort die autoritative Quelle; dieses Dokument hält die Storage-Policy.
|
||||
|
||||
**Begründung Filesystem-Wahl:**
|
||||
|
||||
@@ -378,6 +378,7 @@ Wenn Hermes-Worker auf weiteren Hosts skaliert: dieser Storage-Layout-Plan gilt
|
||||
- `docs/DISASTER_RECOVERY.md` — Konkrete Recovery-Pläne, inkl. NTFS-Migration
|
||||
- `docs/GITOPS_DRIFT_RUNBOOK.md` — Drift-Erkennung und -Behebung
|
||||
- `docs/AI_CONTEXT.md` — Kontext für AI-Assistenten
|
||||
- `docs/HARDWARE_INVENTORY.md` — physische Host-, Disk- und Health-Baseline
|
||||
|
||||
## 18. Changelog
|
||||
|
||||
@@ -387,6 +388,7 @@ Wenn Hermes-Worker auf weiteren Hosts skaliert: dieser Storage-Layout-Plan gilt
|
||||
| 1.1 (Draft) | 2026-05-15 | Operator-Review-Feedback eingearbeitet: `system`/`domains` auf `only`, `services` als recovery-kritisch markiert, `data/`-Behandlung pro Stack klassifiziert (statt blanket Exclude), Backup-Tooling Ist/Soll explizit getrennt, Posture-Check zusätzlich bei Boot/vor Backup/nach Mover, Hard Rules 11+12 ergänzt (Restore-Pfad-Pflicht, Posture-vor-Backup), Alarmziel-Optionen benannt, Review-Items in eigene Sektion §20 verschoben. | Operator + AI-Assistenten |
|
||||
| 1.2 (Draft) | 2026-05-15 | Operator-Entscheidungen #3, #4, #6, #9, #11 eingearbeitet: Backrest abgeschaltet (Borg alleinige Backup-Technologie), persönliche Daten vollständig im Pflicht-Backup-Scope, ntfy als Alarmziel verbindlich, kebab-case-Migration im Rahmen der Recovery-Phase, Mirror-Backup für Gitea-Repo-Inhalte als verbindliche Spec (Implementierung in `SERVICES_RECOVERY.md` zu detaillieren). Offen: Items #1, #2, #5, #7, #8, #10. | Operator + AI-Assistenten |
|
||||
| 1.3 (Draft) | 2026-05-15 | Operator-Entscheidungen #1, #7, #8 eingearbeitet: Disk-Größen/-Modelle als Deferred via Posture-Check-Detection (kein Blocker), optionale Stacks (Filebrowser, code-server, Speedtest, Scrutiny, Uptime-Kuma) bleiben im Layout und sind produktiv, Network-Verweis auf MASTER_V2 bestätigt. Damit alle akuten Items entschieden. Verbleibend: Items #2, #5, #10 (Retention, Schwellen-Kalibrierung, RESTORE_MATRIX-Klassifikation) — alle als Folge-Aufgaben über Inkraftsetzung hinaus, kein Commit-Blocker. | Operator + AI-Assistenten |
|
||||
| 1.4 (Active) | 2026-05-27 | Datei von `docs/STORAGE_LAYOUT.draft.md` auf `docs/STORAGE_LAYOUT.md` gehoben; Hardware-/Diskwerte aus `docs/HARDWARE_INVENTORY.md` übernommen; Gitea-Bundle-Mirror und H:/ Nearline-Pull als umgesetzte Folgepfade referenziert. Verbleibend: Retention-Kalibrierung, Monitoring-Schwellen und RESTORE_MATRIX-Detailklassifikation als normale Folgeaufgaben. | Operator + AI-Assistenten |
|
||||
|
||||
## 19. Inkraftsetzung
|
||||
|
||||
@@ -394,13 +396,13 @@ Dieses Dokument tritt in Kraft mit dem Commit der finalen Fassung in `master`-Br
|
||||
|
||||
Erste Audit-Review dieses Dokuments: spätestens 90 Tage nach Inkraftsetzung. Danach jährlich oder bei jeder strukturellen Änderung des Storage-Layouts.
|
||||
|
||||
## 20. Open Review Items (vor finalem Commit zu entscheiden)
|
||||
## 20. Review Items und Folgeaufgaben
|
||||
|
||||
Diese Sektion dokumentiert offene Operator-Entscheidungen und Lücken. **Vor Statuswechsel von Draft auf Active ist jeder Punkt entweder eingearbeitet oder bewusst als „bleibt offen" mit Verweis auf Folge-Issue/-Doc markiert.**
|
||||
Diese Sektion dokumentiert erledigte Review-Punkte und verbleibende Folgeaufgaben nach Aktivierung des Dokuments.
|
||||
|
||||
| Nr. | Item | Status | Verantwortung |
|
||||
|-----|------|--------|---------------|
|
||||
| 1 | Disk-Größen und Modelle in §3 (Disk1, Parity, externe Backup-Platte) | **DEFERRED 2026-05-15** — wird automatisch via Posture-Check-Detection-Lauf ergänzt; Disk1-Filesystem ist seit 2026-05-25 XFS | Operator + Posture-Check |
|
||||
| 1 | Disk-Größen und Modelle in §3 (Disk1, Parity, externe Backup-Platte) | **ERLEDIGT 2026-05-27** — Werte aus `docs/HARDWARE_INVENTORY.md` übernommen; Disk1-Filesystem ist seit 2026-05-25 XFS | Operator + Posture-Check |
|
||||
| 2 | Retention-Werte in §8.1 (Borg-Repos lokal/remote) — abhängig von tatsächlicher Storage-Kapazität | Vorschlag steht, anzupassen | Operator |
|
||||
| 3 | Backup-Tooling: Backrest abschalten, Borg alleinige Backup-Technologie | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §8.0) |
|
||||
| 4 | Backup-Scope für persönliche Daten: `documents`, `photos`, `finance`, `projekte` (und `media` falls behalten) **vollständig** im Pflicht-Scope | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §8.2) |
|
||||
@@ -410,6 +412,6 @@ Diese Sektion dokumentiert offene Operator-Entscheidungen und Lücken. **Vor Sta
|
||||
| 8 | Verweis auf `HOMELAB_ARCHITECTURE_MASTER_V2.md` für Network-Architektur (§10) — Net-Architektur steht dort authoritativ, Verweis ist ausreichend | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §10) |
|
||||
| 9 | Naming-Konvention: kebab-case durchziehen, Migration im Rahmen der Recovery-Phase | **ENTSCHIEDEN 2026-05-15** | erledigt (siehe §6); pro Stack in RESTORE_MATRIX.md zu dokumentieren |
|
||||
| 10 | Pro-Stack-Klassifizierung in `RESTORE_MATRIX.md` (DB-Typ, Nutzdaten in `data/`, Dump-Verfahren, letzter Restore-Test, kebab-case-Migrationsname) — als Folge-Aufgabe aus Hard Rule §12.11 | Folge-Aufgabe | Operator + Recovery-Phase |
|
||||
| 11 | Mirror-Backup für `services/gitea/git/repositories/` auf zweites Medium, ≤ 6 h Frequenz, konkrete Implementierung in `docs/SERVICES_RECOVERY.md` zu erstellen | **ENTSCHIEDEN 2026-05-15**, Implementierungs-Doc offen | Operator + Folge-Doc |
|
||||
| 11 | Mirror-Backup für `services/gitea/git/repositories/` auf zweites Medium, ≤ 6 h Frequenz, konkrete Implementierung in `docs/SERVICES_RECOVERY.md` zu erstellen | **ERLEDIGT 2026-05-26** — `ops/borg-ui/scripts/gitea-bundle-mirror.sh` erzeugt verifizierte Bundles; Host-Erstlauf mit 4 Bundles und `git fsck` erfolgreich | Operator + Folge-Doc |
|
||||
|
||||
Wenn alle 11 Punkte bearbeitet sind und der Operator die Datei reviewed hat, wird sie als `docs/STORAGE_LAYOUT.md` (ohne `.draft`) committed und Status auf `Active` gesetzt.
|
||||
Das Dokument ist mit Version 1.4 als Active geführt. Offene Punkte 2, 5 und 10 bleiben normale Folgeaufgaben und blockieren die Gültigkeit der Hard Rules nicht.
|
||||
@@ -269,6 +269,42 @@ Diese Ausnahme bleibt bewusst bestehen. Der File-Provider wird weiterhin nur fue
|
||||
|
||||
---
|
||||
|
||||
## Ausnahme: Authelia configuration.yml
|
||||
|
||||
> **Diese Datei wird von Komodo nicht automatisch deployed.**
|
||||
|
||||
`security/authelia/configuration.yml` ist die Repo-Baseline fuer nicht geheime Einstellungen (Access-Control, Session, Storage-Struktur, Notifier, TOTP). Die produktive Host-Datei darf zusaetzlich OIDC-Clients und hostseitige Identity-Provider-Konfiguration enthalten. Secret-Werte und die User-Datenbank bleiben grundsaetzlich ausserhalb von Git.
|
||||
|
||||
| Git-Pfad | Host-Pfad (NAS) |
|
||||
|---|---|
|
||||
| `security/authelia/configuration.yml` | `/mnt/user/appdata/authelia/config/configuration.yml` |
|
||||
|
||||
### Pflicht-Workflow bei Aenderungen an `configuration.yml`
|
||||
|
||||
1. Datei im Git-Repo (`security/authelia/`) aendern.
|
||||
2. Commit + Push.
|
||||
3. Aenderung manuell in die Host-Datei mergen, OIDC-/Identity-Provider-Sektionen erhalten.
|
||||
4. `docker restart authelia` und Login-Smoke-Test auf einer ACL-betroffenen Domain.
|
||||
5. `services/authelia-diff.sh` (Default-Aufruf) muss `exit 0` liefern.
|
||||
|
||||
### Automatische Drift-Erkennung
|
||||
|
||||
`services/authelia-diff.sh` vergleicht die `access_control:`-Sektion zwischen Repo-Baseline und Host-Datei. Der Posture-Check (`services/posture-check/posture-check.sh`) ruft das Skript als Check `authelia_config_drift` auf und meldet Drift als Warning via ntfy.
|
||||
|
||||
Konfigurierbare Variablen (Defaults sind das produktive Zielbild):
|
||||
|
||||
- `AUTHELIA_REPO_BASELINE` — Pfad zur Repo-Datei auf dem Host, Default `/mnt/user/services/homelab-infra/security/authelia/configuration.yml`
|
||||
- `AUTHELIA_HOST_CONFIG` — Pfad zur produktiven Host-Datei, Default `/mnt/user/appdata/authelia/config/configuration.yml`
|
||||
- `AUTHELIA_DIFF_SECTIONS` — Komma-Liste der zu vergleichenden Top-Level-Sektionen, Default `access_control`
|
||||
- `AUTHELIA_DIFF_SCRIPT` — Pfad zum Diff-Skript fuer den Posture-Check, Default `/mnt/user/services/homelab-infra/services/authelia-diff.sh`
|
||||
- `SKIP_AUTHELIA_DRIFT=1` — Check im Posture-Check ueberspringen
|
||||
|
||||
Pflicht-Setup auf dem Host: Repo-Spiegel unter `/mnt/user/services/homelab-infra/` (Read-only-Clone von Gitea `Micha/homelab-infra`, regelmaessig `git pull --ff-only`). Ohne Repo-Spiegel meldet der Check Warning, weil die Baseline-Datei fehlt — Critical wird der Check bewusst nicht.
|
||||
|
||||
> **Merksatz:** Push allein reicht hier nicht. Ohne den manuellen Merge ins Host-Configfile wirkt die Aenderung nicht, und der Drift-Check wuerde Warning melden.
|
||||
|
||||
---
|
||||
|
||||
## Secrets-Regeln
|
||||
|
||||
- Secrets liegen niemals im Repository
|
||||
@@ -311,6 +347,26 @@ dns:
|
||||
|
||||
---
|
||||
|
||||
## Service-Removal-Checkliste
|
||||
|
||||
Wenn ein Stack endgueltig entfernt wird (Beispiele: Homepage am 2026-05-25, Uptime-Kuma am 2026-05-25, Jellyfin am 2026-05-25), muss in **einem** Aenderungsblock auch der gesamte Sicht-/Backup-Pfad nachgezogen werden, sonst entstehen "Tote-Pfad-Warnings", die erst Tage spaeter auftauchen.
|
||||
|
||||
Pflicht-Schritte vor dem Schliessen:
|
||||
|
||||
1. Komodo: Stack stoppen, destroy, Stack-Eintrag loeschen.
|
||||
2. Gitea-Webhook fuer den Stack deaktivieren.
|
||||
3. Repo-Pfad per `git rm` entfernen.
|
||||
4. Appdata nach `/mnt/user/appdata/_archive/<name>-removed-YYYY-MM-DD/` verschieben (14 Tage Karenz).
|
||||
5. DNS-Eintrag im Cloudflare entfernen, sofern Public-Domain.
|
||||
6. Authelia ACL-Eintrag in `security/authelia/configuration.yml` und auf dem Host bereinigen.
|
||||
7. Monitoring: Blackbox-Target in `monitoring/blackbox/blackbox.yml` entfernen, Cert-Check-Liste pruefen.
|
||||
8. **Borg-UI Source-Liste**: `https://borg.kaleschke.info` -> Repository `appdata-critical` -> Source Directories -> alle `/local/appdata/<name>` und ggf. `/local/<name>`-Eintraege loeschen. Sonst kommen daily `HomelabBorgLastJobCompletedWithWarnings`-Push-Nachrichten mit `BackupFileNotFoundError` im Logfile.
|
||||
9. `docs/SERVICE_CATALOG.md`, `docs/REPO_MAP.md`, `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 7.8 (Entfernt), `docs/MIGRATION_LOG.md` nachziehen.
|
||||
|
||||
Wenn ein Stack `webhook_enabled` in Komodo hatte, zusaetzlich pruefen, ob der zugehoerige Gitea-Hook deaktiviert oder geloescht wurde.
|
||||
|
||||
---
|
||||
|
||||
## Dokumentationspflicht
|
||||
|
||||
Nach jeder erfolgreichen Migration oder relevanten Aenderung muessen diese Dateien geprueft werden:
|
||||
|
||||
@@ -21,6 +21,13 @@ services:
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U mailarchiver -d mailarchiver"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
networks:
|
||||
backend_net:
|
||||
external: true
|
||||
|
||||
@@ -18,6 +18,13 @@ services:
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "redis-cli -a \"$$(cat /run/secrets/redis_password)\" --no-auth-warning ping | grep -q PONG"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 15s
|
||||
|
||||
networks:
|
||||
backend_net:
|
||||
external: true
|
||||
|
||||
@@ -62,6 +62,7 @@ INFLUXDB_BIND_IP=192.168.178.58
|
||||
- `https://monitoring.kaleschke.info` leitet zu Authelia.
|
||||
- Grafana-Datasources `Prometheus`, `Loki` und `InfluxDB 3 Core` testen erfolgreich.
|
||||
- Prometheus Targets: `prometheus`, `node-exporter`, `cadvisor`, `traefik`, `blackbox-http`.
|
||||
- Node Exporter Textfile Collector: `/mnt/user/services/posture-check/textfile/homelab.prom` wird vom Host-Skript `services/posture-check/export-prometheus-textfile.sh` befuellt.
|
||||
- Alertmanager ist erreichbar und sendet ueber `monitoring-alertmanager-ntfy-bridge` nach `https://ntfy.kaleschke.info/homelab-alerts`.
|
||||
- Loki zeigt Container-Logs mit Labels `container`, `compose_project`, `compose_service`.
|
||||
- InfluxDB 3 Core enthaelt die Datenbank `homelab`.
|
||||
@@ -83,9 +84,17 @@ Blackbox-HTTP-Alerts unterscheiden zwischen einem einzelnen kaputten Endpoint un
|
||||
|
||||
- `HomelabExternalConnectivityDown` feuert, wenn mindestens 5 Public-Endpoints gleichzeitig fuer 8 Minuten nicht erreichbar sind. Das deckt WAN-, DNS- oder Provider-Ausfaelle ab, inklusive laengerer DSL-Reconnects.
|
||||
- `HomelabEndpointDown` feuert fuer einzelne Endpoints erst nach 8 Minuten und wird unterdrueckt, solange der Sammelalert aktiv ist. Dadurch erzeugt ein Telekom-24h-Reconnect keine ntfy-Flut pro Domain.
|
||||
- `HomelabCertificateExpiresSoon` und `HomelabCertificateExpiresCritical` nutzen Blackbox TLS-Metriken fuer 21-/7-Tage-Warnungen.
|
||||
- `HomelabBorgBackupStale`, `HomelabBorgLastJobFailed`, `HomelabBorgLastJobCompletedWithWarnings` und `HomelabCriticalContainerDown` nutzen Host-Textfile-Metriken. Voraussetzung: `services/posture-check/export-prometheus-textfile.sh` laeuft regelmaessig auf dem Host, empfohlen alle 15 Minuten.
|
||||
|
||||
Test:
|
||||
|
||||
```bash
|
||||
curl -fsS http://alertmanager-ntfy-bridge:8080/healthz
|
||||
```
|
||||
|
||||
Textfile-Metriken manuell aktualisieren:
|
||||
|
||||
```bash
|
||||
bash /mnt/user/services/homelab-infra/services/posture-check/export-prometheus-textfile.sh
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.7.3
|
||||
image: prom/prometheus:v3.7.3@sha256:49214755b6153f90a597adcbff0252cc61069f8ab69ce8411285cd4a560e8038
|
||||
container_name: monitoring-prometheus
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -25,7 +25,7 @@ services:
|
||||
- cadvisor
|
||||
|
||||
alertmanager:
|
||||
image: prom/alertmanager:v0.28.1
|
||||
image: prom/alertmanager:v0.28.1@sha256:27c475db5fb156cab31d5c18a4251ac7ed567746a2483ff264516437a39b15ba
|
||||
container_name: monitoring-alertmanager
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -42,7 +42,7 @@ services:
|
||||
- no-new-privileges:true
|
||||
|
||||
alertmanager-ntfy-bridge:
|
||||
image: python:3.13-alpine
|
||||
image: python:3.13-alpine@sha256:420cd0bf0f3998275875e02ecd5808168cf0843cbb4d3c536432f729247b2acc
|
||||
container_name: monitoring-alertmanager-ntfy-bridge
|
||||
restart: unless-stopped
|
||||
dns:
|
||||
@@ -63,7 +63,7 @@ services:
|
||||
- no-new-privileges:true
|
||||
|
||||
blackbox-exporter:
|
||||
image: prom/blackbox-exporter:v0.27.0
|
||||
image: prom/blackbox-exporter:v0.27.0@sha256:a50c4c0eda297baa1678cd4dc4712a67fdea713b832d43ce7fcc5f9bea05094d
|
||||
container_name: monitoring-blackbox-exporter
|
||||
restart: unless-stopped
|
||||
dns:
|
||||
@@ -81,7 +81,7 @@ services:
|
||||
- no-new-privileges:true
|
||||
|
||||
loki:
|
||||
image: grafana/loki:3.7.2
|
||||
image: grafana/loki:3.7.2@sha256:191d4fdfb7264f16989f0a57f320872620a5a7c2ceeec6229212c4190ec49b86
|
||||
container_name: monitoring-loki
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -97,7 +97,7 @@ services:
|
||||
- no-new-privileges:true
|
||||
|
||||
promtail:
|
||||
image: grafana/promtail:3.6.10
|
||||
image: grafana/promtail:3.6.10@sha256:2a0f5e3e160ee5d549c585f6cc4f4e1c566ff783324a424bd75bc16503fc660e
|
||||
container_name: monitoring-promtail
|
||||
restart: unless-stopped
|
||||
command:
|
||||
@@ -115,7 +115,7 @@ services:
|
||||
- loki
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:12.4.3
|
||||
image: grafana/grafana:12.4.3@sha256:2e986801428cd689c2358605289c90ab37d2b39e24808874971f54c99bcdc412
|
||||
container_name: monitoring-grafana
|
||||
restart: unless-stopped
|
||||
dns:
|
||||
@@ -273,18 +273,20 @@ services:
|
||||
echo "Dashboard import complete."
|
||||
|
||||
node-exporter:
|
||||
image: prom/node-exporter:v1.9.1
|
||||
image: prom/node-exporter:v1.9.1@sha256:d00a542e409ee618a4edc67da14dd48c5da66726bbd5537ab2af9c1dfc442c8a
|
||||
container_name: monitoring-node-exporter
|
||||
restart: unless-stopped
|
||||
command:
|
||||
- --path.procfs=/host/proc
|
||||
- --path.sysfs=/host/sys
|
||||
- --path.rootfs=/rootfs
|
||||
- --collector.textfile.directory=/textfile
|
||||
- --collector.filesystem.mount-points-exclude=^/(dev|proc|sys|run|var/lib/docker/.+|var/lib/containers/storage/.+)($|/)
|
||||
volumes:
|
||||
- /proc:/host/proc:ro
|
||||
- /sys:/host/sys:ro
|
||||
- /:/rootfs:ro
|
||||
- /mnt/user/services/posture-check/textfile:/textfile:ro
|
||||
networks:
|
||||
- monitoring_net
|
||||
expose:
|
||||
@@ -293,7 +295,7 @@ services:
|
||||
- no-new-privileges:true
|
||||
|
||||
cadvisor:
|
||||
image: ghcr.io/google/cadvisor:v0.53.0
|
||||
image: ghcr.io/google/cadvisor:v0.53.0@sha256:c3770bd6fc6c6a9cb2b47143e6b3cc3fdd9d20a8453dffbb7e09a145e7e0c4e4
|
||||
container_name: monitoring-cadvisor
|
||||
restart: unless-stopped
|
||||
command:
|
||||
|
||||
@@ -28,6 +28,24 @@ groups:
|
||||
summary: "{{ $labels.instance }} is slow"
|
||||
description: "Blackbox probe duration is above 5 seconds for {{ $labels.instance }}."
|
||||
|
||||
- alert: HomelabCertificateExpiresSoon
|
||||
expr: (probe_ssl_earliest_cert_expiry{job="blackbox-http"} - time()) < 21 * 24 * 3600 and (probe_ssl_earliest_cert_expiry{job="blackbox-http"} - time()) > 7 * 24 * 3600
|
||||
for: 30m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "TLS certificate expires soon for {{ $labels.instance }}"
|
||||
description: "The earliest certificate expiry for {{ $labels.instance }} is below 21 days."
|
||||
|
||||
- alert: HomelabCertificateExpiresCritical
|
||||
expr: (probe_ssl_earliest_cert_expiry{job="blackbox-http"} - time()) <= 7 * 24 * 3600
|
||||
for: 15m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "TLS certificate is close to expiry for {{ $labels.instance }}"
|
||||
description: "The earliest certificate expiry for {{ $labels.instance }} is at or below 7 days, or already expired."
|
||||
|
||||
- name: homelab-host
|
||||
rules:
|
||||
- alert: HomelabDiskAlmostFull
|
||||
@@ -56,3 +74,59 @@ groups:
|
||||
annotations:
|
||||
summary: "Traefik 5xx responses for {{ $labels.service }}"
|
||||
description: "Traefik reports at least 5 5xx responses for {{ $labels.service }} within 5 minutes."
|
||||
|
||||
- name: homelab-backup-and-containers
|
||||
rules:
|
||||
- alert: HomelabTextfileExporterStale
|
||||
expr: time() - homelab_textfile_exporter_last_run_timestamp_seconds > 2 * 60 * 60
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Homelab textfile metrics are stale"
|
||||
description: "The host textfile exporter has not refreshed metrics for more than 2 hours."
|
||||
|
||||
- alert: HomelabBorgMetricsMissing
|
||||
expr: absent(homelab_borg_last_completed_timestamp_seconds)
|
||||
for: 15m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Borg backup metrics are missing"
|
||||
description: "Prometheus cannot see the homelab_borg_last_completed_timestamp_seconds metric."
|
||||
|
||||
- alert: HomelabBorgBackupStale
|
||||
expr: time() - homelab_borg_last_completed_timestamp_seconds > 30 * 60 * 60
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Borg backup is stale"
|
||||
description: "The latest completed Borg backup is older than 30 hours."
|
||||
|
||||
- alert: HomelabBorgLastJobFailed
|
||||
expr: homelab_borg_last_success != 1
|
||||
for: 15m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Latest Borg backup did not complete successfully"
|
||||
description: "The latest Borg UI job status is {{ $labels.status }} for archive {{ $labels.archive }}."
|
||||
|
||||
- alert: HomelabBorgLastJobCompletedWithWarnings
|
||||
expr: homelab_borg_last_job_warning == 1
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "Latest Borg backup completed with warnings"
|
||||
description: "The latest Borg UI job completed with warnings for archive {{ $labels.archive }}."
|
||||
|
||||
- alert: HomelabCriticalContainerDown
|
||||
expr: homelab_critical_container_running == 0
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
summary: "Critical container is down: {{ $labels.name }}"
|
||||
description: "The host textfile exporter reports that critical container {{ $labels.name }} is not running."
|
||||
|
||||
@@ -5,6 +5,7 @@ These scripts are intended to run on the Unraid host before a Borg backup starts
|
||||
## Current script
|
||||
|
||||
- `pre-backup-dumps.sh`
|
||||
- `gitea-bundle-mirror.sh`
|
||||
|
||||
## Output
|
||||
|
||||
@@ -12,7 +13,13 @@ Fresh dump artifacts are written to:
|
||||
|
||||
- `/mnt/user/backups/borg/dumps/latest`
|
||||
|
||||
Fresh Gitea repository bundles are written to:
|
||||
|
||||
- `/mnt/user/backups/git-bundles/gitea`
|
||||
|
||||
Borg UI should include `/local/borg-dumps` as a backup source.
|
||||
The Gitea bundle target should also be part of the Borg scope, either through
|
||||
the backups share or an explicit Borg source.
|
||||
|
||||
The dump set also includes `unraid-flash-config.tar.gz`, a host-generated
|
||||
archive of `/boot/config` plus checksum and manifest. Treat this archive as
|
||||
@@ -21,6 +28,8 @@ secret backup material.
|
||||
## Notes
|
||||
|
||||
- The script is written for host execution where `docker` is available.
|
||||
- `gitea-bundle-mirror.sh` additionally expects host access to the Gitea bare
|
||||
repositories under `/mnt/user/services/gitea/data/git/repositories`.
|
||||
- It does not assume Backrest.
|
||||
- It keeps only the latest dump set because Borg itself provides history.
|
||||
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
# Run this on the Unraid host. It creates verified git bundles for every bare
|
||||
# Gitea repository so a Gitea outage does not make repo bootstrap depend on the
|
||||
# Gitea application database.
|
||||
#
|
||||
# Bundles and their sha256 sidecars are written 0644 on purpose, so the
|
||||
# Nearline-Pull-Workflow (docs/H_DRIVE_NEARLINE_PULL.md) kann sie ueber den
|
||||
# SMB-Read-Share holen. Bundle-Inhalt = Git-Historie ohne Secrets (durch
|
||||
# .gitignore abgedeckt) und nicht sensibler als die uebrigen Dumps unter
|
||||
# /mnt/user/backups/borg/dumps/latest/, die ebenfalls 0644 sind.
|
||||
|
||||
SOURCE_ROOT="${SOURCE_ROOT:-/mnt/user/services/gitea/data/git/repositories}"
|
||||
BUNDLE_ROOT="${BUNDLE_ROOT:-/mnt/user/backups/git-bundles/gitea}"
|
||||
TMP_ROOT="${TMP_ROOT:-/mnt/cache/tmp/gitea-bundle-mirror}"
|
||||
REPORT_PATH="${REPORT_PATH:-$BUNDLE_ROOT/latest-report.md}"
|
||||
MANIFEST_PATH="${MANIFEST_PATH:-$BUNDLE_ROOT/manifest.tsv}"
|
||||
RUN_ID="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
|
||||
log() {
|
||||
printf '%s %s\n' "[gitea-bundles]" "$*"
|
||||
}
|
||||
|
||||
warn() {
|
||||
printf '%s %s\n' "[gitea-bundles][warn]" "$*" >&2
|
||||
}
|
||||
|
||||
need_cmd() {
|
||||
if ! command -v "$1" >/dev/null 2>&1; then
|
||||
warn "Required command missing: $1"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
bundle_target_for_repo() {
|
||||
repo="$1"
|
||||
rel="${repo#$SOURCE_ROOT/}"
|
||||
rel="${rel%.git}.bundle"
|
||||
printf '%s/%s\n' "$BUNDLE_ROOT" "$rel"
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_ROOT/run.$$"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
main() {
|
||||
need_cmd git
|
||||
need_cmd find
|
||||
need_cmd sha256sum
|
||||
|
||||
if [ ! -d "$SOURCE_ROOT" ]; then
|
||||
warn "Source root missing: $SOURCE_ROOT"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
run_tmp="$TMP_ROOT/run.$$"
|
||||
mkdir -p "$run_tmp" "$(dirname "$REPORT_PATH")" "$(dirname "$MANIFEST_PATH")"
|
||||
|
||||
manifest_tmp="$run_tmp/manifest.tsv"
|
||||
report_tmp="$run_tmp/latest-report.md"
|
||||
: > "$manifest_tmp"
|
||||
|
||||
total=0
|
||||
bundled=0
|
||||
skipped=0
|
||||
failed=0
|
||||
details="$run_tmp/details.txt"
|
||||
: > "$details"
|
||||
|
||||
repo_list="$run_tmp/repos.txt"
|
||||
find "$SOURCE_ROOT" -type d -name '*.git' | sort > "$repo_list"
|
||||
|
||||
while IFS= read -r repo; do
|
||||
total=$((total + 1))
|
||||
|
||||
if [ "$(git --git-dir="$repo" rev-parse --is-bare-repository 2>/dev/null || true)" != "true" ]; then
|
||||
skipped=$((skipped + 1))
|
||||
printf 'SKIP\t%s\tnot a bare repository\n' "$repo" >> "$details"
|
||||
printf '%s\t%s\t%s\t%s\n' "$total" "$bundled" "$skipped" "$failed" > "$run_tmp/counts"
|
||||
continue
|
||||
fi
|
||||
|
||||
target="$(bundle_target_for_repo "$repo")"
|
||||
target_dir="$(dirname "$target")"
|
||||
tmp="$run_tmp/$(basename "$target").tmp"
|
||||
target_tmp="$target_dir/.$(basename "$target").tmp"
|
||||
mkdir -p "$target_dir"
|
||||
|
||||
rel="${repo#$SOURCE_ROOT/}"
|
||||
log "Bundling $rel"
|
||||
|
||||
if git --git-dir="$repo" bundle create "$tmp" --all >/dev/null 2>&1 &&
|
||||
git --git-dir="$repo" bundle verify "$tmp" >/dev/null 2>&1; then
|
||||
chmod 644 "$tmp"
|
||||
rm -f "$target_tmp"
|
||||
cp "$tmp" "$target_tmp"
|
||||
chmod 644 "$target_tmp"
|
||||
mv "$target_tmp" "$target"
|
||||
rm -f "$tmp"
|
||||
git --git-dir="$repo" bundle verify "$target" >/dev/null 2>&1
|
||||
(
|
||||
cd "$target_dir"
|
||||
sha256sum "$(basename "$target")" > "$(basename "$target").sha256.tmp"
|
||||
)
|
||||
chmod 644 "$target.sha256.tmp"
|
||||
mv "$target.sha256.tmp" "$target.sha256"
|
||||
size_bytes="$(wc -c < "$target" | tr -d ' ')"
|
||||
printf '%s\t%s\t%s\t%s\n' "$RUN_ID" "$rel" "${target#$BUNDLE_ROOT/}" "$size_bytes" >> "$manifest_tmp"
|
||||
printf 'OK\t%s\t%s bytes\n' "$rel" "$size_bytes" >> "$details"
|
||||
bundled=$((bundled + 1))
|
||||
else
|
||||
failed=$((failed + 1))
|
||||
rm -f "$tmp"
|
||||
printf 'FAIL\t%s\tbundle create or verify failed\n' "$rel" >> "$details"
|
||||
fi
|
||||
|
||||
printf '%s\t%s\t%s\t%s\n' "$total" "$bundled" "$skipped" "$failed" > "$run_tmp/counts"
|
||||
done < "$repo_list"
|
||||
|
||||
if [ -f "$run_tmp/counts" ]; then
|
||||
IFS="$(printf '\t')" read -r total bundled skipped failed < "$run_tmp/counts"
|
||||
fi
|
||||
|
||||
{
|
||||
printf '# Gitea Bundle Mirror Report\n\n'
|
||||
printf 'Timestamp: %s\n' "$RUN_ID"
|
||||
printf 'Source: `%s`\n' "$SOURCE_ROOT"
|
||||
printf 'Target: `%s`\n' "$BUNDLE_ROOT"
|
||||
printf 'Total repositories: %s\n' "$total"
|
||||
printf 'Bundled: %s\n' "$bundled"
|
||||
printf 'Skipped: %s\n' "$skipped"
|
||||
printf 'Failed: %s\n\n' "$failed"
|
||||
printf '## Details\n\n'
|
||||
if [ -s "$details" ]; then
|
||||
while IFS="$(printf '\t')" read -r status name message; do
|
||||
printf -- '- `%s` %s: %s\n' "$status" "$name" "$message"
|
||||
done < "$details"
|
||||
else
|
||||
printf -- '- No repositories found.\n'
|
||||
fi
|
||||
} > "$report_tmp"
|
||||
|
||||
chmod 644 "$report_tmp" "$manifest_tmp"
|
||||
mv "$report_tmp" "$REPORT_PATH"
|
||||
mv "$manifest_tmp" "$MANIFEST_PATH"
|
||||
|
||||
log "Report written to $REPORT_PATH"
|
||||
[ "$failed" -eq 0 ]
|
||||
}
|
||||
|
||||
main "$@"
|
||||
@@ -37,7 +37,13 @@ ensure_dirs() {
|
||||
atomic_write() {
|
||||
target="$1"
|
||||
tmp="$2"
|
||||
mode="${3:-644}"
|
||||
mkdir -p "$(dirname "$target")"
|
||||
# Standard 0644, damit der Nearline-Pull-Workflow (docs/H_DRIVE_NEARLINE_PULL.md)
|
||||
# und Restore-Test-Skripte die Dumps per SMB-Read-Share oder unprivilegiert
|
||||
# lesen koennen. Sensible Sonderfaelle wie unraid-flash-config rufen mit
|
||||
# explizitem 600 auf, damit die bewusste Beschraenkung erhalten bleibt.
|
||||
chmod "$mode" "$tmp"
|
||||
mv "$tmp" "$target"
|
||||
}
|
||||
|
||||
@@ -179,15 +185,15 @@ backup_unraid_flash_config() {
|
||||
--exclude='config/plugins/*/*.zip' \
|
||||
--exclude='config/plugins/*/*.md5' \
|
||||
-czf "$tmp" config
|
||||
chmod 600 "$tmp"
|
||||
atomic_write "$output" "$tmp"
|
||||
# Flash-Config ist sensibel (enthaelt /boot/config inkl. Plugin-/SMB-/Network-Settings);
|
||||
# bewusst 0600, damit der Nearline-Pull ueber SMB sie nicht versehentlich greift.
|
||||
atomic_write "$output" "$tmp" 600
|
||||
|
||||
(
|
||||
cd "$LATEST_DIR"
|
||||
sha256sum "$(basename "$output")"
|
||||
) > "$tmp_checksum"
|
||||
chmod 600 "$tmp_checksum"
|
||||
atomic_write "$checksum" "$tmp_checksum"
|
||||
atomic_write "$checksum" "$tmp_checksum" 600
|
||||
|
||||
{
|
||||
printf 'created_utc=%s\n' "$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
@@ -201,8 +207,7 @@ backup_unraid_flash_config() {
|
||||
printf 'note=%s\n' 'Contains Unraid configuration and must be treated as secret backup material.'
|
||||
printf 'excluded=%s\n' 'downloadable plugin package archives under /boot/config/plugins/*/'
|
||||
} > "$tmp_manifest"
|
||||
chmod 600 "$tmp_manifest"
|
||||
atomic_write "$manifest" "$tmp_manifest"
|
||||
atomic_write "$manifest" "$tmp_manifest" 600
|
||||
}
|
||||
|
||||
dump_optional_pg_db() {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
param(
|
||||
[string]$SourceRoot = "\\192.168.178.58\backups",
|
||||
[string]$DestinationRoot = "H:\kallilab-nearline-backups",
|
||||
[switch]$WhatIf
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Jobs = @(
|
||||
@{
|
||||
Name = "borg-dumps-latest"
|
||||
Source = Join-Path $SourceRoot "borg\dumps\latest"
|
||||
Destination = Join-Path $DestinationRoot "borg-dumps\latest"
|
||||
Purpose = "Latest database/application dumps (Unraid-Flash-Artefakte bewusst ausgeschlossen, weil 0600 root:root - dafuer bleibt die Hetzner-Borg-Kette die Restore-Quelle)"
|
||||
# /XF schliesst bewusst die unraid-flash-config-Artefakte aus,
|
||||
# weil sie hostseitig 0600 root:root sind und der SMB-Share das
|
||||
# nicht ueberbruecken kann. Restore-Quelle dafuer bleibt das
|
||||
# Hetzner-Borg-Repo (siehe docs/RESTORE_MATRIX.md Tier 1 Unraid OS Flash).
|
||||
ExcludeFiles = @("unraid-flash-config.tar.gz", "unraid-flash-config.tar.gz.sha256", "unraid-flash-config.manifest.txt")
|
||||
},
|
||||
@{
|
||||
Name = "gitea-bundles"
|
||||
Source = Join-Path $SourceRoot "git-bundles\gitea"
|
||||
Destination = Join-Path $DestinationRoot "git-bundles\gitea"
|
||||
Purpose = "Verified bare-repository bundles for Gitea bootstrap"
|
||||
ExcludeFiles = @()
|
||||
}
|
||||
)
|
||||
|
||||
function Assert-PathExists {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string]$Label
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "$Label not found: $Path"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-RobocopyJob {
|
||||
param(
|
||||
[hashtable]$Job,
|
||||
[string]$LogRoot
|
||||
)
|
||||
|
||||
$logPath = Join-Path $LogRoot ("{0}-{1}.log" -f (Get-Date -Format "yyyyMMdd-HHmmss"), $Job.Name)
|
||||
New-Item -ItemType Directory -Force -Path $Job.Destination | Out-Null
|
||||
|
||||
$args = @(
|
||||
$Job.Source,
|
||||
$Job.Destination,
|
||||
"/E",
|
||||
"/COPY:DAT",
|
||||
"/DCOPY:DAT",
|
||||
"/R:2",
|
||||
"/W:5",
|
||||
"/FFT",
|
||||
"/XJ",
|
||||
"/XD",
|
||||
".tmp",
|
||||
"/NP",
|
||||
"/TEE",
|
||||
"/LOG:$logPath"
|
||||
)
|
||||
|
||||
if ($Job.ContainsKey("ExcludeFiles") -and $Job.ExcludeFiles.Count -gt 0) {
|
||||
$args += "/XF"
|
||||
$args += $Job.ExcludeFiles
|
||||
}
|
||||
|
||||
Write-Host "Running robocopy job: $($Job.Name)"
|
||||
Write-Host " Source: $($Job.Source)"
|
||||
Write-Host " Destination: $($Job.Destination)"
|
||||
|
||||
# stdout an $null hängen, damit der Robocopy-Live-Output nicht
|
||||
# in $results landet und die Report-Tabelle zerlegt. /LOG: + /TEE
|
||||
# protokollieren weiter vollstaendig.
|
||||
& robocopy @args | Out-Null
|
||||
$code = $LASTEXITCODE
|
||||
|
||||
if ($code -gt 7) {
|
||||
throw "Robocopy job '$($Job.Name)' failed with exit code $code. See log: $logPath"
|
||||
}
|
||||
|
||||
[pscustomobject]@{
|
||||
Name = $Job.Name
|
||||
Source = $Job.Source
|
||||
Destination = $Job.Destination
|
||||
ExitCode = $code
|
||||
Log = $logPath
|
||||
}
|
||||
}
|
||||
|
||||
Assert-PathExists -Path $SourceRoot -Label "Source root"
|
||||
|
||||
foreach ($job in $Jobs) {
|
||||
Assert-PathExists -Path $job.Source -Label "Source for job '$($job.Name)'"
|
||||
}
|
||||
|
||||
if ($WhatIf) {
|
||||
Write-Host "H:/ nearline pull plan only. No files will be copied."
|
||||
Write-Host "SourceRoot: $SourceRoot"
|
||||
Write-Host "DestinationRoot: $DestinationRoot"
|
||||
Write-Host ""
|
||||
foreach ($job in $Jobs) {
|
||||
Write-Host "- $($job.Name)"
|
||||
Write-Host " Purpose: $($job.Purpose)"
|
||||
Write-Host " Source: $($job.Source)"
|
||||
Write-Host " Destination: $($job.Destination)"
|
||||
}
|
||||
exit 0
|
||||
}
|
||||
|
||||
$destinationDrive = Split-Path -Qualifier $DestinationRoot
|
||||
Assert-PathExists -Path $destinationDrive -Label "Destination drive"
|
||||
|
||||
$logRoot = Join-Path $DestinationRoot "_logs"
|
||||
$reportRoot = Join-Path $DestinationRoot "_reports"
|
||||
New-Item -ItemType Directory -Force -Path $DestinationRoot, $logRoot, $reportRoot | Out-Null
|
||||
|
||||
$results = foreach ($job in $Jobs) {
|
||||
Invoke-RobocopyJob -Job $job -LogRoot $logRoot
|
||||
}
|
||||
|
||||
$reportPath = Join-Path $reportRoot ("nearline-pull-{0}.md" -f (Get-Date -Format "yyyy-MM-dd-HHmmss"))
|
||||
|
||||
$lines = @()
|
||||
$lines += "# H:/ Nearline Pull Report - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
$lines += ""
|
||||
$lines += "- Source root: ``$SourceRoot``"
|
||||
$lines += "- Destination root: ``$DestinationRoot``"
|
||||
$lines += "- Mode: non-destructive copy, no ``/MIR``, no purge"
|
||||
$lines += ""
|
||||
$lines += "| Job | Exit code | Source | Destination | Log |"
|
||||
$lines += "|---|---:|---|---|---|"
|
||||
foreach ($result in $results) {
|
||||
$lines += "| $($result.Name) | $($result.ExitCode) | ``$($result.Source)`` | ``$($result.Destination)`` | ``$($result.Log)`` |"
|
||||
}
|
||||
$lines += ""
|
||||
$lines += "Expected critical artifacts after run:"
|
||||
$lines += ""
|
||||
$lines += "- ``borg-dumps/latest/immich.dump``"
|
||||
$lines += "- ``borg-dumps/latest/komodo-mongo.archive.gz``"
|
||||
$lines += "- ``git-bundles/gitea/latest-report.md``"
|
||||
$lines += "- ``git-bundles/gitea/micha/*.bundle``"
|
||||
$lines += ""
|
||||
$lines += "Bewusst NICHT in Nearline-Scope:"
|
||||
$lines += ""
|
||||
$lines += "- ``unraid-flash-config.tar.gz`` (hostseitig 0600 root:root; Restore aus Hetzner-Borg)"
|
||||
|
||||
$lines | Set-Content -LiteralPath $reportPath -Encoding UTF8
|
||||
|
||||
Write-Host "H:/ nearline pull completed."
|
||||
Write-Host "Report: $reportPath"
|
||||
@@ -0,0 +1,26 @@
|
||||
// Renovate Bot-Config (NICHT die Repo-Config).
|
||||
//
|
||||
// Die Repo-Config liegt im Repository selbst unter `renovate.json` und
|
||||
// enthaelt nur Repo-spezifische Sachen (extends, packageRules, ignorePaths,
|
||||
// docker-compose patterns).
|
||||
//
|
||||
// Diese Bot-Config hier wird ueber RENOVATE_CONFIG_FILE in den Renovate-
|
||||
// Container gemountet. Sie enthaelt nur Plattform-, Discovery- und Limits-
|
||||
// Einstellungen. Den Auth-Token uebergeben wir ueber --env-file.
|
||||
module.exports = {
|
||||
platform: "gitea",
|
||||
endpoint: "https://git.kaleschke.info/api/v1",
|
||||
username: "renovate",
|
||||
gitAuthor: "Renovate Bot <renovate@kaleschke.info>",
|
||||
onboarding: false,
|
||||
requireConfig: "optional",
|
||||
// Autodiscover funktioniert in Gitea nur fuer eigene/Org-Repos; unser
|
||||
// Service-Account hat nur Collaborator-Rechte. Daher explicit list.
|
||||
autodiscover: false,
|
||||
repositories: ["Micha/homelab-infra"],
|
||||
// Limits konservativ: wenig PRs gleichzeitig, damit das Review-Volumen
|
||||
// handhabbar bleibt.
|
||||
prHourlyLimit: 0,
|
||||
prConcurrentLimit: 5,
|
||||
branchConcurrentLimit: 10,
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Self-hosted Renovate runner fuer Gitea.
|
||||
#
|
||||
# Wird vom Host-User-Script `renovate-six-hourly` aufgerufen. Liest das
|
||||
# Gitea-PAT aus einer Host-Secret-Datei, startet den Renovate-Container
|
||||
# ein einziges Mal, schreibt ein Log, beendet sich.
|
||||
#
|
||||
# Operator-Setup-Aufgaben (einmalig):
|
||||
# 1. Gitea-User `renovate` anlegen (Service-Account), 2FA nicht zwingend
|
||||
# 2. Diesem User Repo-Schreibrechte auf `Micha/*` geben
|
||||
# 3. Im Gitea-Profil des renovate-Users ein Access-Token erzeugen:
|
||||
# Scope: `write:repository` + `read:user`
|
||||
# 4. Token in `/mnt/user/appdata/secrets/renovate_token.txt` ablegen (chmod 600)
|
||||
# 5. Erstlauf: `bash /mnt/user/services/homelab-infra/ops/renovate/run-renovate.sh`
|
||||
# 6. User-Script `renovate-six-hourly` aktivieren
|
||||
|
||||
RENOVATE_IMAGE="${RENOVATE_IMAGE:-renovate/renovate:41}"
|
||||
RENOVATE_TOKEN_FILE="${RENOVATE_TOKEN_FILE:-/mnt/user/appdata/secrets/renovate_token.txt}"
|
||||
RENOVATE_LOG_DIR="${RENOVATE_LOG_DIR:-/mnt/user/services/renovate/logs}"
|
||||
RENOVATE_STATE_DIR="${RENOVATE_STATE_DIR:-/mnt/user/services/renovate/state}"
|
||||
RENOVATE_CONFIG_FILE="${RENOVATE_CONFIG_FILE:-/mnt/user/services/homelab-infra/ops/renovate/bot-config.js}"
|
||||
# Gitea sitzt hinter Traefik unter git.kaleschke.info; der WAN-Pfad geht
|
||||
# ueber Public-IP -> FRITZBox. Vom Docker-Container aus loest der Standard-
|
||||
# Resolver den Host moeglicherweise nicht auf (siehe `extra_hosts` im Komodo-
|
||||
# Compose). Wir mappen direkt auf die LAN-IP des Unraid-Hosts.
|
||||
GITEA_HOST_LAN_IP="${GITEA_HOST_LAN_IP:-192.168.178.58}"
|
||||
|
||||
if [ ! -r "$RENOVATE_TOKEN_FILE" ]; then
|
||||
echo "Renovate token file missing or unreadable: $RENOVATE_TOKEN_FILE" >&2
|
||||
echo "See ops/renovate/run-renovate.sh header for operator setup steps." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -r "$RENOVATE_CONFIG_FILE" ]; then
|
||||
echo "Renovate config missing: $RENOVATE_CONFIG_FILE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$RENOVATE_LOG_DIR" "$RENOVATE_STATE_DIR"
|
||||
|
||||
TS="$(date -u '+%Y-%m-%dT%H-%M-%SZ')"
|
||||
LOG_FILE="$RENOVATE_LOG_DIR/renovate-$TS.log"
|
||||
LATEST_SYMLINK="$RENOVATE_LOG_DIR/latest.log"
|
||||
|
||||
# Renovate liest die Konfiguration ueber RENOVATE_CONFIG_FILE als Pfad im
|
||||
# Container; wir mounten die Repo-Datei read-only nach /usr/src/app/config.json.
|
||||
{
|
||||
echo "[renovate] starting $TS"
|
||||
echo "[renovate] image: $RENOVATE_IMAGE"
|
||||
echo "[renovate] config: $RENOVATE_CONFIG_FILE"
|
||||
echo "[renovate] log: $LOG_FILE"
|
||||
echo
|
||||
|
||||
# Token wird ueber --env-file uebergeben, damit der Wert weder in
|
||||
# `ps`-Ausgabe noch im docker inspect -Snapshot landet. Das Env-File
|
||||
# liegt unter $RENOVATE_STATE_DIR/.env und wird mit 0600 angelegt.
|
||||
ENV_FILE="$RENOVATE_STATE_DIR/.env"
|
||||
umask 077
|
||||
cat > "$ENV_FILE" <<EFEOF
|
||||
RENOVATE_TOKEN=$(cat "$RENOVATE_TOKEN_FILE")
|
||||
RENOVATE_CONFIG_FILE=/usr/src/app/config.js
|
||||
LOG_LEVEL=${RENOVATE_LOG_LEVEL:-info}
|
||||
EFEOF
|
||||
chmod 600 "$ENV_FILE"
|
||||
|
||||
docker run --rm \
|
||||
--name renovate-run \
|
||||
--add-host "git.kaleschke.info:$GITEA_HOST_LAN_IP" \
|
||||
--dns 1.1.1.1 \
|
||||
--dns 8.8.8.8 \
|
||||
-v "$RENOVATE_CONFIG_FILE":/usr/src/app/config.js:ro \
|
||||
-v "$RENOVATE_STATE_DIR":/tmp/renovate \
|
||||
--env-file "$ENV_FILE" \
|
||||
"$RENOVATE_IMAGE" 2>&1
|
||||
rc=$?
|
||||
shred -u "$ENV_FILE" 2>/dev/null || rm -f "$ENV_FILE"
|
||||
|
||||
echo
|
||||
echo "[renovate] finished rc=$rc"
|
||||
exit $rc
|
||||
} | tee "$LOG_FILE"
|
||||
|
||||
ln -sfn "$LOG_FILE" "$LATEST_SYMLINK"
|
||||
@@ -32,6 +32,11 @@ Ziel:
|
||||
- `paperless-restore-test.sh`: hosttauglicher Paperless-Restore-Job
|
||||
- `paperless-plan.md`: konkreter Paperless-Testplan
|
||||
- `paperless-compose.test.yml`: isolierte Testinstanz fuer Paperless inkl. Test-Postgres und Test-Redis
|
||||
- `immich-restore-test.ps1`: Immich-Mini-Restore-Ablauf als Plan-/Windows-Scaffold
|
||||
- `immich-restore-test.sh`: hosttauglicher Immich-Restore-Job, erster echter Lauf noch offen
|
||||
- `immich-plan.md`: konkreter Immich-Testplan
|
||||
- `immich-runbook.md`: Operator-Runbook fuer den ersten Immich-Lauf
|
||||
- `immich-compose.test.yml`: isolierte Testinstanz fuer Immich inkl. pgvecto-rs Test-Postgres und Test-Redis
|
||||
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
||||
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
||||
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
||||
@@ -76,9 +81,10 @@ Aktuell ist das erste validierte Muster vorhanden.
|
||||
- echter Vaultwarden-Restore am 2026-05-07 erfolgreich verifiziert
|
||||
- echter Gitea-Restore am 2026-05-07 erfolgreich verifiziert
|
||||
- echter Paperless-Restore am 2026-05-07 erfolgreich verifiziert
|
||||
- Immich-Restore-Test am 2026-05-26 vorbereitet; erster echter Lauf mit Report steht noch aus
|
||||
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
||||
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
||||
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
|
||||
- naechster grosser Kandidat ist ein weiterer datenbankgestuetzter Dienst oder die Automatisierung
|
||||
- naechster grosser Kandidat ist der erste echte Immich-Lauf mit Zeitmessung; erst danach in die Rotation aufnehmen
|
||||
|
||||
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
||||
|
||||
@@ -21,11 +21,11 @@ require_path() {
|
||||
}
|
||||
|
||||
latest_archive_name() {
|
||||
docker exec "$BORG_CONTAINER" python3 - <<'PY'
|
||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('/data/borg.db')
|
||||
cur = conn.cursor()
|
||||
cur.execute("select archive_name from backup_jobs where status='completed' order by created_at desc limit 1")
|
||||
cur.execute("select archive_name from backup_jobs where status in ('completed', 'completed_with_warnings') order by created_at desc limit 1")
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
raise SystemExit("No completed borg archive found")
|
||||
@@ -34,7 +34,7 @@ PY
|
||||
}
|
||||
|
||||
borg_repo_url() {
|
||||
docker exec "$BORG_CONTAINER" python3 - <<'PY'
|
||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||
import sqlite3
|
||||
conn = sqlite3.connect('/data/borg.db')
|
||||
cur = conn.cursor()
|
||||
@@ -59,10 +59,16 @@ conn = sqlite3.connect('/data/borg.db')
|
||||
cur = conn.cursor()
|
||||
cur.execute("select path from repositories where path is not null and path != '' order by id asc limit 1")
|
||||
repo = cur.fetchone()[0]
|
||||
cur.execute("select archive_name from backup_jobs where status='completed' order by created_at desc limit 1")
|
||||
cur.execute("select archive_name from backup_jobs where status in ('completed', 'completed_with_warnings') order by created_at desc limit 1")
|
||||
archive = cur.fetchone()[0]
|
||||
with open('/local/secrets/borg_repo_passphrase.txt', 'r', encoding='utf-8') as f:
|
||||
os.environ['BORG_PASSPHRASE'] = f.read().strip()
|
||||
known_hosts = '/data/known_hosts'
|
||||
if os.path.exists(known_hosts):
|
||||
os.environ.setdefault(
|
||||
'BORG_RSH',
|
||||
f'ssh -o UserKnownHostsFile={known_hosts} -o StrictHostKeyChecking=yes',
|
||||
)
|
||||
os.makedirs(extract_dir, exist_ok=True)
|
||||
os.chdir(extract_dir)
|
||||
subprocess.run(['borg', 'extract', f'{repo}::{archive}', *paths], check=True)
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
services:
|
||||
restoretest-immich-postgres:
|
||||
# gleiches Image wie Produktion, damit pgvecto-rs / pgvector-Extensions
|
||||
# beim Restore aus immich.dump verfuegbar sind
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||
container_name: restoretest-immich-postgres
|
||||
restart: "no"
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
POSTGRES_USER: immich
|
||||
POSTGRES_DB: immich
|
||||
POSTGRES_PASSWORD: restoretest-immich-db
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- /mnt/user/backups/restore-lab/immich/postgres:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U immich -d immich"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
restoretest-immich-redis:
|
||||
image: redis:7.4-alpine
|
||||
container_name: restoretest-immich-redis
|
||||
restart: "no"
|
||||
command:
|
||||
- redis-server
|
||||
- --save
|
||||
- ""
|
||||
- --appendonly
|
||||
- "no"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
restoretest-immich-server:
|
||||
# gleiches Image wie Produktion; ML-Container bleibt bewusst weg,
|
||||
# weil der Smoke-Test nur Login-Page, DB-Restore und Asset-Count prueft.
|
||||
image: ghcr.io/immich-app/immich-server:release@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5
|
||||
container_name: restoretest-immich-server
|
||||
restart: "no"
|
||||
depends_on:
|
||||
restoretest-immich-postgres:
|
||||
condition: service_healthy
|
||||
restoretest-immich-redis:
|
||||
condition: service_started
|
||||
environment:
|
||||
DB_HOSTNAME: restoretest-immich-postgres
|
||||
DB_USERNAME: immich
|
||||
DB_PASSWORD: restoretest-immich-db
|
||||
DB_DATABASE_NAME: immich
|
||||
REDIS_HOSTNAME: restoretest-immich-redis
|
||||
# ML bewusst deaktiviert: Endpoint zeigt auf eine lokale, nicht
|
||||
# erreichbare URL. Immich-Server startet, ML-Features bleiben aus.
|
||||
IMMICH_MACHINE_LEARNING_URL: http://restoretest-immich-ml-disabled:9999
|
||||
TZ: Europe/Berlin
|
||||
ports:
|
||||
# nur 127.0.0.1 - keine Public-Route, keine Traefik-Labels
|
||||
- "127.0.0.1:12283:2283"
|
||||
volumes:
|
||||
# Test-Upload-Verzeichnis ist leer und liegt im Restore-Lab.
|
||||
# Produktive Assets unter /mnt/user/photos/immich werden NICHT eingebunden,
|
||||
# damit der Smoke-Test keine produktiven Daten anfasst.
|
||||
- /mnt/user/backups/restore-lab/immich/upload:/usr/src/app/upload
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
@@ -0,0 +1,89 @@
|
||||
# Immich Restore Test Plan
|
||||
|
||||
## Ziel
|
||||
|
||||
Nachweisen, dass `immich.dump` aus dem produktiven Borg-Archiv in einer isolierten Testumgebung wieder einspielbar ist und Immich-Server damit anlaufen, einloggen und Asset-Metadaten anzeigen kann.
|
||||
|
||||
Bewusst **nicht** Teil dieses Tests:
|
||||
|
||||
- Wiederherstellung produktiver Foto-Dateien aus `/mnt/user/photos/immich` und `/mnt/user/photos/family_archive`. Der Smoke-Test bleibt DB-/UI-zentriert.
|
||||
- Machine-Learning-Container. Spart Image-Pull-Zeit und Resource-Last; ML-Features sind im Smoke-Test nicht erforderlich.
|
||||
- Echte Browser-Login-Sequenz. Smoke-Test prueft nur, dass die Login-Seite ausgeliefert wird und die DB-Tabellen `assets` und `users` lesbar sind.
|
||||
|
||||
## Quelle
|
||||
|
||||
- Backup-Quelle: produktives Borg-Archiv (`hetzner_borg_appdata_critical` oder lokales Mirror)
|
||||
- fachlich relevanter Dump im Archiv:
|
||||
- `local/borg-dumps/latest/immich.dump`
|
||||
- Erzeuger: `ops/borg-ui/scripts/pre-backup-dumps.sh`, Funktion `dump_pg_db immich_postgres ... immich immich` mit `pg_dump -Fc`
|
||||
- produktive Foto-Pfade werden im Smoke-Test bewusst **nicht** angefasst
|
||||
|
||||
## Test-Ziel
|
||||
|
||||
- Restore-Lab: `/mnt/user/backups/restore-lab/immich`
|
||||
- Testdatenpfade:
|
||||
- `/mnt/user/backups/restore-lab/immich/postgres` (Test-Postgres-Datadir)
|
||||
- `/mnt/user/backups/restore-lab/immich/upload` (leeres Upload-Volume, Immich-Server braucht den Pfad nur als Mountpoint)
|
||||
- `/mnt/user/backups/restore-lab/immich/dumps/latest/immich.dump` (extrahierter Dump)
|
||||
- Testcontainer:
|
||||
- `restoretest-immich-server`
|
||||
- `restoretest-immich-postgres` (`tensorchord/pgvecto-rs:pg14-v0.2.0` - identisch zur Produktion, weil der Dump pgvecto-rs Extension referenziert)
|
||||
- `restoretest-immich-redis` (`redis:7.4-alpine`, rebuildbar)
|
||||
- Testport Web: `127.0.0.1:12283:2283`
|
||||
- Report-Ziel: `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md`
|
||||
|
||||
## Schutzregeln
|
||||
|
||||
- produktive Pfade `/mnt/user/photos/immich` und `/mnt/user/photos/family_archive` werden **nicht** in den Test-Container gemountet
|
||||
- produktive Domain `immich.kaleschke.info` wird **nicht** uebernommen
|
||||
- keine Traefik-Labels fuer die Testinstanz
|
||||
- keine produktive `immich_postgres`-/`immich_redis`-Instanz fuer den Test verwenden
|
||||
- ML-Container bleibt weg
|
||||
- Testcontainer publishen nur auf `127.0.0.1`, nicht auf LAN- oder Tailscale-Interface
|
||||
- Borg-Passphrase wird aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` gelesen und niemals in Logs, Reports oder Doku geschrieben
|
||||
|
||||
## Geplanter Ablauf
|
||||
|
||||
1. Restore-Ziel unter `/mnt/user/backups/restore-lab/immich` vorbereiten (postgres, upload, dumps/latest)
|
||||
2. `local/borg-dumps/latest/immich.dump` aus dem aktuellsten Borg-Archiv extrahieren
|
||||
3. Test-Postgres (`pgvecto-rs`) und Test-Redis mit `ops/restore-tests/immich-compose.test.yml` starten
|
||||
4. `immich.dump` in Test-Postgres importieren (`pg_restore -Fc --clean --if-exists --no-owner --no-privileges`)
|
||||
5. Testinstanz `restoretest-immich-server` starten
|
||||
6. lokalen Smoke-Test gegen `http://127.0.0.1:12283` ausfuehren und Asset/User-Count aus DB lesen
|
||||
7. Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md` schreiben
|
||||
8. Testcontainer stoppen und Restore-Lab bereinigen
|
||||
|
||||
## Smoke-Test
|
||||
|
||||
Minimal erfolgreich:
|
||||
|
||||
- Test-Postgres startet `healthy`
|
||||
- `pg_restore -Fc` laeuft ohne Fehler durch
|
||||
- Immich-Server liefert HTTP `200`, `302` oder `303` auf `/`
|
||||
- Response enthaelt mindestens einen der Marker `Immich`, `Login`, `Signin`
|
||||
- `select count(*) from assets;` und `select count(*) from users;` sind lesbar
|
||||
|
||||
Optional spaeter:
|
||||
|
||||
- Echte Login-Form via API ansprechen
|
||||
- pgvecto-rs Extension explizit per `\dx` pruefen
|
||||
- Test mit gemountetem **read-only** Foto-Sample-Pfad und Thumbnail-Rendering
|
||||
- Test inkl. ML-Container, sobald genug Test-Ressourcen verfuegbar
|
||||
|
||||
## Bekannte Komplikationen
|
||||
|
||||
| Risiko | Beschreibung | Mitigation |
|
||||
|---|---|---|
|
||||
| Dump-Groesse unbekannt | `pg_dump -Fc` der Immich-DB kann je nach Asset-/Face-Tabellen mehrere GB sein | Erster Lauf bewusst mit `--what-if`, anschliessend Operator-Test mit Zeitmessung |
|
||||
| `pg_restore`-Dauer unbekannt | Index-/Constraint-Aufbau und pgvecto-rs-Index-Build koennen lange dauern | Test-Postgres mit Health-Polling startet; Lauf nicht abbrechen ohne `pg_restore`-Exit |
|
||||
| pgvecto-rs Extension-Mismatch | Wenn das pgvecto-rs-Image im Test nicht exakt dieselbe Version wie Produktion ist, kann `CREATE EXTENSION vectors` im Restore fehlschlagen | Compose pinnt denselben Digest wie `apps/immich/docker-compose.yml` |
|
||||
| Immich-Server-Migrations beim Start | Immich fuehrt beim ersten Start DB-Migrations aus; das kann nach Restore noch laufen, bevor Web-UI antwortet | Smoke-Test pollt HTTP bis zu 120 s, bevor er als Fehler markiert |
|
||||
| Asset-Files fehlen | Der Test mountet kein Foto-Volume; Immich zeigt "missing" auf Asset-Detail-Seiten | Smoke-Test prueft nur Login-Page und DB-Counts, nicht Asset-Rendering |
|
||||
| ML-Endpoint unreachable | Immich-Server kann ML-Endpoint nicht erreichen | `IMMICH_MACHINE_LEARNING_URL` zeigt bewusst auf einen nicht erreichbaren Hostnamen; Login bleibt funktional, ML-Features bleiben deaktiviert |
|
||||
|
||||
## Noch offen vor dem ersten echten Lauf
|
||||
|
||||
- Dump-Groesse `immich.dump` auf dem Host bestimmen (`ls -lh /mnt/user/backups/borg/dumps/latest/immich.dump`)
|
||||
- Erwartete Restore-Dauer durch ersten Lauf mit `--keep-data` messen
|
||||
- Pruefen, ob die Immich-Tabellen `assets`/`users` im aktuellen Schema noch existieren (Schema-Drift bei Major-Update wuerde die Asset-Count-Query brechen, das Skript faengt das tolerant ab)
|
||||
- Schedule-Eintrag in `ops/restore-tests/schedule.md`: aktuell ist Immich nur als "spaeter, eigener Sprint" gefuehrt. Erst nach erstem erfolgreichen Lauf in Schedule aufnehmen, z. B. quartalsweise.
|
||||
@@ -0,0 +1,44 @@
|
||||
param(
|
||||
[string]$BackupSource = "/mnt/user/backups/borg",
|
||||
[string]$DumpSource = "/mnt/user/backups/borg/dumps/latest/immich.dump",
|
||||
[string]$RestoreRoot = "/mnt/user/backups/restore-lab/immich",
|
||||
[string]$ReportRoot = "/mnt/user/backups/restore-reports",
|
||||
[string]$BorgPassphraseFile = "/mnt/user/appdata/secrets/borg_repo_passphrase.txt",
|
||||
[switch]$WhatIf
|
||||
)
|
||||
|
||||
$Mode = if ($WhatIf) { "WhatIf" } else { "PlanOnly" }
|
||||
|
||||
Write-Output "Immich restore test scaffold"
|
||||
Write-Output "BackupSource: $BackupSource"
|
||||
Write-Output "DumpSource: $DumpSource"
|
||||
Write-Output "RestoreRoot: $RestoreRoot"
|
||||
Write-Output "ReportRoot: $ReportRoot"
|
||||
Write-Output "BorgPassphraseFile: $BorgPassphraseFile"
|
||||
Write-Output "Expected Borg source paths inside archive:"
|
||||
Write-Output " - local/borg-dumps/latest/immich.dump"
|
||||
Write-Output ""
|
||||
Write-Output "Planned isolation:"
|
||||
Write-Output " - Test Postgres: tensorchord/pgvecto-rs:pg14-v0.2.0 (same as production)"
|
||||
Write-Output " - Test Redis: redis:7.4-alpine (rebuildable, no restore needed)"
|
||||
Write-Output " - Test Server: ghcr.io/immich-app/immich-server:release (pinned digest like production)"
|
||||
Write-Output " - ML container: deliberately omitted"
|
||||
Write-Output " - Test endpoint: 127.0.0.1:12283 (no Traefik, no public domain)"
|
||||
Write-Output " - Productive photo paths under /mnt/user/photos/* will NOT be mounted"
|
||||
Write-Output "Mode: $Mode"
|
||||
Write-Output ""
|
||||
Write-Output "Planned steps:"
|
||||
Write-Output "1. Prepare restore-lab target under /mnt/user/backups/restore-lab/immich"
|
||||
Write-Output "2. Extract immich.dump from current Borg archive into test path"
|
||||
Write-Output ' Template: borg extract "$BORG_REPO" "::ARCHIVE_NAME" local/borg-dumps/latest/immich.dump'
|
||||
Write-Output ' Passphrase source: $(cat /mnt/user/appdata/secrets/borg_repo_passphrase.txt)'
|
||||
Write-Output "3. Start isolated test Postgres (pgvecto-rs) and test Redis"
|
||||
Write-Output "4. Import immich.dump into test Postgres with pg_restore -Fc --clean --if-exists --no-owner --no-privileges"
|
||||
Write-Output "5. Start restoretest-immich-server against isolated DB/Redis (ML omitted)"
|
||||
Write-Output "6. Run smoke checks against http://127.0.0.1:12283 and DB asset count"
|
||||
Write-Output "7. Write markdown report under /mnt/user/backups/restore-reports"
|
||||
Write-Output "8. Stop test containers and clean restore data after success"
|
||||
Write-Output ""
|
||||
Write-Output "This script is intentionally a scaffold only."
|
||||
Write-Output "No restore, no dump import, no container start, no file write is executed yet."
|
||||
Write-Output "Actual run happens on the Unraid host via ops/restore-tests/immich-restore-test.sh"
|
||||
Executable
+247
@@ -0,0 +1,247 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Immich Restore Smoke Test
|
||||
#
|
||||
# Nicht-destruktiver Restore-Smoke-Test fuer Immich.
|
||||
# - liest immich.dump aus dem produktiven Borg-Archiv
|
||||
# - importiert in eine isolierte Test-Postgres-Instanz mit gleichem Image
|
||||
# wie Produktion (tensorchord/pgvecto-rs)
|
||||
# - startet einen isolierten Immich-Server-Container ohne Traefik und
|
||||
# ohne ML-Container
|
||||
# - prueft Login-Page und Asset-Anzahl aus DB
|
||||
# - bereinigt anschliessend
|
||||
#
|
||||
# Produktiver Immich-Stack wird NICHT angefasst.
|
||||
# Produktive Foto-Pfade unter /mnt/user/photos/* werden NICHT gemountet.
|
||||
|
||||
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/immich"
|
||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/immich-extract"
|
||||
COMPOSE_FILE="$SCRIPT_DIR/immich-compose.test.yml"
|
||||
REPORT_FILE="$REPORT_ROOT/immich-$(date +%F).md"
|
||||
|
||||
if [ "$WHATIF" -eq 1 ]; then
|
||||
cat <<EOF
|
||||
Immich restore test
|
||||
Mode: WhatIf
|
||||
RestoreRoot: $RESTORE_ROOT
|
||||
ReportRoot: $REPORT_ROOT
|
||||
Expected Borg source paths:
|
||||
- local/borg-dumps/latest/immich.dump
|
||||
Planned isolation:
|
||||
- Test-Postgres: tensorchord/pgvecto-rs:pg14-v0.2.0
|
||||
- Test-Redis: redis:7.4-alpine (rebuildbar, kein Restore)
|
||||
- Test-Server: ghcr.io/immich-app/immich-server:release (Image-Pin wie Produktion)
|
||||
- ML-Container bewusst weggelassen
|
||||
- Test-Upload: leer, unter $RESTORE_ROOT/upload
|
||||
- Productive photo paths NOT mounted: /mnt/user/photos/immich, /mnt/user/photos/family_archive
|
||||
- Test endpoint: 127.0.0.1:12283 (no Traefik, no public domain)
|
||||
Smoke-Test:
|
||||
- Test-Postgres healthy
|
||||
- pg_restore -Fc -> immich.dump
|
||||
- HTTP 200/302/3xx von 127.0.0.1:12283
|
||||
- Asset-Count aus DB
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_cmd docker
|
||||
require_cmd curl
|
||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||
require_path "$COMPOSE_FILE"
|
||||
|
||||
cleanup() {
|
||||
cleanup_compose "$COMPOSE_FILE"
|
||||
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/postgres" "$RESTORE_ROOT/upload" "$RESTORE_ROOT/dumps/latest"
|
||||
|
||||
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
|
||||
|
||||
borg_extract "/restore/immich-extract" \
|
||||
"local/borg-dumps/latest/immich.dump"
|
||||
|
||||
mv "$EXTRACT_DIR/local/borg-dumps/latest/immich.dump" "$RESTORE_ROOT/dumps/latest/immich.dump"
|
||||
|
||||
# Stufe 1: Test-Postgres und Test-Redis starten
|
||||
docker compose -f "$COMPOSE_FILE" up -d \
|
||||
restoretest-immich-postgres restoretest-immich-redis >/dev/null
|
||||
|
||||
# Warten auf Postgres ready
|
||||
until docker exec restoretest-immich-postgres pg_isready -U immich -d immich >/dev/null 2>&1; do
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Einige Postgres-Images melden bereits "ready", waehrend die per ENV
|
||||
# gewuenschte Datenbank noch im Entrypoint entsteht. Der Smoke-Test legt
|
||||
# die isolierte Test-DB deshalb defensiv an und akzeptiert nur das Rennen,
|
||||
# in dem die DB parallel bereits erzeugt wurde.
|
||||
db_ok=0
|
||||
for attempt in $(seq 1 12); do
|
||||
if docker exec restoretest-immich-postgres sh -lc \
|
||||
'createdb -U immich immich 2>/tmp/immich-createdb.err || grep -q "already exists" /tmp/immich-createdb.err'; then
|
||||
db_ok=1
|
||||
break
|
||||
fi
|
||||
sleep 5
|
||||
done
|
||||
|
||||
if [ "$db_ok" -ne 1 ]; then
|
||||
docker exec restoretest-immich-postgres sh -lc 'cat /tmp/immich-createdb.err >&2' || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stufe 2: Dump in Test-Postgres importieren.
|
||||
# Der Postgres-Entrypoint kann kurz nach "ready" noch vom Init-Server auf
|
||||
# den finalen Server wechseln; pg_restore toleriert deshalb nur transiente
|
||||
# Start-/Shutdown-Fehler und versucht danach erneut.
|
||||
restore_ok=0
|
||||
for attempt in $(seq 1 12); do
|
||||
if docker exec -i restoretest-immich-postgres \
|
||||
pg_restore -U immich -d immich --clean --if-exists --no-owner --no-privileges \
|
||||
< "$RESTORE_ROOT/dumps/latest/immich.dump" 2>/tmp/immich-pg-restore.err; then
|
||||
restore_ok=1
|
||||
break
|
||||
fi
|
||||
|
||||
if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/immich-pg-restore.err; then
|
||||
sleep 5
|
||||
continue
|
||||
fi
|
||||
|
||||
cat /tmp/immich-pg-restore.err >&2
|
||||
exit 1
|
||||
done
|
||||
|
||||
if [ "$restore_ok" -ne 1 ]; then
|
||||
cat /tmp/immich-pg-restore.err >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Immich prueft seit v2 Systemordner-Marker unter UPLOAD_LOCATION.
|
||||
# Da der Smoke-Test bewusst keine produktiven Foto-Pfade mountet, erzeugen
|
||||
# wir eine leere Test-Struktur mit den erwarteten Markern.
|
||||
for dir in thumbs upload backups library profile encoded-video; do
|
||||
mkdir -p "$RESTORE_ROOT/upload/$dir"
|
||||
touch "$RESTORE_ROOT/upload/$dir/.immich"
|
||||
done
|
||||
chmod -R a+rwX "$RESTORE_ROOT/upload"
|
||||
|
||||
# Stufe 3: Immich-Server starten (ohne ML)
|
||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-immich-server >/dev/null
|
||||
|
||||
# Immich-Server braucht beim ersten Start einige Sekunden fuer DB-Migrations-Checks.
|
||||
# Wir geben ihm bis zu 120s und pollen den HTTP-Endpunkt.
|
||||
http_status=""
|
||||
for _ in $(seq 1 60); do
|
||||
http_status="$(curl -s -o /tmp/immich-body.html -w '%{http_code}' -L http://127.0.0.1:12283 || true)"
|
||||
if [ "$http_status" = "200" ] || [ "$http_status" = "302" ] || [ "$http_status" = "303" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Body-Check: Immich-UI hat typische Marker. Wir matchen tolerant.
|
||||
body_check="ok"
|
||||
if ! grep -qiE "immich|login|signin" /tmp/immich-body.html 2>/dev/null; then
|
||||
body_check="missing-marker"
|
||||
fi
|
||||
|
||||
if [ "$http_status" != "200" ] && [ "$http_status" != "302" ] && [ "$http_status" != "303" ]; then
|
||||
echo "Immich HTTP smoke failed: status=$http_status" >&2
|
||||
docker ps -a --filter name=restoretest-immich >&2 || true
|
||||
docker logs --tail 120 restoretest-immich-server >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$body_check" != "ok" ]; then
|
||||
echo "Immich HTTP smoke failed: body marker=$body_check" >&2
|
||||
docker logs --tail 120 restoretest-immich-server >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Asset-Count aus DB. Immich v2 nutzt Singular-Tabellen (`asset`,
|
||||
# `"user"`); ältere Schema-Staende werden tolerant als Fallback versucht.
|
||||
query_count() {
|
||||
local sql="$1"
|
||||
docker exec restoretest-immich-postgres \
|
||||
psql -U immich -d immich -tAc "$sql" 2>/dev/null \
|
||||
| tr -d '[:space:]' || true
|
||||
}
|
||||
|
||||
asset_count="$(query_count 'select count(*) from asset;')"
|
||||
if [ -z "$asset_count" ]; then
|
||||
asset_count="$(query_count 'select count(*) from assets;')"
|
||||
fi
|
||||
if [ -z "$asset_count" ]; then
|
||||
asset_count="n/a"
|
||||
fi
|
||||
|
||||
# User-Count als zusaetzlicher DB-Sanity-Check
|
||||
user_count="$(query_count 'select count(*) from "user";')"
|
||||
if [ -z "$user_count" ]; then
|
||||
user_count="$(query_count 'select count(*) from users;')"
|
||||
fi
|
||||
if [ -z "$user_count" ]; then
|
||||
user_count="n/a"
|
||||
fi
|
||||
|
||||
write_report "$REPORT_FILE" <<EOF
|
||||
# Immich Restore Test Report - $(date +%F)
|
||||
|
||||
- Service: \`immich\`
|
||||
- Source repo: \`$repo\`
|
||||
- Archive: \`$archive\`
|
||||
- Restore root: \`$RESTORE_ROOT\`
|
||||
- Test containers:
|
||||
- \`restoretest-immich-server\`
|
||||
- \`restoretest-immich-postgres\` (tensorchord/pgvecto-rs:pg14-v0.2.0)
|
||||
- \`restoretest-immich-redis\`
|
||||
- Test endpoint: \`http://127.0.0.1:12283\`
|
||||
- ML container: deliberately omitted
|
||||
- Result: \`SUCCESS\`
|
||||
|
||||
## Checks
|
||||
|
||||
- Borg extract of \`immich.dump\`: \`ok\`
|
||||
- Dump import into isolated Postgres: \`ok\`
|
||||
- HTTP status after redirect: \`$http_status\`
|
||||
- Login page marker: \`$body_check\`
|
||||
- Asset count in test DB: \`$asset_count\`
|
||||
- User count in test DB: \`$user_count\`
|
||||
|
||||
## Notes
|
||||
|
||||
- Test ran without Traefik and without the productive domain.
|
||||
- Productive photo paths under /mnt/user/photos/* were NOT mounted.
|
||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
|
||||
EOF
|
||||
|
||||
echo "Immich restore test ok -> $REPORT_FILE"
|
||||
@@ -0,0 +1,128 @@
|
||||
# Immich Restore Runbook
|
||||
|
||||
## Status
|
||||
|
||||
Skript und Test-Compose sind vorbereitet. **Erstlauf 2026-05-27 erfolgreich** (`SUCCESS`, HTTP `200`, `11977` Assets im Test-DB-Check). Report: `/mnt/user/backups/restore-reports/immich-2026-05-27.md`. Folgelaeufe je Quartal gemaess `docs/RESTORE_DRILL_ROUTINE.md` (Q2 = Immich).
|
||||
|
||||
Vor dem ersten Lauf muss Operator entscheiden:
|
||||
|
||||
- ist genug freier Platz unter `/mnt/user/backups/restore-lab/immich` vorhanden (Dump + Test-Postgres-Datadir + Upload-Dummy)?
|
||||
- ist genug freier RAM/CPU verfuegbar, um Immich-Server + Test-Postgres parallel zur produktiven Last laufen zu lassen?
|
||||
- soll der Lauf zuerst mit `--what-if` ausgefuehrt werden, dann mit `--keep-data` zur Zeitmessung?
|
||||
|
||||
## Vorbedingungen
|
||||
|
||||
- Borg-Quelle ist verfuegbar
|
||||
- Borg-UI laeuft (`docker ps | grep borg-ui`)
|
||||
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||
- aktueller Dump `immich.dump` ist Teil des letzten Borg-Archivs (siehe `pre-backup-dumps.sh`)
|
||||
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
|
||||
- produktiver Immich-Stack laeuft (oder ist bewusst aus); der Test ist davon unabhaengig
|
||||
|
||||
## Bestaetigter Host-Stand (Soll)
|
||||
|
||||
- produktiver Immich-Server: `immich_server` Container mit Image `ghcr.io/immich-app/immich-server:release@sha256:c15bff75068effb03f4355997d03dc7e0fc58720c2b54ad6f7f10d1bc57efaa5`
|
||||
- produktive Postgres: `immich_postgres` mit `tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52`
|
||||
- produktive Foto-Pfade: `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`
|
||||
- aktueller Dump-Pfad: `/mnt/user/backups/borg/dumps/latest/immich.dump`
|
||||
- Secret: `/mnt/user/appdata/secrets/immich_postgres_password.txt` (wird vom Test **nicht** gebraucht; Test nutzt eigenes Test-Passwort)
|
||||
|
||||
## Bestaetigter Teststand
|
||||
|
||||
- noch kein echter Mini-Restore gelaufen
|
||||
- Skript-Set vorbereitet:
|
||||
- `ops/restore-tests/immich-compose.test.yml`
|
||||
- `ops/restore-tests/immich-restore-test.sh`
|
||||
- `ops/restore-tests/immich-restore-test.ps1` (Scaffold, kein Live-Run)
|
||||
- `ops/restore-tests/immich-plan.md`
|
||||
- `ops/restore-tests/immich-runbook.md`
|
||||
|
||||
## Erster Lauf - trockene Variante
|
||||
|
||||
```bash
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/immich-restore-test.sh --what-if
|
||||
```
|
||||
|
||||
Erwartete Ausgabe: nur Plan-Output, kein Docker-Start, kein Borg-Extract.
|
||||
|
||||
## Erster Lauf - echter Test (Operator-freigegeben)
|
||||
|
||||
```bash
|
||||
# Optional: laufende Borg-Jobs pruefen, damit Borg-UI nicht parallel ausgelastet ist
|
||||
docker exec borg-ui sqlite3 /data/borg.db \
|
||||
"select status, archive_name, datetime(updated_at,'unixepoch') from backup_jobs order by id desc limit 5;"
|
||||
|
||||
# Lauf mit Datenerhalt fuer Zeitmessung
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/immich-restore-test.sh --keep-data
|
||||
```
|
||||
|
||||
Bei erfolgreichem Lauf:
|
||||
|
||||
- Report unter `/mnt/user/backups/restore-reports/immich-YYYY-MM-DD.md`
|
||||
- Test-Container `restoretest-immich-*` sind nach Lauf bereits `down`
|
||||
- Restore-Lab-Daten bleiben mit `--keep-data` erhalten; ohne Flag werden sie geloescht
|
||||
|
||||
## Smoke-Test-Pruefungen
|
||||
|
||||
Minimal erwartet im Report:
|
||||
|
||||
- `HTTP status after redirect: 200|302|303`
|
||||
- `Login page marker: ok`
|
||||
- `Asset count in test DB`: Zahl, oder `n/a` bei Schema-Drift
|
||||
- `User count in test DB`: Zahl, oder `n/a` bei Schema-Drift
|
||||
- Pre-Dump-Hook-Kette (`pg_dump -Fc`) und Restore-Kette (`pg_restore -Fc`) sind kompatibel
|
||||
- pgvecto-rs Extension ist im Restore-DB sichtbar
|
||||
|
||||
Manuelle Folgepruefung (optional):
|
||||
|
||||
Das Skript stoppt die Test-Container auch bei `--keep-data`; dieses Flag erhaelt nur die Restore-Lab-Daten. Fuer eine manuelle Folgepruefung nach einem erfolgreichen `--keep-data`-Lauf die Testinstanz kurz wieder hochfahren und danach wieder stoppen:
|
||||
|
||||
```bash
|
||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/immich-compose.test.yml up -d \
|
||||
restoretest-immich-postgres restoretest-immich-redis restoretest-immich-server
|
||||
|
||||
docker exec restoretest-immich-postgres psql -U immich -d immich -c "\dx"
|
||||
docker exec restoretest-immich-postgres psql -U immich -d immich -tAc "select count(*) from assets;"
|
||||
docker logs --tail 100 restoretest-immich-server
|
||||
|
||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/immich-compose.test.yml down
|
||||
```
|
||||
|
||||
## Cleanup nach Lauf ohne `--keep-data`
|
||||
|
||||
Das Skript bereinigt:
|
||||
|
||||
- Test-Container via `docker compose down`
|
||||
- Restore-Lab unter `/mnt/user/backups/restore-lab/immich`
|
||||
- Extract-Cache unter `/mnt/user/appdata/borg-ui/restore/immich-extract`
|
||||
|
||||
**Vorsicht:** `rm -rf` arbeitet ausschliesslich auf dem festen Restore-Lab-Pfad. Produktive Immich-Pfade unter `/mnt/user/photos/*` werden vom Skript niemals beschrieben.
|
||||
|
||||
## Fehlerfaelle
|
||||
|
||||
| Symptom | Ursache | Massnahme |
|
||||
|---|---|---|
|
||||
| `pg_restore: error: could not find extension ... vectors` | Test-Postgres-Image nicht pgvecto-rs | Compose-Pin im Test-Compose pruefen |
|
||||
| HTTP-Timeout nach 120 s | Immich-Migrations laufen noch | Wartezeit im Skript erhoehen oder Logs pruefen |
|
||||
| `pg_isready` nie healthy | Test-Postgres bricht beim Start ab (Datadir-Konflikt) | Restore-Lab vor Lauf vollstaendig leer; `docker logs restoretest-immich-postgres` |
|
||||
| Body matcht keine Marker | Immich UI hat sich versioniert; Marker-Liste anpassen | Marker im Skript erweitern (`grep -qiE`) |
|
||||
| Disk-Space-Mangel | Dump + Postgres-Datadir + Extract-Cache | mehr Platz freigeben oder Lauf abbrechen |
|
||||
|
||||
## Schedule-Eintrag (geplant, noch nicht aktiv)
|
||||
|
||||
Aktuell in `ops/restore-tests/schedule.md` nur als "spaeter, eigener Sprint" gelistet.
|
||||
|
||||
Nach erstem erfolgreichen Lauf vorschlagen:
|
||||
|
||||
- quartalsweise (`0 9 1 1,4,7,10 *` o. ae.)
|
||||
- ohne `--keep-data`
|
||||
- Report wird automatisch von `monthly-random-restore.sh` mit eingelesen, sobald Immich dort eintragbar ist
|
||||
|
||||
## Festgelegte Entscheidungen
|
||||
|
||||
- Immich-Restore-Test nutzt isoliertes Test-Postgres mit gleichem Image wie Produktion.
|
||||
- ML-Container wird im Smoke-Test **nicht** mitgestartet.
|
||||
- Produktive Foto-Pfade werden **nicht** in den Test gemountet.
|
||||
- Test-Daten werden nach erfolgreichem Lauf geloescht (`--keep-data` ueberschreibt das).
|
||||
- Borg-Passphrase wird aus Host-Secret-Datei gelesen und nirgendwo geloggt.
|
||||
- `ntfy` wird im ersten echten Lauf nicht eingebunden.
|
||||
@@ -0,0 +1,77 @@
|
||||
services:
|
||||
# Wegwerf-Mongo fuer Komodo-Bootstrap-Trockenlauf.
|
||||
# Schreibt in den Restore-Lab-Pfad, NICHT in das produktive
|
||||
# /mnt/user/appdata/komodo/mongo-Volume.
|
||||
restoretest-komodo-mongo:
|
||||
image: mongo:7.0.32@sha256:32979a1189dfdc44da3f5ed40d910495f5ad8f6f7f77556646f890a30b2d3f56
|
||||
container_name: restoretest-komodo-mongo
|
||||
restart: "no"
|
||||
command: --quiet
|
||||
environment:
|
||||
MONGO_INITDB_ROOT_USERNAME: komodo
|
||||
MONGO_INITDB_ROOT_PASSWORD: restoretest-komodo-mongo-pwd
|
||||
volumes:
|
||||
- /mnt/user/backups/restore-lab/komodo/mongo:/data/db
|
||||
healthcheck:
|
||||
test: ["CMD", "mongosh", "--quiet", "--eval", "db.adminCommand('ping').ok"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
restoretest-komodo-core:
|
||||
# Selbes Image wie Produktion, damit Compose-Diff Bootstrap-Kompatibilitaet
|
||||
# nachweist.
|
||||
image: ghcr.io/moghtech/komodo-core:2@sha256:8a7dbba232e4e49797bb412be5f78207c89fcf22cc2727b38631ae30f7518a4c
|
||||
container_name: restoretest-komodo-core
|
||||
init: true
|
||||
restart: "no"
|
||||
depends_on:
|
||||
restoretest-komodo-mongo:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- /mnt/user/backups/restore-lab/komodo/core:/repo-cache
|
||||
- /mnt/user/backups/restore-lab/komodo/keys:/config/keys
|
||||
environment:
|
||||
TZ: Europe/Berlin
|
||||
KOMODO_HOST: http://127.0.0.1:19120
|
||||
KOMODO_TITLE: Restore-Test
|
||||
# Wegwerf-Secrets, ausschliesslich fuer den lokalen Trockenlauf.
|
||||
# Niemals produktive Komodo-Secrets in dieses Compose schreiben.
|
||||
KOMODO_SECRET_KEY: restoretest-secret-key-placeholder-32
|
||||
KOMODO_WEBHOOK_SECRET: restoretest-webhook-secret
|
||||
KOMODO_PASSKEY: restoretest-periphery-passkey
|
||||
KOMODO_DATABASE_ADDRESS: restoretest-komodo-mongo:27017
|
||||
KOMODO_DATABASE_USERNAME: komodo
|
||||
KOMODO_DATABASE_PASSWORD: restoretest-komodo-mongo-pwd
|
||||
KOMODO_LOG_LEVEL: info
|
||||
KOMODO_LOCAL_AUTH: "true"
|
||||
KOMODO_JWT_SECRET: restoretest-jwt-secret-placeholder
|
||||
KOMODO_DISABLE_WEBSOCKETS: "true"
|
||||
ports:
|
||||
- "127.0.0.1:19120:9120"
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
restoretest-komodo-periphery:
|
||||
image: ghcr.io/moghtech/komodo-periphery:2@sha256:8ac9f2ef9c1461b95c862d445da00253005e7094d1e30f5b7b04b8d60ca7a3d6
|
||||
container_name: restoretest-komodo-periphery
|
||||
init: true
|
||||
restart: "no"
|
||||
depends_on:
|
||||
restoretest-komodo-core:
|
||||
condition: service_started
|
||||
volumes:
|
||||
- /mnt/user/backups/restore-lab/komodo/keys:/config/keys
|
||||
# bewusst KEIN docker.sock-Mount: dieser Test-Periphery darf nicht
|
||||
# versehentlich produktive Container managen.
|
||||
- /mnt/user/backups/restore-lab/komodo/periphery:/etc/komodo
|
||||
environment:
|
||||
PERIPHERY_ROOT_DIRECTORY: /tmp/restoretest-periphery
|
||||
PERIPHERY_PASSKEYS: restoretest-periphery-passkey
|
||||
PERIPHERY_SSL_ENABLED: "false"
|
||||
TZ: Europe/Berlin
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
@@ -0,0 +1,88 @@
|
||||
# Komodo Bootstrap Trockenlauf - Plan
|
||||
|
||||
## Ziel
|
||||
|
||||
Nachweisen, dass `ops/komodo/docker-compose.yml` als Recovery-Anker fuer einen Komodo-Kaltstart tauglich ist, ohne den produktiven Komodo-Stack anzufassen.
|
||||
|
||||
Bewusst **nicht** Teil dieses Tests:
|
||||
|
||||
- Restore aus dem produktiven `komodo-mongo.archive.gz`-Dump (eigene Folgeaufgabe; dieser Test prueft nur das Compose-Bootstrap, nicht den Daten-Restore).
|
||||
- docker.sock-Mount fuer die Test-Periphery (die Test-Periphery darf nie produktive Container managen).
|
||||
- Traefik-Route oder Authelia-Anbindung (Test laeuft ausschliesslich auf `127.0.0.1:19120`).
|
||||
|
||||
## Quelle
|
||||
|
||||
- Bootstrap-Anker: `ops/komodo/docker-compose.yml` (Soll-Stand laut `docs/SERVICES_RECOVERY.md` Stufe A-F).
|
||||
- Image-Digests: identisch zur Produktion fuer komodo-core und komodo-periphery; Mongo-Image identisch.
|
||||
|
||||
## Test-Ziel
|
||||
|
||||
- Restore-Lab: `/mnt/user/backups/restore-lab/komodo`
|
||||
- Wegwerf-Pfade:
|
||||
- `/mnt/user/backups/restore-lab/komodo/mongo` (Test-Mongo-Datadir)
|
||||
- `/mnt/user/backups/restore-lab/komodo/core` (Repo-Cache)
|
||||
- `/mnt/user/backups/restore-lab/komodo/keys` (gemeinsamer Keys-Pfad fuer Core+Periphery)
|
||||
- `/mnt/user/backups/restore-lab/komodo/periphery` (Periphery-Etc)
|
||||
- Testcontainer:
|
||||
- `restoretest-komodo-mongo`
|
||||
- `restoretest-komodo-core` (Test-Port `127.0.0.1:19120`)
|
||||
- `restoretest-komodo-periphery` (ohne docker.sock)
|
||||
- Compose-Project: `restoretest-komodo` (isoliert von Produktions-Project `komodo`)
|
||||
- Report-Ziel: `/mnt/user/backups/restore-reports/komodo-bootstrap-YYYY-MM-DD.md`
|
||||
|
||||
## Schutzregeln
|
||||
|
||||
- produktive Datadirs `/mnt/user/appdata/komodo/{mongo,core,periphery}` werden **nicht** gemountet
|
||||
- produktive Container `komodo-mongo`, `komodo-core`, `komodo-periphery` werden **nicht** gestoppt
|
||||
- produktive `KOMODO_*`-Secrets werden **nicht** verwendet
|
||||
- Test-Compose enthaelt nur Wegwerf-Werte fuer `KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_PASSKEY` und Mongo-Root-Password
|
||||
- Test-Periphery laeuft ohne docker.sock-Mount und ohne `/mnt/user/services`-Mount
|
||||
- Test-Port nur auf `127.0.0.1:19120`, keine LAN-/Tailscale-Bindung
|
||||
|
||||
## Geplanter Ablauf
|
||||
|
||||
1. Restore-Lab-Pfade leer anlegen
|
||||
2. `docker compose config` auf dem Test-Compose validieren
|
||||
3. Mongo und Core hochfahren, auf Mongo-`healthy` warten
|
||||
4. HTTP-Smoke gegen `http://127.0.0.1:19120` (Login-Seite oder Auth-Redirect erwartet)
|
||||
5. Periphery dazustarten, kurz beobachten
|
||||
6. Mongo-`authenticated ping` mit Test-Credentials
|
||||
7. Report schreiben
|
||||
8. Cleanup `docker compose down -v` und Restore-Lab loeschen (ausser `--keep-data`)
|
||||
|
||||
## Smoke-Test
|
||||
|
||||
Minimal erfolgreich:
|
||||
|
||||
- `docker compose config` valid
|
||||
- Test-Mongo erreicht `healthy`
|
||||
- Mongo-Authentifizierung mit Test-Creds funktioniert (`db.adminCommand({ping:1}).ok = 1`)
|
||||
- Komodo-Core HTTP `200`, `302`, `303` oder `401` (alles ist ein valider Lebenszeichen)
|
||||
- Test-Periphery container state `running`
|
||||
|
||||
Optional spaeter:
|
||||
|
||||
- Periphery-Verbindung gegen Test-Core verifizieren (braucht Periphery-Konfig mit `core_url`)
|
||||
- Echtes Restore aus `komodo-mongo.archive.gz`-Dump in die Test-Mongo
|
||||
- Schreiben einer Wegwerf-Resource (Server/Stack) ueber die API
|
||||
|
||||
## Bekannte Komplikationen
|
||||
|
||||
| Risiko | Beschreibung | Mitigation |
|
||||
|---|---|---|
|
||||
| Image-Drift | Komodo-Images aktualisieren ihre Major-Tag-Digests | Compose pinnt denselben Digest wie Produktion; bei Image-Update auch Test-Compose nachziehen |
|
||||
| Port-Konflikt | wenn 19120 anderweitig belegt ist | nur `127.0.0.1`-Bind; bei Konflikt Port im Compose anpassen |
|
||||
| Volume-Reste | unterbrochener Lauf laesst Wegwerf-Datadir liegen | Skript loescht Restore-Lab vor jedem Lauf; `--keep-data` ueberschreibt das bewusst |
|
||||
| Periphery-Erreichbarkeit | Core sucht Periphery initial nicht aktiv | Test prueft nur Periphery `State.Status=running`; voller Handshake ist optional |
|
||||
|
||||
## Bestaetigte Laeufe
|
||||
|
||||
| Datum | Mode | Ergebnis | Report |
|
||||
|---|---|---|---|
|
||||
| 2026-05-30 | `--what-if` | Plan-Ausgabe wie erwartet | (kein Report, nur stdout) |
|
||||
| 2026-05-30 | `--keep-data` | `SUCCESS`, 5/5 Checks gruen, Core HTTP `200`, Mongo healthy in ~6 s | `/mnt/user/backups/restore-reports/komodo-bootstrap-2026-05-30.md` |
|
||||
|
||||
## Folgeschritte
|
||||
|
||||
- Quartals-Belegung: Komodo-Bootstrap passt zum DR-Sanity-Check (`docs/RESTORE_DRILL_ROUTINE.md` Q2/Q4) und kann ohne Borg-Archiv jederzeit wiederholt werden.
|
||||
- Optional fuer kuenftige Laeufe: echtes Restore aus `komodo-mongo.archive.gz` in die Test-Mongo, danach Schreiben einer Wegwerf-Resource ueber die API.
|
||||
@@ -0,0 +1,95 @@
|
||||
# Komodo Bootstrap Trockenlauf - Runbook
|
||||
|
||||
## Status
|
||||
|
||||
Skript und Test-Compose sind vorbereitet. **Erstlauf 2026-05-30 erfolgreich** (`SUCCESS`, alle 5 Checks gruen, Komodo Core HTTP `200`). Report: `/mnt/user/backups/restore-reports/komodo-bootstrap-2026-05-30.md`. Folgelaeufe quartalsweise empfohlen als Teil des DR-Sanity-Checks (`docs/RESTORE_DRILL_ROUTINE.md`).
|
||||
|
||||
## Vorbedingungen
|
||||
|
||||
- Docker auf dem Unraid-Host
|
||||
- `borg-ui`-Container muss **nicht** laufen (im Gegensatz zum Immich-Test braucht der Komodo-Bootstrap kein Borg-Archiv)
|
||||
- freier Speicher unter `/mnt/user/backups/restore-lab/komodo` (~500 MB reichen)
|
||||
- Port `127.0.0.1:19120` ist frei
|
||||
|
||||
## Bestaetigter Host-Stand (Soll)
|
||||
|
||||
- produktiver Komodo-Stack: `komodo-mongo`, `komodo-core`, `komodo-periphery` unter Project `komodo`
|
||||
- produktive Mongo-Datadir: `/mnt/user/appdata/komodo/mongo`
|
||||
- produktive Secrets: `KOMODO_*` Stack-ENV-only (Restore-Reihenfolge siehe `docs/SECRETS_MAP.md`)
|
||||
- Test isoliert das alles unter Project `restoretest-komodo` mit Restore-Lab-Datadir
|
||||
|
||||
## Erster Lauf - trockene Variante
|
||||
|
||||
```bash
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/komodo-bootstrap-test.sh --what-if
|
||||
```
|
||||
|
||||
Erwartete Ausgabe: nur Plan-Output, kein Docker-Start, kein Verzeichnis angelegt.
|
||||
|
||||
## Erster Lauf - echter Test
|
||||
|
||||
```bash
|
||||
# optional: produktiven Komodo-Stack-Status pruefen, damit nichts kollidiert
|
||||
docker ps --filter name=komodo --format "{{.Names}}\t{{.Status}}"
|
||||
|
||||
# Lauf mit Datenerhalt
|
||||
bash /mnt/user/services/homelab-infra/ops/restore-tests/komodo-bootstrap-test.sh --keep-data
|
||||
```
|
||||
|
||||
Bei Erfolg:
|
||||
|
||||
- Report unter `/mnt/user/backups/restore-reports/komodo-bootstrap-YYYY-MM-DD.md`
|
||||
- Test-Container `restoretest-komodo-*` werden nach Lauf gestoppt und entfernt (auch bei `--keep-data`)
|
||||
- Restore-Lab-Daten bleiben mit `--keep-data` erhalten
|
||||
|
||||
## Smoke-Test-Pruefungen
|
||||
|
||||
Minimal erwartet im Report:
|
||||
|
||||
- `docker compose config valid: ok`
|
||||
- `Test-Mongo healthy: ok`
|
||||
- `Mongo authenticated ping (Test-Creds): ok`
|
||||
- `Komodo Core HTTP status: 200|302|303|401`
|
||||
- `Test-Periphery container state: running`
|
||||
|
||||
Manuelle Folgepruefung (optional):
|
||||
|
||||
```bash
|
||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/komodo-bootstrap-compose.test.yml \
|
||||
-p restoretest-komodo up -d
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:19120
|
||||
docker exec restoretest-komodo-mongo mongosh --quiet -u komodo \
|
||||
-p restoretest-komodo-mongo-pwd --authenticationDatabase admin --eval 'db.adminCommand({ping:1})'
|
||||
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/komodo-bootstrap-compose.test.yml \
|
||||
-p restoretest-komodo down -v
|
||||
```
|
||||
|
||||
## Cleanup ohne `--keep-data`
|
||||
|
||||
Skript bereinigt:
|
||||
|
||||
- Test-Container und Test-Volumes ueber `docker compose down -v`
|
||||
- Restore-Lab unter `/mnt/user/backups/restore-lab/komodo`
|
||||
|
||||
Produktive Komodo-Container, Mongo-Datadir und `KOMODO_*`-Secrets werden niemals beruehrt.
|
||||
|
||||
## Fehlerfaelle
|
||||
|
||||
| Symptom | Ursache | Massnahme |
|
||||
|---|---|---|
|
||||
| `Test-Mongo never reported healthy` | mongo-image konnte nicht starten | `docker logs restoretest-komodo-mongo` pruefen; Restore-Lab-Pfad leer? |
|
||||
| HTTP-Timeout nach 120 s | Komodo-Core haengt in Mongo-Connect | `docker logs restoretest-komodo-core` pruefen; Mongo-Auth-Test wiederholen |
|
||||
| `bind: address already in use` | Port 19120 belegt | Compose-Port-Mapping anpassen oder konfligierenden Prozess identifizieren |
|
||||
| Periphery `restarting` | Periphery braucht zusaetzliche ENVs | Logs lesen; Periphery-Verbindung ist optional fuer den Smoke-Test |
|
||||
|
||||
## Schedule
|
||||
|
||||
Aktuell nicht im automatischen Schedule. Empfohlen als Teil des quartalsweisen DR-Sanity-Check (`docs/RESTORE_DRILL_ROUTINE.md`).
|
||||
|
||||
## Festgelegte Entscheidungen
|
||||
|
||||
- Test-Compose nutzt dieselben Image-Digests wie Produktion.
|
||||
- Test-Periphery laeuft bewusst ohne docker.sock-Mount.
|
||||
- Test-Secrets sind Wegwerf-Werte im Compose; niemals produktive Werte einsetzen.
|
||||
- Test-Port nur auf `127.0.0.1`, keine LAN-Bindung.
|
||||
- `restoretest-komodo` als Compose-Project-Name reserviert; Test-Container heissen `restoretest-komodo-*`.
|
||||
@@ -0,0 +1,135 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Komodo Bootstrap Trockenlauf
|
||||
#
|
||||
# Verifiziert, dass `ops/komodo/docker-compose.yml` als Recovery-Anker
|
||||
# tauglich ist: Wegwerf-Mongo, Wegwerf-Core, Wegwerf-Periphery werden
|
||||
# isoliert hochgefahren und auf Bootstrap-Faehigkeit geprueft.
|
||||
#
|
||||
# Produktive Komodo-Container, produktive Mongo-Datadir und produktive
|
||||
# Komodo-Secrets 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/komodo"
|
||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||
COMPOSE_FILE="$SCRIPT_DIR/komodo-bootstrap-compose.test.yml"
|
||||
PROJECT_NAME="restoretest-komodo"
|
||||
REPORT_FILE="$REPORT_ROOT/komodo-bootstrap-$(date +%F).md"
|
||||
|
||||
if [ "$WHATIF" -eq 1 ]; then
|
||||
cat <<EOF
|
||||
Komodo bootstrap trockenlauf
|
||||
Mode: WhatIf
|
||||
RestoreRoot: $RESTORE_ROOT
|
||||
ReportRoot: $REPORT_ROOT
|
||||
Planned isolation:
|
||||
- Test-Mongo: mongo:7.0.32 (gleicher Digest wie Produktion), Datadir $RESTORE_ROOT/mongo
|
||||
- Test-Core: ghcr.io/moghtech/komodo-core:2 (gleicher Digest wie Produktion), Port 127.0.0.1:19120
|
||||
- Test-Periphery: ghcr.io/moghtech/komodo-periphery:2, ohne docker.sock-Mount
|
||||
- KOMODO_*-Secrets: Wegwerf-Werte ausschliesslich fuer Trockenlauf
|
||||
- Compose-Project: $PROJECT_NAME (isoliert von "komodo")
|
||||
Smoke-Test:
|
||||
- compose config valid
|
||||
- Mongo healthy
|
||||
- Core HTTP 200/4xx auf 127.0.0.1:19120 (Login-Seite erwartet)
|
||||
- Periphery container running
|
||||
EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_cmd docker
|
||||
require_path "$COMPOSE_FILE"
|
||||
|
||||
cleanup() {
|
||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
|
||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||
rm -rf "$RESTORE_ROOT"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
rm -rf "$RESTORE_ROOT"
|
||||
mkdir -p "$RESTORE_ROOT/mongo" "$RESTORE_ROOT/core" "$RESTORE_ROOT/keys" "$RESTORE_ROOT/periphery"
|
||||
|
||||
# Stufe 1: Compose syntaktisch validieren
|
||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" config >/dev/null
|
||||
|
||||
# Stufe 2: Mongo und Core hochfahren
|
||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" up -d \
|
||||
restoretest-komodo-mongo restoretest-komodo-core >/dev/null
|
||||
|
||||
# Stufe 3: Warten auf Mongo healthy
|
||||
mongo_ok=0
|
||||
for _ in $(seq 1 30); do
|
||||
s="$(docker inspect restoretest-komodo-mongo --format '{{.State.Health.Status}}' 2>/dev/null || true)"
|
||||
if [ "$s" = "healthy" ]; then mongo_ok=1; break; fi
|
||||
sleep 2
|
||||
done
|
||||
if [ "$mongo_ok" -ne 1 ]; then
|
||||
echo "Test-Mongo never reported healthy" >&2
|
||||
docker logs --tail 80 restoretest-komodo-mongo >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stufe 4: Warten bis Core HTTP antwortet
|
||||
http_status=""
|
||||
for _ in $(seq 1 60); do
|
||||
http_status="$(curl -s -o /tmp/komodo-bootstrap-body.html -w '%{http_code}' -L http://127.0.0.1:19120 || true)"
|
||||
if [ "$http_status" = "200" ] || [ "$http_status" = "302" ] || [ "$http_status" = "303" ] || [ "$http_status" = "401" ]; then
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
# Stufe 5: Periphery dazustarten und Health pruefen
|
||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" up -d \
|
||||
restoretest-komodo-periphery >/dev/null
|
||||
sleep 8
|
||||
periphery_state="$(docker inspect restoretest-komodo-periphery --format '{{.State.Status}}' 2>/dev/null || echo missing)"
|
||||
|
||||
# Stufe 6: Mongo-Ping mit Test-Credentials als zusaetzlicher Sanity-Check
|
||||
mongo_ping="n/a"
|
||||
if docker exec restoretest-komodo-mongo mongosh --quiet -u komodo \
|
||||
-p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
|
||||
--eval 'db.adminCommand({ping:1}).ok' 2>/dev/null | grep -q '^1$'; then
|
||||
mongo_ping="ok"
|
||||
fi
|
||||
|
||||
write_report "$REPORT_FILE" <<EOF
|
||||
# Komodo Bootstrap Trockenlauf - $(date +%F)
|
||||
|
||||
- Compose: \`ops/restore-tests/komodo-bootstrap-compose.test.yml\`
|
||||
- Project: \`$PROJECT_NAME\`
|
||||
- Restore root: \`$RESTORE_ROOT\`
|
||||
- Test endpoint: \`http://127.0.0.1:19120\`
|
||||
- Result: \`SUCCESS\`
|
||||
|
||||
## Checks
|
||||
|
||||
- docker compose config valid: \`ok\`
|
||||
- Test-Mongo healthy: \`ok\`
|
||||
- Mongo authenticated ping (Test-Creds): \`$mongo_ping\`
|
||||
- Komodo Core HTTP status: \`$http_status\`
|
||||
- Test-Periphery container state: \`$periphery_state\`
|
||||
|
||||
## Notes
|
||||
|
||||
- Produktive Komodo-Container, Mongo-Datadir und Secrets wurden nicht beruehrt.
|
||||
- Test-Periphery laeuft bewusst ohne docker.sock-Mount.
|
||||
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
||||
EOF
|
||||
|
||||
echo "Komodo bootstrap trockenlauf ok -> $REPORT_FILE"
|
||||
@@ -1,5 +1,5 @@
|
||||
param(
|
||||
[ValidateSet("freshness","vaultwarden","gitea","paperless")]
|
||||
[ValidateSet("freshness","vaultwarden","gitea","paperless","immich")]
|
||||
[string]$Mode,
|
||||
[switch]$WhatIf
|
||||
)
|
||||
@@ -35,4 +35,12 @@ switch ($Mode) {
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
"immich" {
|
||||
if ($WhatIf) {
|
||||
& (Join-Path $base "immich-restore-test.ps1") -WhatIf
|
||||
} else {
|
||||
& (Join-Path $base "immich-restore-test.ps1")
|
||||
}
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +28,14 @@ case "$MODE" in
|
||||
fi
|
||||
exec "$SCRIPT_DIR/paperless-restore-test.sh"
|
||||
;;
|
||||
immich)
|
||||
if [ "$WHATIF" = "--what-if" ]; then
|
||||
exec "$SCRIPT_DIR/immich-restore-test.sh" --what-if
|
||||
fi
|
||||
exec "$SCRIPT_DIR/immich-restore-test.sh"
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless} [--what-if]" >&2
|
||||
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich} [--what-if]" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -7,7 +7,7 @@ SUCCESS_TOPIC="${2:-${RESTORE_SUCCESS_TOPIC:-homelab-info}}"
|
||||
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
|
||||
|
||||
if [ -z "$MODE" ]; then
|
||||
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless> [success_topic]" >&2
|
||||
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless|immich> [success_topic]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -26,16 +26,15 @@ Alle 2 Monate:
|
||||
|
||||
Quartalsweise:
|
||||
|
||||
- Restore-/DR-Sanity-Check
|
||||
- Restore-/DR-Sanity-Check gemaess `docs/RESTORE_DRILL_ROUTINE.md`
|
||||
- `immich` Restore-Smoke-Test (DB + UI, ohne produktive Foto-Mounts; Erstlauf 2026-05-27 erfolgreich)
|
||||
- pruefen:
|
||||
- Restore-Lab-Struktur
|
||||
- Reports
|
||||
- Skripte und Pfade
|
||||
- Doku noch passend
|
||||
|
||||
Spaeter:
|
||||
|
||||
- `immich` als eigener Sprint
|
||||
Die Quartals-Belegung (welcher Dienst, welcher Sanity-Fokus) steht in `docs/RESTORE_DRILL_ROUTINE.md` Tabelle "Quartals-Kadenz".
|
||||
|
||||
## Konkreter Kalender
|
||||
|
||||
@@ -51,6 +50,8 @@ Spaeter:
|
||||
- `monthly-random-restore.sh`
|
||||
- Quartalsweise am 1. Werktag des Quartals:
|
||||
- DR-/Restore-Sanity-Check
|
||||
- Quartalsweise am 2. Sonntag im zweiten Quartalsmonat, 08:30:
|
||||
- `immich`
|
||||
|
||||
## Unraid User Scripts Cron
|
||||
|
||||
@@ -60,6 +61,7 @@ Spaeter:
|
||||
| `restore-vaultwarden-monthly` | `0 7 1-7 * 6` | erster Samstag im Monat 07:00 |
|
||||
| `restore-gitea-monthly` | `15 7 15-21 * 6` | dritter Samstag im Monat 07:15 |
|
||||
| `restore-paperless-bimonthly` | `0 8 8-14 1,3,5,7,9,11 *` | zweiter Samstag in ungeraden Monaten 08:00 |
|
||||
| `restore-immich-quarterly` | `30 8 8-14 2,5,8,11 0` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
|
||||
| `monthly-random-restore` | `0 9 1 * *` | erster Kalendertag im Monat 09:00 |
|
||||
|
||||
## Betriebsmodus
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended",
|
||||
":dependencyDashboard",
|
||||
":semanticCommits"
|
||||
],
|
||||
"dependencyDashboardTitle": "Renovate Dependency Dashboard",
|
||||
"labels": ["dependencies"],
|
||||
"rangeStrategy": "pin",
|
||||
"lockFileMaintenance": {
|
||||
"enabled": false
|
||||
},
|
||||
"docker-compose": {
|
||||
"managerFilePatterns": [
|
||||
"/(^|/)docker-compose([^/]*)\\.ya?ml$/",
|
||||
"/(^|/)compose([^/]*)\\.ya?ml$/"
|
||||
]
|
||||
},
|
||||
"dockerfile": {
|
||||
"enabled": true
|
||||
},
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Major-Updates getrennt sammeln, manuell mergen",
|
||||
"matchUpdateTypes": ["major"],
|
||||
"groupName": "major-updates",
|
||||
"groupSlug": "major-updates",
|
||||
"automerge": false,
|
||||
"labels": ["dependencies", "major"]
|
||||
},
|
||||
{
|
||||
"description": "Patch- und Minor-Digest-Updates fuer stabile Images zusammenfassen",
|
||||
"matchUpdateTypes": ["minor", "patch", "digest"],
|
||||
"matchManagers": ["docker-compose", "dockerfile"],
|
||||
"groupName": "minor-and-patch-updates",
|
||||
"groupSlug": "minor-patch-updates",
|
||||
"automerge": false,
|
||||
"labels": ["dependencies", "minor-patch"]
|
||||
},
|
||||
{
|
||||
"description": "Stateful Tier-1 (Postgres, Mongo, Redis): keine Auto-Group, einzelne PRs, kein Auto-Merge",
|
||||
"matchPackageNames": [
|
||||
"postgres",
|
||||
"mongo",
|
||||
"redis",
|
||||
"tensorchord/pgvecto-rs"
|
||||
],
|
||||
"groupName": null,
|
||||
"automerge": false,
|
||||
"labels": ["dependencies", "stateful-tier1"]
|
||||
},
|
||||
{
|
||||
"description": "Komodo Major-Tag (release :2 mit Digest-Pin) wird nicht von Renovate auf :3 hochgesetzt",
|
||||
"matchPackageNames": [
|
||||
"ghcr.io/moghtech/komodo-core",
|
||||
"ghcr.io/moghtech/komodo-periphery"
|
||||
],
|
||||
"matchUpdateTypes": ["major"],
|
||||
"enabled": false
|
||||
}
|
||||
],
|
||||
"ignorePaths": [
|
||||
"**/_archive/**",
|
||||
"ops/grafana-influxdb/**",
|
||||
"ops/loki/**"
|
||||
]
|
||||
}
|
||||
@@ -26,6 +26,14 @@ services:
|
||||
- backend_net
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
healthcheck:
|
||||
# authelia 4.39.x removed the "helper health-check" subcommand;
|
||||
# use the /api/health endpoint via the image-provided wget instead.
|
||||
test: ["CMD-SHELL", "wget -q --spider http://localhost:9091/api/health || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=frontend_net"
|
||||
|
||||
@@ -24,6 +24,14 @@ services:
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
|
||||
healthcheck:
|
||||
# vaultwarden image ships curl, not wget
|
||||
test: ["CMD-SHELL", "curl -fsS http://localhost:80/alive || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=frontend_net
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# Vergleicht die Repo-Baseline der Authelia-Konfiguration gegen die produktive
|
||||
# Host-Datei. Bewusst nur fuer Sektionen, die laut Repo-Konvention auf Host
|
||||
# und Repo identisch sein muessen (Default: access_control). OIDC-Clients,
|
||||
# identity_providers und Secret-Werte bleiben hostseitig und werden nicht
|
||||
# verglichen.
|
||||
#
|
||||
# Aufruf-Defaults siehe Variablen unten. Aufruf typischerweise:
|
||||
# bash services/authelia-diff.sh
|
||||
#
|
||||
# Exit-Codes:
|
||||
# 0 alle verglichenen Sektionen identisch
|
||||
# 1 Drift festgestellt (Diff wird auf stdout ausgegeben)
|
||||
# 2 Repo-Baseline oder Host-Datei fehlt
|
||||
# 3 Sektion in mindestens einer Datei nicht gefunden
|
||||
# 4 internes Werkzeug fehlt (awk/diff)
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
AUTHELIA_REPO_BASELINE="${AUTHELIA_REPO_BASELINE:-/mnt/user/services/homelab-infra/security/authelia/configuration.yml}"
|
||||
AUTHELIA_HOST_CONFIG="${AUTHELIA_HOST_CONFIG:-/mnt/user/appdata/authelia/config/configuration.yml}"
|
||||
AUTHELIA_DIFF_SECTIONS="${AUTHELIA_DIFF_SECTIONS:-access_control}"
|
||||
|
||||
for cmd in awk diff; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
echo "authelia-diff: missing required command '$cmd'" >&2
|
||||
exit 4
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ! -f "$AUTHELIA_REPO_BASELINE" ]; then
|
||||
echo "authelia-diff: repo baseline not found: $AUTHELIA_REPO_BASELINE" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [ ! -f "$AUTHELIA_HOST_CONFIG" ]; then
|
||||
echo "authelia-diff: host config not found: $AUTHELIA_HOST_CONFIG" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# Extrahiert einen Top-Level-Block aus einer YAML-Datei.
|
||||
# Block-Anfang: Zeile, die mit "<section>:" beginnt (kein Whitespace davor).
|
||||
# Block-Ende: naechste Top-Level-Key-Zeile (`^[A-Za-z_][A-Za-z0-9_]*:`).
|
||||
# Eingaberauschen wird entfernt: reine Kommentarzeilen, trailing whitespace,
|
||||
# Leerzeilen.
|
||||
extract_section() {
|
||||
local file="$1"
|
||||
local section="$2"
|
||||
awk -v section="$section" '
|
||||
BEGIN { inside = 0; found = 0 }
|
||||
{
|
||||
line = $0
|
||||
sub(/[[:space:]]+$/, "", line)
|
||||
}
|
||||
# Top-Level-Key entdeckt
|
||||
/^[A-Za-z_][A-Za-z0-9_]*:/ {
|
||||
key = line
|
||||
sub(/:.*$/, "", key)
|
||||
if (key == section) {
|
||||
inside = 1
|
||||
found = 1
|
||||
print line
|
||||
next
|
||||
} else if (inside == 1) {
|
||||
inside = 0
|
||||
}
|
||||
}
|
||||
inside == 1 {
|
||||
# Kommentar- und Leerzeilen ignorieren
|
||||
if (line ~ /^[[:space:]]*#/) next
|
||||
if (line ~ /^[[:space:]]*$/) next
|
||||
print line
|
||||
}
|
||||
END {
|
||||
if (!found) exit 10
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
tmpdir="$(mktemp -d -t authelia-diff.XXXXXX)"
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
overall_status=0
|
||||
diff_output=""
|
||||
missing_sections=""
|
||||
|
||||
IFS=',' read -r -a sections <<< "$AUTHELIA_DIFF_SECTIONS"
|
||||
for section in "${sections[@]}"; do
|
||||
section="${section// /}"
|
||||
[ -z "$section" ] && continue
|
||||
|
||||
repo_file="$tmpdir/repo.$section"
|
||||
host_file="$tmpdir/host.$section"
|
||||
|
||||
if ! extract_section "$AUTHELIA_REPO_BASELINE" "$section" > "$repo_file" 2>/dev/null; then
|
||||
missing_sections="${missing_sections}${missing_sections:+, }$section (repo)"
|
||||
continue
|
||||
fi
|
||||
if ! extract_section "$AUTHELIA_HOST_CONFIG" "$section" > "$host_file" 2>/dev/null; then
|
||||
missing_sections="${missing_sections}${missing_sections:+, }$section (host)"
|
||||
continue
|
||||
fi
|
||||
|
||||
if ! diff_chunk="$(diff -u \
|
||||
--label "repo:$section" "$repo_file" \
|
||||
--label "host:$section" "$host_file")"; then
|
||||
overall_status=1
|
||||
diff_output="${diff_output}${diff_chunk}"$'\n'
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$missing_sections" ] && [ "$overall_status" -eq 0 ]; then
|
||||
echo "authelia-diff: sections missing: $missing_sections" >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [ "$overall_status" -ne 0 ]; then
|
||||
printf '%s' "$diff_output"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TEXTFILE_DIR="${TEXTFILE_DIR:-/mnt/user/services/posture-check/textfile}"
|
||||
OUTPUT_FILE="${OUTPUT_FILE:-$TEXTFILE_DIR/homelab.prom}"
|
||||
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
||||
CRITICAL_CONTAINERS="${CRITICAL_CONTAINERS:-traefik authelia postgresql17 gitea komodo-core komodo-mongo komodo-periphery vaultwarden borg-ui ntfy adguard unbound Tailscale-Docker monitoring-alertmanager monitoring-alertmanager-ntfy-bridge monitoring-blackbox-exporter monitoring-cadvisor monitoring-grafana monitoring-loki monitoring-node-exporter monitoring-promtail immich_server immich_postgres immich_redis paperless-ngx nextcloud nextcloud-postgres nextcloud-redis mealie mealie-postgres}"
|
||||
|
||||
mkdir -p "$TEXTFILE_DIR"
|
||||
tmp="$(mktemp "$TEXTFILE_DIR/homelab.prom.XXXXXX")"
|
||||
cleanup() {
|
||||
rm -f "$tmp"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
now="$(date +%s)"
|
||||
|
||||
{
|
||||
cat <<'EOF'
|
||||
# HELP homelab_textfile_exporter_last_run_timestamp_seconds Unix timestamp of the last successful homelab textfile exporter run.
|
||||
# TYPE homelab_textfile_exporter_last_run_timestamp_seconds gauge
|
||||
EOF
|
||||
printf 'homelab_textfile_exporter_last_run_timestamp_seconds %s\n' "$now"
|
||||
|
||||
cat <<'EOF'
|
||||
# HELP homelab_critical_container_running Whether a critical container is currently running according to docker inspect.
|
||||
# TYPE homelab_critical_container_running gauge
|
||||
EOF
|
||||
for container in $CRITICAL_CONTAINERS; do
|
||||
running="0"
|
||||
if docker inspect -f '{{.State.Running}}' "$container" 2>/dev/null | grep -qx true; then
|
||||
running="1"
|
||||
fi
|
||||
printf 'homelab_critical_container_running{name="%s"} %s\n' "$container" "$running"
|
||||
done
|
||||
|
||||
cat <<'EOF'
|
||||
# HELP homelab_borg_last_completed_timestamp_seconds Unix timestamp of the most recent completed Borg backup job known to Borg UI.
|
||||
# TYPE homelab_borg_last_completed_timestamp_seconds gauge
|
||||
# HELP homelab_borg_last_success Whether the most recent Borg backup job completed successfully.
|
||||
# TYPE homelab_borg_last_success gauge
|
||||
# HELP homelab_borg_last_job_warning Whether the most recent Borg backup job completed with warnings.
|
||||
# TYPE homelab_borg_last_job_warning gauge
|
||||
EOF
|
||||
|
||||
if docker inspect "$BORG_CONTAINER" >/dev/null 2>&1; then
|
||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||
import datetime as dt
|
||||
import sqlite3
|
||||
|
||||
conn = sqlite3.connect("/data/borg.db")
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
|
||||
latest = cur.execute("""
|
||||
select status, completed_at, archive_name
|
||||
from backup_jobs
|
||||
order by coalesce(started_at, created_at) desc
|
||||
limit 1
|
||||
""").fetchone()
|
||||
|
||||
completed = cur.execute("""
|
||||
select completed_at, archive_name
|
||||
from backup_jobs
|
||||
where status in ('completed', 'completed_with_warnings')
|
||||
and completed_at is not null
|
||||
order by completed_at desc
|
||||
limit 1
|
||||
""").fetchone()
|
||||
|
||||
def parse_ts(value):
|
||||
if not value:
|
||||
return 0
|
||||
value = value.replace("Z", "+00:00")
|
||||
try:
|
||||
parsed = dt.datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
try:
|
||||
parsed = dt.datetime.strptime(value, "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
return 0
|
||||
if parsed.tzinfo is None:
|
||||
parsed = parsed.replace(tzinfo=dt.timezone.utc)
|
||||
return int(parsed.timestamp())
|
||||
|
||||
def escape_label(value):
|
||||
return (value or "").replace("\\", "\\\\").replace('"', '\\"')
|
||||
|
||||
latest_status = latest["status"] if latest else "missing"
|
||||
latest_success = 1 if latest_status in ("completed", "completed_with_warnings") else 0
|
||||
latest_warning = 1 if latest_status == "completed_with_warnings" else 0
|
||||
completed_ts = parse_ts(completed["completed_at"]) if completed else 0
|
||||
latest_archive = escape_label(latest["archive_name"] if latest else "")
|
||||
completed_archive = escape_label(completed["archive_name"] if completed else "")
|
||||
|
||||
print(f'homelab_borg_last_success{{status="{latest_status}",archive="{latest_archive}"}} {latest_success}')
|
||||
print(f'homelab_borg_last_job_warning{{status="{latest_status}",archive="{latest_archive}"}} {latest_warning}')
|
||||
print(f'homelab_borg_last_completed_timestamp_seconds{{archive="{completed_archive}"}} {completed_ts}')
|
||||
PY
|
||||
else
|
||||
printf 'homelab_borg_last_success{status="container_missing",archive=""} 0\n'
|
||||
printf 'homelab_borg_last_job_warning{status="container_missing",archive=""} 0\n'
|
||||
printf 'homelab_borg_last_completed_timestamp_seconds{archive=""} 0\n'
|
||||
fi
|
||||
} > "$tmp"
|
||||
|
||||
# 0644 statt mktemp-default 0600, damit der node-exporter-Textfile-Collector
|
||||
# (laeuft als nobody:65534) die Datei lesen kann.
|
||||
chmod 644 "$tmp"
|
||||
mv "$tmp" "$OUTPUT_FILE"
|
||||
trap - EXIT
|
||||
|
||||
printf '%s\n' "$OUTPUT_FILE"
|
||||
@@ -10,6 +10,8 @@ TMP_DIR="${TMP_DIR:-/tmp/kallilab-posture-check}"
|
||||
ALLOW_DISK1_NTFS="${ALLOW_DISK1_NTFS:-0}"
|
||||
ALERT_STATE_PATH="${ALERT_STATE_PATH:-/mnt/user/services/posture-check/last-alert.state}"
|
||||
ALERT_REPEAT_SECONDS="${ALERT_REPEAT_SECONDS:-86400}"
|
||||
SKIP_AUTHELIA_DRIFT="${SKIP_AUTHELIA_DRIFT:-0}"
|
||||
AUTHELIA_DIFF_SCRIPT="${AUTHELIA_DIFF_SCRIPT:-/mnt/user/services/homelab-infra/services/authelia-diff.sh}"
|
||||
|
||||
mkdir -p "$TMP_DIR"
|
||||
RESULTS_FILE="$TMP_DIR/results.$$"
|
||||
@@ -219,6 +221,41 @@ check_nvme_smart() {
|
||||
fi
|
||||
}
|
||||
|
||||
check_authelia_config_drift() {
|
||||
if [ "$SKIP_AUTHELIA_DRIFT" = "1" ]; then
|
||||
add_result "ok" "authelia_config_drift" "Authelia drift check skipped via SKIP_AUTHELIA_DRIFT=1"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ ! -x "$AUTHELIA_DIFF_SCRIPT" ] && [ ! -f "$AUTHELIA_DIFF_SCRIPT" ]; then
|
||||
add_result "warning" "authelia_config_drift" "Authelia diff script missing: $AUTHELIA_DIFF_SCRIPT"
|
||||
return
|
||||
fi
|
||||
|
||||
local output
|
||||
local rc
|
||||
output="$(bash "$AUTHELIA_DIFF_SCRIPT" 2>&1)"
|
||||
rc=$?
|
||||
|
||||
case "$rc" in
|
||||
0)
|
||||
add_result "ok" "authelia_config_drift" "Authelia repo baseline matches host config (access_control)"
|
||||
;;
|
||||
1)
|
||||
add_result "warning" "authelia_config_drift" "Authelia repo<->host drift in access_control; run authelia-diff.sh for details"
|
||||
;;
|
||||
2)
|
||||
add_result "warning" "authelia_config_drift" "Authelia diff aborted: $output"
|
||||
;;
|
||||
3)
|
||||
add_result "warning" "authelia_config_drift" "Authelia diff: section missing in repo or host: $output"
|
||||
;;
|
||||
*)
|
||||
add_result "warning" "authelia_config_drift" "Authelia diff returned unexpected rc=$rc: $output"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
send_ntfy() {
|
||||
local severity="$1"
|
||||
local topic="$2"
|
||||
@@ -388,6 +425,7 @@ main() {
|
||||
done
|
||||
|
||||
check_nvme_smart
|
||||
check_authelia_config_drift
|
||||
write_json
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,27 @@ Zeit: taeglich 06:20, Cron `20 6 * * *`.
|
||||
bash /mnt/user/services/homelab-infra/services/posture-check/compose-runtime-drift.sh
|
||||
```
|
||||
|
||||
## `prometheus-textfile-export-15min`
|
||||
|
||||
Zeit: alle 15 Minuten, Cron `*/15 * * * *`.
|
||||
|
||||
Zweck:
|
||||
|
||||
- Borg-Backup-Frische fuer Prometheus sichtbar machen
|
||||
- kritische Container als explizite 0/1-Metrik exportieren
|
||||
- Grundlage fuer `HomelabBorgBackupStale`, `HomelabBorgLastJobFailed` und `HomelabCriticalContainerDown`
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
bash /mnt/user/services/homelab-infra/services/posture-check/export-prometheus-textfile.sh
|
||||
```
|
||||
|
||||
Ziel-Datei:
|
||||
|
||||
```text
|
||||
/mnt/user/services/posture-check/textfile/homelab.prom
|
||||
```
|
||||
|
||||
## `homelab-operations-report-daily`
|
||||
|
||||
Zeit: taeglich nach Borg und den Morgenchecks, z. B. 07:30, Cron `30 7 * * *`.
|
||||
|
||||
@@ -8,6 +8,7 @@ services:
|
||||
command:
|
||||
- --api.dashboard=true
|
||||
- --api.insecure=false
|
||||
- --ping=true
|
||||
- --providers.docker=true
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --providers.docker.network=frontend_net
|
||||
@@ -46,6 +47,12 @@ services:
|
||||
- frontend_net
|
||||
- backend_net
|
||||
- monitoring_net
|
||||
healthcheck:
|
||||
test: ["CMD", "traefik", "healthcheck", "--ping"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30s
|
||||
labels:
|
||||
- traefik.enable=true
|
||||
- traefik.docker.network=frontend_net
|
||||
|
||||
Reference in New Issue
Block a user