Compare commits
128 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af4b7015ee | |||
| d48d473942 | |||
| e80e5dd49f | |||
| 3c339474a7 | |||
| c79afdfab0 | |||
| 8172793c68 | |||
| 8e46440944 | |||
| dfe1dc1c99 | |||
| 4007da3302 | |||
| 9836ea3c4f | |||
| 803f84b3af | |||
| d05ca63545 | |||
| 9847baf327 | |||
| 8ec5bc55d9 | |||
| 9c844074e0 | |||
| c126b71852 | |||
| e89b88a513 | |||
| 8bb250220b | |||
| 2f64aee109 | |||
| ed55b88ec1 | |||
| ce747f687f | |||
| cf11b4d75b | |||
| 796901ec6b | |||
| de7b714b4d | |||
| 8045e22873 | |||
| 52f8c2adcb | |||
| 0ddae675a8 | |||
| 7ce8e948cd | |||
| 2a87220862 | |||
| f2d4cad566 | |||
| e7370e4820 | |||
| dc26eb313c | |||
| dc7cbfa6cd | |||
| cf9ca59eb1 | |||
| d2a9c3b8cb | |||
| 0177350e64 | |||
| 2f3a029098 | |||
| a4c79d9d81 | |||
| 18a90fbb4b | |||
| 30f076c85a | |||
| 6e65f81503 | |||
| 6123584a02 | |||
| c33e29016b | |||
| 2628a0c795 | |||
| c7eed6bdad | |||
| 6c61ad3860 | |||
| 2d1b541847 | |||
| c3491eb382 | |||
| 023ee63687 | |||
| 3a263a4846 | |||
| 68d3ace598 | |||
| 0ef98a23e1 | |||
| 6353da47c5 | |||
| 207f49f001 | |||
| a687d9b73e | |||
| e3459c76d0 | |||
| 254eb81496 | |||
| 9a6d7123ce | |||
| 151d253aff | |||
| dda6021116 | |||
| 2f3d184a3b | |||
| bc3ecad45a | |||
| 88a42f3f78 | |||
| af2c6ee533 | |||
| f382c25696 | |||
| d710a506e8 | |||
| 2ea65e906d | |||
| 2d438cf02b | |||
| 7ba10c893b | |||
| fb948ac951 | |||
| 9ca6e47472 | |||
| 38fa8c5dd5 | |||
| ba87719de3 | |||
| 53c34dca0e | |||
| 7d87698715 | |||
| c47639ecf4 | |||
| b158f9d871 | |||
| d947c7f066 | |||
| 9edd6c24e6 | |||
| 7a513e9fc8 | |||
| 4b96d13510 | |||
| 642eb88b40 | |||
| dd494046ce | |||
| 16d3b8f2fa | |||
| a9b232195d | |||
| 5ee4a158d6 | |||
| 86435d4091 | |||
| 5e52316fab | |||
| 8a4df239fa | |||
| 893b34a585 | |||
| d1f9491b24 | |||
| 14de2f4801 | |||
| 90d1595285 | |||
| c1985e177b | |||
| a244f2d677 | |||
| ef032f2dde | |||
| 6fec64d0a1 | |||
| 5d1ae68705 | |||
| 2913e1005f | |||
| 6f0e6f0d5a | |||
| f473fbaa8b | |||
| c922d1f241 | |||
| ba3ef8fcfc | |||
| 52fc007123 | |||
| 8d71dfb9ad | |||
| 440000c085 | |||
| cacf77bfb0 | |||
| cd4dd178ed | |||
| 541c7be853 | |||
| b1ae9f3c26 | |||
| e2624796f0 | |||
| 9f63e6e3bc | |||
| 8eb367f0b5 | |||
| 745761f518 | |||
| ac637d30fb | |||
| b0a6244e21 | |||
| 4fb17a09e6 | |||
| be5c68751f | |||
| 3bfd065326 | |||
| eeebeec804 | |||
| 55fdb13532 | |||
| 8709fe8239 | |||
| 89114b1b12 | |||
| 3da19421d0 | |||
| 16e661be87 | |||
| 12c05376d0 | |||
| dfd0ccbb9a | |||
| ae5d4aedfc |
@@ -28,3 +28,5 @@ Thumbs.db
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.log
|
*.log
|
||||||
.serena/
|
.serena/
|
||||||
|
.claude/settings.local.json
|
||||||
|
memory/
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ Wenn Drift vermutet wird, nicht raten. Erst die Pflichtmatrix in `docs/GITOPS_DR
|
|||||||
- `traefik`: Host-Ports 80/443
|
- `traefik`: Host-Ports 80/443
|
||||||
- `gitea`: SSH-Port 222
|
- `gitea`: SSH-Port 222
|
||||||
- `AdGuard Home`: DNS-Port 53 und LAN-Admin-Port 8082
|
- `AdGuard Home`: DNS-Port 53 und LAN-Admin-Port 8082
|
||||||
- `tailscale`: `network_mode: host`
|
- `tailscale`: natives Unraid-Plugin (`tailscale.plg`, Interface `tailscale1`), Subnet-Router fuers LAN; nicht repo-/Komodo-verwaltet. Der frueher repo-verwaltete userspace-Docker-Stack `host-services/tailscale/` wurde am 2026-06-06 entfernt.
|
||||||
- `Plex-Media-Server`: historischer Host-Netz-Sonderfall, nicht als Repo-Stack enthalten
|
- `Plex-Media-Server`: historischer Host-Netz-Sonderfall, nicht als Repo-Stack enthalten
|
||||||
- `scrutiny`: `privileged: true` fuer SMART/Laufwerkszugriff
|
- `scrutiny`: `privileged: true` fuer SMART/Laufwerkszugriff
|
||||||
- `Komodo`: Docker-Socket und native Auth ohne pauschale ForwardAuth
|
- `Komodo`: Docker-Socket und native Auth ohne pauschale ForwardAuth
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
> **Single Source of Truth** für Docker-Netzwerkarchitektur, Sicherheitsregeln, Zielbild und Migration des Kallilabcore-Homelabs.
|
> **Single Source of Truth** für Docker-Netzwerkarchitektur, Sicherheitsregeln, Zielbild und Migration des Kallilabcore-Homelabs.
|
||||||
> **Arbeitsregel für KI-Assistenten:** Dieses Dokument immer zuerst lesen, bevor Fragen zu Containern, Netzwerken, Traefik, Tailscale, Migration oder Security beantwortet werden.
|
> **Arbeitsregel für KI-Assistenten:** Dieses Dokument immer zuerst lesen, bevor Fragen zu Containern, Netzwerken, Traefik, Tailscale, Migration oder Security beantwortet werden.
|
||||||
|
|
||||||
**Stand:** 2026-05-23 | **Aktueller Schwerpunkt:** GitOps / Doku-Synchronisierung / Reproduzierbare Deployments
|
**Stand:** 2026-06-02 | **Aktueller Schwerpunkt:** GitOps / Doku-Synchronisierung / Reproduzierbare Deployments
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -145,6 +145,7 @@ Diese Dienste sind über echte `*.kaleschke.info`-Domains erreichbar:
|
|||||||
- `gitea` (Web) — git.kaleschke.info
|
- `gitea` (Web) — git.kaleschke.info
|
||||||
- `immich_server` — immich.kaleschke.info
|
- `immich_server` — immich.kaleschke.info
|
||||||
- `nextcloud` — cloud.kaleschke.info
|
- `nextcloud` — cloud.kaleschke.info
|
||||||
|
- `plex` — plex.kaleschke.info (Traefik, native Plex-Auth; Plex Remote Access/Port 32400 bleibt aus)
|
||||||
|
|
||||||
### 4.2 Nicht öffentlich / nur Tailscale oder Traefik + Middleware
|
### 4.2 Nicht öffentlich / nur Tailscale oder Traefik + Middleware
|
||||||
Diese Dienste sind **keine Public Apps**:
|
Diese Dienste sind **keine Public Apps**:
|
||||||
@@ -162,6 +163,8 @@ Diese Dienste sind **keine Public Apps**:
|
|||||||
- `bentopdf` — pdf.kaleschke.info (Middleware)
|
- `bentopdf` — pdf.kaleschke.info (Middleware)
|
||||||
- `monitoring-grafana` — monitoring.kaleschke.info (Middleware)
|
- `monitoring-grafana` — monitoring.kaleschke.info (Middleware)
|
||||||
- `hermes-dashboard` — hermes.kaleschke.info (Middleware)
|
- `hermes-dashboard` — hermes.kaleschke.info (Middleware)
|
||||||
|
- `super-productivity` — sp.kaleschke.info (Middleware)
|
||||||
|
- `n8n` — n8n.kaleschke.info (Traefik ohne pauschale Middleware, native Auth + Webhook-Ausnahme analog Komodo)
|
||||||
- `Traefik-Dashboard`
|
- `Traefik-Dashboard`
|
||||||
- `AdGuard Home` — Admin-UI auf Port 8082 (`80` im Container), kein Traefik, nur Tailscale-IP `100.80.98.33`; 2026-05-26 bewusst keine 2FA-/Traefik-Umstellung
|
- `AdGuard Home` — Admin-UI auf Port 8082 (`80` im Container), kein Traefik, nur Tailscale-IP `100.80.98.33`; 2026-05-26 bewusst keine 2FA-/Traefik-Umstellung
|
||||||
|
|
||||||
@@ -238,7 +241,7 @@ Legende Status:
|
|||||||
| `AdGuard Home` | ✅ | `dns_net` (172.23.0.3), `frontend_net` | Port 53 DNS direkt, Port 8082 Admin nur auf Tailscale-IP `100.80.98.33` | DNS-Server + Upstream zu unbound; kein Traefik fuer Admin-UI | Admin-Port bleibt bewusst ohne Traefik/2FA, aber nicht mehr auf allen LAN-Interfaces |
|
| `AdGuard Home` | ✅ | `dns_net` (172.23.0.3), `frontend_net` | Port 53 DNS direkt, Port 8082 Admin nur auf Tailscale-IP `100.80.98.33` | DNS-Server + Upstream zu unbound; kein Traefik fuer Admin-UI | Admin-Port bleibt bewusst ohne Traefik/2FA, aber nicht mehr auf allen LAN-Interfaces |
|
||||||
| `unbound` | ✅ | `dns_net` | intern | Upstream-Resolver für AdGuard, isoliert | — |
|
| `unbound` | ✅ | `dns_net` | intern | Upstream-Resolver für AdGuard, isoliert | — |
|
||||||
| `ddns-updater` | ✅ | `frontend_net` | intern | Cloudflare DNS API; bleibt in `frontend_net` | Dokumentierte Ausnahme |
|
| `ddns-updater` | ✅ | `frontend_net` | intern | Cloudflare DNS API; bleibt in `frontend_net` | Dokumentierte Ausnahme |
|
||||||
| `tailscale` | ✅ | `host` | VPN-Zugang | Git-Stack (`host-services/tailscale/`) | nutzt `NET_ADMIN`, `NET_RAW` und `/dev/net/tun` als dokumentierte VPN-Ausnahme |
|
| `tailscale` | ✅ | `host` | VPN-Zugang / Subnet-Router | **Natives Unraid-Plugin** (`tailscale.plg`, Interface `tailscale1`, State `/boot/config/plugins/tailscale/state`) — **nicht** repo-/Komodo-verwaltet | Subnet-Router fuer `192.168.178.0/24`; der redundante userspace-Docker-Stack `host-services/tailscale/` wurde am 2026-06-06 entfernt |
|
||||||
|
|
||||||
### 7.2 Sicherheit / Identity
|
### 7.2 Sicherheit / Identity
|
||||||
|
|
||||||
@@ -271,7 +274,9 @@ Legende Status:
|
|||||||
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
||||||
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
|
| `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 |
|
| `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, **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 | — |
|
| `plex` | ✅ | `host` | Traefik via `plex.kaleschke.info` + Plex native Auth; LAN direkt `:32400` | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme; Traefik routet per File-Provider-Ausnahme auf `http://192.168.178.58:32400`, weil Docker-Labels Host-Netz-Container aus Traefik heraus auf `127.0.0.1` routen wuerden; kein direkter WAN-Port 32400 und Plex Remote Access bleibt aus; Server geclaimt von `Xeridos`; Smart-TVs (Schlafzimmer, Wohnzimmer) ueber WLAN-LAN per mDNS | — |
|
||||||
|
| `super-productivity` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | Persoenliche Task-PWA des Operators; Issues kommen aus Gitea `Micha/mails` via n8n-Mail-Workflow | Deploy + Webhook + DNS-Eintrag offen |
|
||||||
|
| `n8n` | ✅ vorbereitet | `frontend_net` | Traefik, native Auth (keine pauschale Authelia) | Workflow-Automation; erster Workflow: GMX-Mail -> OpenAI-Extraktion -> Gitea-Issue in `Micha/mails`; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret | Deploy + Webhook + Owner-Setup offen |
|
||||||
|
|
||||||
### 7.5 Admin / Operations
|
### 7.5 Admin / Operations
|
||||||
|
|
||||||
@@ -304,7 +309,7 @@ Legende Status:
|
|||||||
|
|
||||||
| Container | Status | Ziel |
|
| Container | Status | Ziel |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| — | — | Plex ist nicht mehr offen: der Dienst ist als Repo-Compose-Stack unter `host-services/plex/` dokumentiert; `host`-Netz bleibt als Discovery-Ausnahme. |
|
| — | — | Plex ist nicht mehr direkt offen: der Dienst ist als Repo-Compose-Stack unter `host-services/plex/` dokumentiert; `host`-Netz bleibt als Discovery-Ausnahme. Externer Zugriff laeuft ausschliesslich ueber Traefik/443 auf `plex.kaleschke.info`; keine direkte 32400-WAN-Freigabe. Technisch nutzt Plex als einzige Host-Netz-Route `traefik/dynamic/plex.yml`, weil Docker-Labels fuer `network_mode: host` in Traefik auf `127.0.0.1:32400` zeigen. |
|
||||||
|
|
||||||
### 7.8 Entfernte Container
|
### 7.8 Entfernte Container
|
||||||
|
|
||||||
@@ -402,6 +407,8 @@ Für den laufenden Betrieb gilt stattdessen:
|
|||||||
| `nextcloud` | keine zentrale ForwardAuth-Middleware | Nextcloud bringt eigene Auth, Clients und WebDAV/CardDAV-Endpunkte mit; Traefik bleibt Reverse Proxy, Auth bleibt app-nativ |
|
| `nextcloud` | keine zentrale ForwardAuth-Middleware | Nextcloud bringt eigene Auth, Clients und WebDAV/CardDAV-Endpunkte mit; Traefik bleibt Reverse Proxy, Auth bleibt app-nativ |
|
||||||
| `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP`; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume |
|
| `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP`; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume |
|
||||||
| `monitoring-promtail` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket |
|
| `monitoring-promtail` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket |
|
||||||
|
| `n8n` | keine pauschale Authelia-Middleware | Webhook-Endpunkte (`/webhook/*`, `/webhook-test/*`) muessen ohne ForwardAuth erreichbar bleiben; n8n bringt eigene Owner-/Login-Auth mit (analog Komodo/Nextcloud) |
|
||||||
|
| `plex` | Traefik ohne Authelia, File-Provider-Ausnahme trotz Host-Netz | Plex bringt native Konto-/Client-Auth mit; vorgeschaltete ForwardAuth wuerde Plex Web, Apps und Client-Flows stoeren. Docker-Labels sind fuer diesen Host-Netz-Container ungeeignet, weil Traefik sonst `127.0.0.1:32400` nutzt; daher `traefik/dynamic/plex.yml` mit Ziel `192.168.178.58:32400`. Route nur ueber Traefik/443 (`plex.kaleschke.info`), direkter Plex-WAN-Port 32400 und Plex Remote Access bleiben deaktiviert. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -459,6 +466,22 @@ Damit ist sofort klar:
|
|||||||
|
|
||||||
## 13. Betriebserfahrungen und Entscheidungs-Log
|
## 13. Betriebserfahrungen und Entscheidungs-Log
|
||||||
|
|
||||||
|
### Fix Common Problems Plugin entfernt (2026-06-03)
|
||||||
|
|
||||||
|
Befund: Drei `grep -R ... /usr/local/emhttp`-Prozesse liefen seit ~7 Tagen durchgehend mit je 100 % CPU (TIME+ 177-179 h). Status `R`, von PID 1 adoptierte Zombies einer laengst beendeten Fix-Common-Problems-(FCP)-Scan-Session. Folge: konstante Load 14.6 auf 12 Cores, IOWAIT-Peaks bis 55 %, USB-Flash unter Dauer-IO.
|
||||||
|
|
||||||
|
Ursache: Unraids `/usr/local/emhttp` enthaelt Symlinks `mnt -> /mnt` (mehrere TB Array) und `boot -> /boot` (USB-Flash). GNU `grep -R` dereferenziert Symlinks rekursiv. Ein FCP-Scan-Schritt (`/etc/cron.daily/fix.common.problems.sh -> scripts/scan.php`) hat dadurch effektiv die gesamte Array-Struktur gegrept und ist beim ersten Treffer-Loop haengen geblieben. Der Lock `/tmp/fix.common.problems/scanRunning` war vom 2026-06-03 04:40 - jeder weitere Daily-Cron-Run wuerde dasselbe Verhalten reproduzieren.
|
||||||
|
|
||||||
|
Massnahme: FCP-Plugin per `plugin remove fix.common.problems.plg` deinstalliert. Cron-Eintrag, Plugin-Verzeichnis und `/tmp`-Reste sauber. Load fiel innerhalb Minuten auf 1.08 (1-min).
|
||||||
|
|
||||||
|
Entscheidung: FCP wird bewusst **nicht** wieder installiert. Begruendung:
|
||||||
|
|
||||||
|
- Restliche Risiken werden bereits ueber andere Wege abgedeckt: Scrutiny (Laufwerks-SMART), Monitoring-Stack (Container-Health, Prometheus-Alerts, Blackbox), Posture-Check (Filesystem-/Drift-/Authelia-Audit), Critical-Events-Watcher (`services/posture-check/docker-critical-events.sh`).
|
||||||
|
- FCP ist ein externes Community-Plugin und nicht Teil der Repo-managed GitOps-Welt; Verhalten haengt von einer Online-Templates-Datei ab.
|
||||||
|
- Ein einmaliges Hang-up reicht, um die Flash-Drive 7 Tage lang zu thrashen - das Verhaeltnis Nutzen/Risiko ist negativ.
|
||||||
|
|
||||||
|
Folgen fuer Doku: Eintrag in `docs/AUDIT_2026-05-25_TODO.md` unter "Zuletzt geschlossen"; FCP taucht nicht mehr als Voraussetzung in DR/Monitoring-Pfaden auf, da es nie produktiv referenziert war.
|
||||||
|
|
||||||
### Plex Server Reclaim und LAN-only-Profil (2026-05-28)
|
### Plex Server Reclaim und LAN-only-Profil (2026-05-28)
|
||||||
|
|
||||||
Befund: Die `Preferences.xml` des Plex-Servers war seit dem 18.05.2026 13:18 jungfraeulich (391 Bytes, ohne `PlexOnlineMail`/`PlexOnlineUsername`/`PlexOnlineToken`). Der Server war damit nicht mit einem Plex.tv-Account geclaimt, obwohl die Smart-TVs ueber LAN-Discovery (mDNS/Plex-GDM) weiter funktionierten. Beim Login als `Xeridos` ueber `app.plex.tv` meldete der Server "Keine Berechtigung", weil kein Owner registriert war. Zusaetzlich war die `library_sections`-Konfiguration leer (Backups vom 19./22./28.05. ebenfalls ~370 KB statt MBs/GBs); die Bibliotheks-Konfiguration war seit dem 18.05. weg, die Filmdateien unter `/mnt/user/media/*` blieben aber intakt (~833 Verzeichnisse, davon `movies/` 1.4 TB und `Heimatfilme/` 300 GB).
|
Befund: Die `Preferences.xml` des Plex-Servers war seit dem 18.05.2026 13:18 jungfraeulich (391 Bytes, ohne `PlexOnlineMail`/`PlexOnlineUsername`/`PlexOnlineToken`). Der Server war damit nicht mit einem Plex.tv-Account geclaimt, obwohl die Smart-TVs ueber LAN-Discovery (mDNS/Plex-GDM) weiter funktionierten. Beim Login als `Xeridos` ueber `app.plex.tv` meldete der Server "Keine Berechtigung", weil kein Owner registriert war. Zusaetzlich war die `library_sections`-Konfiguration leer (Backups vom 19./22./28.05. ebenfalls ~370 KB statt MBs/GBs); die Bibliotheks-Konfiguration war seit dem 18.05. weg, die Filmdateien unter `/mnt/user/media/*` blieben aber intakt (~833 Verzeichnisse, davon `movies/` 1.4 TB und `Heimatfilme/` 300 GB).
|
||||||
@@ -474,7 +497,8 @@ Endstand:
|
|||||||
|
|
||||||
- `PlexOnlineUsername="Xeridos"`, `PlexOnlineMail="michideheld@gmx.de"`, `PlexOnlineHome="1"`.
|
- `PlexOnlineUsername="Xeridos"`, `PlexOnlineMail="michideheld@gmx.de"`, `PlexOnlineHome="1"`.
|
||||||
- Bibliotheken neu angelegt via Plex-Web → Verwalte Mediatheken → `/data/movies`, `/data/Heimatfilme` etc.
|
- 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.
|
- `PublishServerOnPlexOnlineKey="0"` (Remote Access deaktiviert), Plex-Relay aus.
|
||||||
|
- 2026-06-06: Externer Komfortzugriff ueber `https://plex.kaleschke.info` via Traefik ergaenzt. Das ist **kein** Plex-Remote-Access und keine direkte FRITZ!Box-Freigabe auf `32400`; Plex bleibt hinter Traefik/443 und nutzt native Plex-Auth.
|
||||||
|
|
||||||
Konsequenzen fuer Doku/Betrieb:
|
Konsequenzen fuer Doku/Betrieb:
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ services:
|
|||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
image: ghcr.io/immich-app/immich-machine-learning:release@sha256:a2501141440f10516d329fdfba2c68082e19eb9ba6016c061ac80d23beadf7f3
|
image: ghcr.io/immich-app/immich-machine-learning:release@sha256:a2501141440f10516d329fdfba2c68082e19eb9ba6016c061ac80d23beadf7f3
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Workaround fuer gunicorn-25.1.0-Control-Socket-Bug: der Worker haengt
|
||||||
|
# nach "Control socket listening at /usr/src/gunicorn.ctl" und erreicht
|
||||||
|
# nie "Application startup complete" -> Container bleibt dauerhaft
|
||||||
|
# unhealthy, ML (Gesichtserkennung/CLIP/Smart-Search) ist tot.
|
||||||
|
# --no-control-socket deaktiviert das fehlerhafte Feature. immich-ml
|
||||||
|
# startet gunicorn als Subprozess, der GUNICORN_CMD_ARGS aus der Env
|
||||||
|
# liest und anhaengt. Bestaetigte Upstream-Regression seit Immich 2.6
|
||||||
|
# (immich#27228, gunicorn#3510). Re-check: bei Immich-Update, das
|
||||||
|
# gunicorn auf >25.1.0/<25.1.0 mit Fix bringt, wieder entfernen.
|
||||||
|
GUNICORN_CMD_ARGS: "--no-control-socket"
|
||||||
volumes:
|
volumes:
|
||||||
- model-cache:/cache
|
- model-cache:/cache
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
mail-archiver:
|
mail-archiver:
|
||||||
image: s1t5/mailarchiver@sha256:ea7fd8c2e3e0ef0941e8dd9e726e35a8de33296f5c7b9ed811df5168ae6a9714
|
image: s1t5/mailarchiver@sha256:4ea7ecc47ad1dd2c523b85c3967574b61e39def1b6fd26edf874e21733c4018c
|
||||||
container_name: mail-archiver
|
container_name: mail-archiver
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ services:
|
|||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# OIDC: Authelia ueber Host-LAN-IP -> Traefik erreichbar (Container-DNS loest
|
||||||
|
# auth.kaleschke.info sonst nicht; gleiches Muster wie Komodo. SNI bleibt der
|
||||||
|
# Hostname, Let's-Encrypt-Cert validiert weiter.
|
||||||
|
extra_hosts:
|
||||||
|
- "auth.kaleschke.info:192.168.178.58"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
TZ: Europe/Berlin
|
TZ: Europe/Berlin
|
||||||
ALLOW_SIGNUP: "false"
|
ALLOW_SIGNUP: "false"
|
||||||
@@ -18,6 +24,16 @@ services:
|
|||||||
|
|
||||||
BASE_URL: https://mealie.kaleschke.info
|
BASE_URL: https://mealie.kaleschke.info
|
||||||
|
|
||||||
|
# --- Authelia OIDC SSO (additiv, 2026-06-06; lokaler Login bleibt) ---
|
||||||
|
OIDC_AUTH_ENABLED: "true"
|
||||||
|
OIDC_PROVIDER_NAME: Authelia
|
||||||
|
OIDC_CONFIGURATION_URL: https://auth.kaleschke.info/.well-known/openid-configuration
|
||||||
|
OIDC_CLIENT_ID: mealie
|
||||||
|
OIDC_CLIENT_SECRET: ${MEALIE_OIDC_CLIENT_SECRET}
|
||||||
|
OIDC_SIGNUP_ENABLED: "true"
|
||||||
|
OIDC_AUTO_REDIRECT: "false"
|
||||||
|
OIDC_REMEMBER_ME: "true"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/appdata/mealie/data:/app/data
|
- /mnt/user/appdata/mealie/data:/app/data
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
services:
|
||||||
|
n8n:
|
||||||
|
image: docker.n8n.io/n8nio/n8n:2.26.2@sha256:61ba01bc5e39304bbc928c9dbecd938c3a5cc1331b68affba6a34d0f654c43d9
|
||||||
|
container_name: n8n
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
dns:
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
GENERIC_TIMEZONE: Europe/Berlin
|
||||||
|
|
||||||
|
N8N_HOST: n8n.kaleschke.info
|
||||||
|
N8N_PORT: "5678"
|
||||||
|
N8N_PROTOCOL: https
|
||||||
|
N8N_EDITOR_BASE_URL: https://n8n.kaleschke.info/
|
||||||
|
WEBHOOK_URL: https://n8n.kaleschke.info/
|
||||||
|
N8N_PROXY_HOPS: "1"
|
||||||
|
|
||||||
|
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
|
||||||
|
|
||||||
|
N8N_DIAGNOSTICS_ENABLED: "false"
|
||||||
|
N8N_PERSONALIZATION_ENABLED: "false"
|
||||||
|
N8N_HIRING_BANNER_ENABLED: "false"
|
||||||
|
N8N_RUNNERS_ENABLED: "true"
|
||||||
|
N8N_BLOCK_ENV_ACCESS_IN_NODE: "true"
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/appdata/n8n/data:/home/node/.n8n
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- frontend_net
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=frontend_net"
|
||||||
|
- "traefik.http.routers.n8n.rule=Host(`n8n.kaleschke.info`)"
|
||||||
|
- "traefik.http.routers.n8n.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.n8n.tls=true"
|
||||||
|
- "traefik.http.routers.n8n.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.n8n.middlewares=secure-headers@file"
|
||||||
|
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend_net:
|
||||||
|
external: true
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
{
|
||||||
|
"name": "GMX -> OpenAI -> Gitea Issue (Super Productivity)",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"pollTimes": {
|
||||||
|
"item": [
|
||||||
|
{
|
||||||
|
"mode": "everyMinute"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"format": "simple",
|
||||||
|
"options": {
|
||||||
|
"customEmailConfig": "[\"UNSEEN\"]",
|
||||||
|
"forceReconnect": 15
|
||||||
|
},
|
||||||
|
"postProcessAction": "read"
|
||||||
|
},
|
||||||
|
"id": "11111111-1111-1111-1111-111111111111",
|
||||||
|
"name": "IMAP: GMX UNSEEN",
|
||||||
|
"type": "n8n-nodes-base.emailReadImap",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
240,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"credentials": {
|
||||||
|
"imap": {
|
||||||
|
"id": "REPLACE_GMX_IMAP_CRED_ID",
|
||||||
|
"name": "GMX IMAP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "a1",
|
||||||
|
"name": "from",
|
||||||
|
"value": "={{ $json.from }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a2",
|
||||||
|
"name": "subject",
|
||||||
|
"value": "={{ $json.subject }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a3",
|
||||||
|
"name": "date",
|
||||||
|
"value": "={{ $json.date }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a4",
|
||||||
|
"name": "messageId",
|
||||||
|
"value": "={{ $json.messageId || $json['message-id'] || '' }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "a5",
|
||||||
|
"name": "text",
|
||||||
|
"value": "={{ ($json.text || $json.textPlain || '').toString().slice(0, 8000) }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "22222222-2222-2222-2222-222222222222",
|
||||||
|
"name": "Extract mail fields",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [
|
||||||
|
460,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://api.openai.com/v1/chat/completions",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpHeaderAuth",
|
||||||
|
"sendHeaders": true,
|
||||||
|
"headerParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={\n \"model\": \"gpt-4o-mini\",\n \"temperature\": 0.2,\n \"response_format\": {\n \"type\": \"json_schema\",\n \"json_schema\": {\n \"name\": \"issue_extraction\",\n \"strict\": true,\n \"schema\": {\n \"type\": \"object\",\n \"additionalProperties\": false,\n \"required\": [\"title\", \"body_md\", \"priority\", \"due_date\", \"category\"],\n \"properties\": {\n \"title\": { \"type\": \"string\", \"maxLength\": 80 },\n \"body_md\": { \"type\": \"string\" },\n \"priority\": { \"type\": \"string\", \"enum\": [\"niedrig\", \"normal\", \"hoch\"] },\n \"due_date\": { \"type\": [\"string\", \"null\"], \"description\": \"ISO YYYY-MM-DD oder null\" },\n \"category\": { \"type\": \"string\" }\n }\n }\n }\n },\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Du extrahierst aus einer E-Mail eine Aufgabe fuer ein Issue-Tracking-System. Antworte ausschliesslich gemaess JSON-Schema. Sprache: Deutsch.\\n- title: imperativ, max. 80 Zeichen, ohne abschliessenden Punkt.\\n- body_md: 2 bis 6 Saetze. Was ist zu tun, warum, bis wann. Keine Begruessungen.\\n- priority: niedrig | normal | hoch.\\n- due_date: ISO YYYY-MM-DD wenn aus Mail ableitbar, sonst null.\\n- category: kurzes Schlagwort (rechnung, termin, technik, familie, sonstiges, ...).\"\n },\n {\n \"role\": \"user\",\n \"content\": {{ JSON.stringify('Absender: ' + $json.from + '\\nDatum: ' + $json.date + '\\nBetreff: ' + $json.subject + '\\n\\nMailtext:\\n' + $json.text) }}\n }\n ]\n}",
|
||||||
|
"options": {
|
||||||
|
"timeout": 60000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "33333333-3333-3333-3333-333333333333",
|
||||||
|
"name": "OpenAI: extract issue",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"position": [
|
||||||
|
680,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"credentials": {
|
||||||
|
"httpHeaderAuth": {
|
||||||
|
"id": "REPLACE_OPENAI_HEADER_AUTH_CRED_ID",
|
||||||
|
"name": "OpenAI Bearer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "b1",
|
||||||
|
"name": "extracted",
|
||||||
|
"value": "={{ JSON.parse($json.choices[0].message.content) }}",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "b2",
|
||||||
|
"name": "mail",
|
||||||
|
"value": "={{ $('Extract mail fields').item.json }}",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "44444444-4444-4444-4444-444444444444",
|
||||||
|
"name": "Parse OpenAI JSON",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [
|
||||||
|
900,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"assignments": {
|
||||||
|
"assignments": [
|
||||||
|
{
|
||||||
|
"id": "c1",
|
||||||
|
"name": "title",
|
||||||
|
"value": "={{ ($json.extracted.priority === 'hoch' ? '[P1] ' : '') + $json.extracted.title }}",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "c2",
|
||||||
|
"name": "body",
|
||||||
|
"value": "={{ $json.extracted.body_md + '\\n\\n---\\n**Kategorie:** ' + $json.extracted.category + '\\n**Prioritaet:** ' + $json.extracted.priority + ($json.extracted.due_date ? '\\n**Faellig:** ' + $json.extracted.due_date : '') + '\\n**Quelle:** Mail von ' + $json.mail.from + ' (' + $json.mail.date + ')\\n**Betreff:** ' + $json.mail.subject + '\\n**Message-ID:** ' + $json.mail.messageId }}",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "55555555-5555-5555-5555-555555555555",
|
||||||
|
"name": "Build issue payload",
|
||||||
|
"type": "n8n-nodes-base.set",
|
||||||
|
"typeVersion": 3.4,
|
||||||
|
"position": [
|
||||||
|
1120,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "https://git.kaleschke.info/api/v1/repos/Micha/mails/issues",
|
||||||
|
"authentication": "genericCredentialType",
|
||||||
|
"genericAuthType": "httpHeaderAuth",
|
||||||
|
"sendHeaders": true,
|
||||||
|
"headerParameters": {
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "Content-Type",
|
||||||
|
"value": "application/json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Accept",
|
||||||
|
"value": "application/json"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"sendBody": true,
|
||||||
|
"specifyBody": "json",
|
||||||
|
"jsonBody": "={\n \"title\": {{ JSON.stringify($json.title) }},\n \"body\": {{ JSON.stringify($json.body) }},\n \"assignees\": [\"Micha\"]\n}",
|
||||||
|
"options": {
|
||||||
|
"timeout": 30000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "66666666-6666-6666-6666-666666666666",
|
||||||
|
"name": "Gitea: create issue",
|
||||||
|
"type": "n8n-nodes-base.httpRequest",
|
||||||
|
"typeVersion": 4.2,
|
||||||
|
"position": [
|
||||||
|
1340,
|
||||||
|
300
|
||||||
|
],
|
||||||
|
"credentials": {
|
||||||
|
"httpHeaderAuth": {
|
||||||
|
"id": "REPLACE_GITEA_HEADER_AUTH_CRED_ID",
|
||||||
|
"name": "Gitea Token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"connections": {
|
||||||
|
"IMAP: GMX UNSEEN": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Extract mail fields",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Extract mail fields": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "OpenAI: extract issue",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"OpenAI: extract issue": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Parse OpenAI JSON",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Parse OpenAI JSON": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Build issue payload",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Build issue payload": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Gitea: create issue",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "homelab-n8n"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
nextcloud:
|
nextcloud:
|
||||||
image: nextcloud:33.0.4-apache@sha256:caa40b8beaf0057ac213d8dfc515c36ce64f7a8f0825b6a287e6f7cf2f4a095d
|
image: nextcloud:33.0.5-apache@sha256:56bdc45109067500fd0832fa64832b7c77a167d9394cbf5f0f4b59740b94194d
|
||||||
container_name: nextcloud
|
container_name: nextcloud
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
ntfy:
|
ntfy:
|
||||||
image: binwiederhier/ntfy@sha256:b32b4221a64ec2e7c000f0782b2feef24022e1a09a24e531640f4cbba6cfa1e6
|
image: binwiederhier/ntfy@sha256:f8a9b104313b87cc24ae4f775f39e6328205b57dff6ede3eaf098a91e5d79f59
|
||||||
container_name: ntfy
|
container_name: ntfy
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
dns:
|
dns:
|
||||||
|
|||||||
@@ -15,15 +15,18 @@ services:
|
|||||||
PAPERLESS_API_TOKEN: "${PAPERLESS_API_TOKEN}"
|
PAPERLESS_API_TOKEN: "${PAPERLESS_API_TOKEN}"
|
||||||
MANUAL_TAG: "paperless-gpt"
|
MANUAL_TAG: "paperless-gpt"
|
||||||
AUTO_TAG: "paperless-gpt-auto"
|
AUTO_TAG: "paperless-gpt-auto"
|
||||||
LLM_PROVIDER: "ollama"
|
LLM_PROVIDER: "openai"
|
||||||
LLM_MODEL: "qwen3:8b"
|
LLM_MODEL: "gpt-5.4-mini"
|
||||||
OLLAMA_HOST: "http://192.168.178.103:11434"
|
OPENAI_API_KEY: "${OPENAI_API_KEY}"
|
||||||
OLLAMA_CONTEXT_LENGTH: "4096"
|
OPENAI_BASE_URL: "https://api.openai.com/v1"
|
||||||
TOKEN_LIMIT: "1500"
|
TOKEN_LIMIT: "12000"
|
||||||
|
LLM_REQUESTS_PER_MINUTE: "30"
|
||||||
LLM_LANGUAGE: "German"
|
LLM_LANGUAGE: "German"
|
||||||
OCR_PROVIDER: "llm"
|
OCR_PROVIDER: "llm"
|
||||||
VISION_LLM_PROVIDER: "ollama"
|
VISION_LLM_PROVIDER: "openai"
|
||||||
VISION_LLM_MODEL: "minicpm-v:latest"
|
VISION_LLM_MODEL: "gpt-5.4-mini"
|
||||||
|
VISION_LLM_TEMPERATURE: "1.0"
|
||||||
|
VISION_LLM_REQUESTS_PER_MINUTE: "20"
|
||||||
OCR_PROCESS_MODE: "image"
|
OCR_PROCESS_MODE: "image"
|
||||||
CREATE_NEW_TAGS: "true"
|
CREATE_NEW_TAGS: "true"
|
||||||
AUTO_GENERATE_TITLE: "true"
|
AUTO_GENERATE_TITLE: "true"
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ services:
|
|||||||
image: ghcr.io/paperless-ngx/paperless-ngx:2.20.15@sha256:6c86cad803970ea782683a8e80e7403444c5bf3cf70de63b4d3c8e87500db92f
|
image: ghcr.io/paperless-ngx/paperless-ngx:2.20.15@sha256:6c86cad803970ea782683a8e80e7403444c5bf3cf70de63b4d3c8e87500db92f
|
||||||
container_name: paperless-ngx
|
container_name: paperless-ngx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# OIDC: Authelia ueber Host-LAN-IP -> Traefik erreichbar (Container-DNS sonst nicht)
|
||||||
|
extra_hosts:
|
||||||
|
- "auth.kaleschke.info:192.168.178.58"
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
environment:
|
environment:
|
||||||
@@ -17,6 +20,11 @@ services:
|
|||||||
- PAPERLESS_OCR_LANGUAGE=deu+eng
|
- PAPERLESS_OCR_LANGUAGE=deu+eng
|
||||||
- PAPERLESS_URL=https://paperless.kaleschke.info
|
- PAPERLESS_URL=https://paperless.kaleschke.info
|
||||||
|
|
||||||
|
# --- Authelia OIDC SSO (additiv, 2026-06-06; lokaler Login bleibt) ---
|
||||||
|
- PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect
|
||||||
|
- PAPERLESS_SOCIAL_AUTO_SIGNUP=true
|
||||||
|
- 'PAPERLESS_SOCIALACCOUNT_PROVIDERS={"openid_connect":{"OAUTH_PKCE_ENABLED":true,"APPS":[{"provider_id":"authelia","name":"Authelia","client_id":"paperless","secret":"${PAPERLESS_OIDC_SECRET}","settings":{"server_url":"https://auth.kaleschke.info"}}]}}'
|
||||||
|
|
||||||
# Barcode / ASN
|
# Barcode / ASN
|
||||||
- PAPERLESS_CONSUMER_ENABLE_BARCODES=1
|
- PAPERLESS_CONSUMER_ENABLE_BARCODES=1
|
||||||
- PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=1
|
- PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=1
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
services:
|
||||||
|
super-productivity:
|
||||||
|
image: johannesjo/super-productivity:v18.9.1@sha256:773760107344e739f4c29409f7842db66a1b167d50eb2c40248cb5b5b328652e
|
||||||
|
container_name: super-productivity
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
networks:
|
||||||
|
- frontend_net
|
||||||
|
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=frontend_net"
|
||||||
|
- "traefik.http.routers.super-productivity.rule=Host(`sp.kaleschke.info`)"
|
||||||
|
- "traefik.http.routers.super-productivity.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.super-productivity.tls=true"
|
||||||
|
- "traefik.http.routers.super-productivity.tls.certresolver=le"
|
||||||
|
- "traefik.http.routers.super-productivity.middlewares=authelia@file,secure-headers@file"
|
||||||
|
- "traefik.http.services.super-productivity.loadbalancer.server.port=80"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
frontend_net:
|
||||||
|
external: true
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
unbound:
|
unbound:
|
||||||
image: shaanmajid/unbound:1.25.1@sha256:96809ff052e8bd79bba30e067d8b27ed9a2f069b6b2a3484fe1d0eb45aba07c5
|
image: shaanmajid/unbound:1.25.1@sha256:f140db02a005904802bf5840093e95e675321aa060a00426fdffc2a3ac2eeb6b
|
||||||
container_name: unbound
|
container_name: unbound
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
### VOLUMES ###
|
||||||
|
DriveLetter Label FS Size_GB Free_GB Health
|
||||||
|
C (kein Label) NTFS 166.9 59.5 Healthy
|
||||||
|
D Daten-Projekte NTFS 167.7 148.6 Healthy
|
||||||
|
E Games NTFS 930.6 714.9 Healthy
|
||||||
|
G M2 SSD NTFS 930.9 877.5 Healthy
|
||||||
|
H Externe HDD NTFS 7452.0 3801.3 Healthy
|
||||||
|
(kein BW) Recovery x5 NTFS diverse diverse Healthy
|
||||||
|
|
||||||
|
### DISKS ###
|
||||||
|
Disk 0 INTEL SSDSC2BW180A3L SATA 167.68 GB GPT Healthy Serial: CVCV3105053K180EGN
|
||||||
|
Disk 1 INTEL SSDSC2BW180A3L SATA 167.68 GB GPT Healthy Serial: CVCV311302TH180EGN
|
||||||
|
Disk 2 Samsung SSD 980 PRO 1TB NVMe 931.51 GB GPT Healthy
|
||||||
|
Disk 3 WDC WDS100T2B0C NVMe 931.51 GB GPT Healthy
|
||||||
|
Disk 4 asmedia ASM235 USB 7.28 TB GPT Healthy
|
||||||
|
|
||||||
|
### PARTITIONS ###
|
||||||
|
Disk 0: [Reserved 16MB] [C: 166.87 GB Basic] [Recovery 809 MB]
|
||||||
|
Disk 1: [Reserved 15.98 MB] [D: 167.66 GB Basic]
|
||||||
|
Disk 2: [Reserved 15.98 MB] [E: 930.63 GB Basic] [Recovery 885 MB] <- F: ist weg
|
||||||
|
Disk 3: [System 100 MB] [Reserved 16 MB] [G: 930.89 GB Basic] [Recovery 524 MB]
|
||||||
|
Disk 4: [Reserved 15.98 MB] [H: 7.28 TB Basic]
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
### D:\ TOP-LEVEL ###
|
||||||
|
00_Inbox Directory 2026-06-04
|
||||||
|
10_Dokumente Directory 2026-06-04
|
||||||
|
11_Bilder Directory 2026-06-04 [ReadOnly-Attribut gesetzt]
|
||||||
|
12_Videos Directory 2026-06-04
|
||||||
|
13_Musik Directory 2026-06-04
|
||||||
|
14_Downloads Directory 2026-06-04
|
||||||
|
20_Projekte Directory 2026-06-04
|
||||||
|
30_Finanzen Directory 2026-06-04
|
||||||
|
90_Archiv Directory 2026-06-04
|
||||||
|
Micha Directory 2026-06-05 [Altquelle, noch vorhanden]
|
||||||
|
WSL Directory 2026-06-04 [nicht in Soll-Doku]
|
||||||
|
DumpStack.log File
|
||||||
|
|
||||||
|
### D:\Micha INHALT ###
|
||||||
|
Videos Directory 2026-06-05 [1 Datei, 0 MB - fast leer]
|
||||||
|
(alle anderen Unterordner weg)
|
||||||
|
|
||||||
|
### D:\00_Inbox INHALT ###
|
||||||
|
Desktop Directory 2026-06-05 [ReadOnly - das ist das Known-Folder-Ziel!]
|
||||||
|
|
||||||
|
### E:\ TOP-LEVEL ###
|
||||||
|
BattleNet Directory 2026-06-04 [SOLL]
|
||||||
|
EA Directory 2026-06-04 [SOLL]
|
||||||
|
EpicGames Directory 2026-06-04 [SOLL]
|
||||||
|
Riot Directory 2026-06-04 [SOLL]
|
||||||
|
Steam Directory 2026-06-05 [SOLL]
|
||||||
|
Ubisoft Directory 2026-06-04 [SOLL]
|
||||||
|
_Standalone FEHLT! [SOLL laut Doku]
|
||||||
|
|
||||||
|
### G:\ TOP-LEVEL ###
|
||||||
|
Apps Directory 2026-06-04 [nicht in Soll-Doku]
|
||||||
|
Gitea_Clone Directory 2026-04-15 [nicht in Soll-Doku - bewusst, homelab-infra]
|
||||||
|
repos Directory 2026-06-05 [SOLL]
|
||||||
|
Tools Directory 2026-06-05 [SOLL - Doku schreibt 'tools' lowercase, NTFS case-insensitive]
|
||||||
|
Workspace Directory 2026-06-04 [nicht in Soll-Doku]
|
||||||
|
|
||||||
|
### KNOWN FOLDER REDIRECTS (Ist) ###
|
||||||
|
Desktop -> D:\00_Inbox\Desktop [ABWEICHUNG! Soll: D:\Micha\Desktop]
|
||||||
|
Documents -> D:\10_Dokumente [OK]
|
||||||
|
Downloads -> D:\14_Downloads [OK]
|
||||||
|
Pictures -> D:\11_Bilder [OK]
|
||||||
|
Music -> D:\13_Musik [OK]
|
||||||
|
Videos -> D:\12_Videos [OK]
|
||||||
|
|
||||||
|
### DOPPELBESTAND D:\Micha\* vs D:\NN_* ###
|
||||||
|
D:\Micha\Dokumente : NICHT VORHANDEN | D:\10_Dokumente : 4011 Dateien, 595 MB
|
||||||
|
D:\Micha\Bilder : NICHT VORHANDEN | D:\11_Bilder : 7789 Dateien, 12367 MB
|
||||||
|
D:\Micha\Videos : 1 Datei, 0 MB | D:\12_Videos : 1 Datei, 0 MB
|
||||||
|
D:\Micha\Musik : NICHT VORHANDEN | D:\13_Musik : 0 Dateien
|
||||||
|
D:\Micha\Downloads : NICHT VORHANDEN | D:\14_Downloads : 2186 Dateien, 2211 MB
|
||||||
|
D:\Micha\Finanzen : NICHT VORHANDEN | D:\30_Finanzen : 126 Dateien, 123 MB
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
### OS BASELINE ###
|
||||||
|
Caption: Microsoft Windows 11 Pro
|
||||||
|
Build: 26200
|
||||||
|
Version: 10.0.26200
|
||||||
|
Architecture: 64-Bit
|
||||||
|
InstallDate: 2026-05-10 13:11:27
|
||||||
|
LastBoot: 2026-06-05 07:57:08
|
||||||
|
Uptime: 0.04 Tage (~1 Stunde zum Audit-Zeitpunkt)
|
||||||
|
Manufacturer: Micro-Star International Co., Ltd.
|
||||||
|
Model: MS-7D32
|
||||||
|
RAM: 31.79 GB
|
||||||
|
CPU: Intel Core i5-14600KF, 14 Cores, 20 Threads, 3500 MHz
|
||||||
|
|
||||||
|
### AKTIVIERUNG ###
|
||||||
|
Name: Windows(R), Professional edition
|
||||||
|
LicenseStatus: 1 (Aktiv)
|
||||||
|
Channel: OEM_DM
|
||||||
|
|
||||||
|
### AUSSTEHENDE UPDATES ###
|
||||||
|
Windows Update pending: 0
|
||||||
|
Reboot pending: Nein
|
||||||
|
|
||||||
|
### DEFENDER ###
|
||||||
|
AMProductVersion: 4.18.26040.7
|
||||||
|
AMServiceEnabled: True
|
||||||
|
AntivirusEnabled: True
|
||||||
|
AntispywareEnabled: True
|
||||||
|
RealTimeProtection: True
|
||||||
|
TamperProtection: True
|
||||||
|
SignatureAge: 0 Tage (aktuell)
|
||||||
|
Exclusions: KEIN ADMIN -> nicht lesbar
|
||||||
|
ASR Rules: KEIN ADMIN -> nicht lesbar (Get-MpPreference liefert leer)
|
||||||
|
|
||||||
|
### FIREWALL ###
|
||||||
|
Domain: Enabled, DefaultInboundAction: NotConfigured, DefaultOutboundAction: NotConfigured
|
||||||
|
Private: Enabled, DefaultInboundAction: NotConfigured, DefaultOutboundAction: NotConfigured
|
||||||
|
Public: Enabled, DefaultInboundAction: NotConfigured, DefaultOutboundAction: NotConfigured
|
||||||
|
HINWEIS: NotConfigured = Windows-Default (eingehend blockieren, ausgehend erlauben)
|
||||||
|
|
||||||
|
### BITLOCKER ###
|
||||||
|
KEIN ADMIN -> Get-BitLockerVolume verweigert (Access Denied). Status unbekannt.
|
||||||
|
|
||||||
|
### SECURE BOOT ###
|
||||||
|
KEIN ADMIN -> Confirm-SecureBootUEFI verweigert. Status unbekannt.
|
||||||
|
|
||||||
|
### TPM ###
|
||||||
|
KEIN ADMIN -> Get-Tpm liefert alle Felder leer. Status unbekannt.
|
||||||
|
|
||||||
|
### UAC ###
|
||||||
|
EnableLUA: 1 (aktiv)
|
||||||
|
ConsentPromptBehaviorAdmin: 5 (Nachfrage mit UI, ohne Secure Desktop laut Wert, aber...)
|
||||||
|
PromptOnSecureDesktop: 1 (Secure Desktop ist AN - Standard-Konfiguration korrekt)
|
||||||
|
|
||||||
|
### LOKALE ADMINS ###
|
||||||
|
Gruppe Administratoren: Administrator, michi
|
||||||
|
|
||||||
|
### BCD ###
|
||||||
|
KEIN ADMIN -> bcdedit /enum verweigert.
|
||||||
|
Letzte bekannte Aussage (Doku boot-cleanup-plan): Keine partition=F: Referenz nach Cleanup + Neustarttest.
|
||||||
|
|
||||||
|
### WinRE ###
|
||||||
|
KEIN ADMIN -> reagentc /info verweigert.
|
||||||
|
Letzte bekannte Aussage (Doku): WinRE Disabled.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
### NETZWERK-ADAPTER (UP) ###
|
||||||
|
Ethernet Intel I225-V MAC: 04-7C-16-53-04-E4 1 Gbps
|
||||||
|
Tailscale Tunnel 100 Gbps (virtuell)
|
||||||
|
vEthernet WSL (Hyper-V) MAC: 00-15-5D-F3-5F-C9 10 Gbps (virtuell)
|
||||||
|
|
||||||
|
### IP-ADRESSEN ###
|
||||||
|
Ethernet: 192.168.178.103/24
|
||||||
|
Tailscale: 100.78.133.37/32
|
||||||
|
WSL bridge: 172.26.80.1/20
|
||||||
|
(WLAN, Bluetooth etc.: APIPA 169.254.x.x - nicht konfiguriert/inaktiv)
|
||||||
|
|
||||||
|
### DNS ###
|
||||||
|
Ethernet DNS: 192.168.178.58 (= Kallilabcore AdGuard Home)
|
||||||
|
WLAN DNS: 192.168.178.58
|
||||||
|
|
||||||
|
### TAILSCALE STATUS ###
|
||||||
|
100.78.133.37 baerchen-1 (dieser Rechner) online
|
||||||
|
100.105.203.21 baerchen (alter Rechner) offline, last seen 20h ago
|
||||||
|
100.73.83.55 iphone-14 iOS online
|
||||||
|
100.112.0.90 kallilab-core linux online
|
||||||
|
100.80.98.33 kallilabcore linux active; direct 192.168.178.58:49917
|
||||||
|
|
||||||
|
### LAUSCHENDE TCP-PORTS ###
|
||||||
|
Port Adresse Prozess Bemerkung
|
||||||
|
135 0.0.0.0/:: svchost RPC Endpoint Mapper
|
||||||
|
139 192.168.178.103 System NetBIOS
|
||||||
|
445 :: System SMB
|
||||||
|
3000 ::1/:: wslrelay / docker Docker / WSL lokal
|
||||||
|
5040 0.0.0.0 svchost WS-Discovery (WDAS)
|
||||||
|
5357 :: System WSD HTTP
|
||||||
|
7680 :: svchost WUDO (Delivery Optimization)
|
||||||
|
11434 127.0.0.1 ollama Ollama API (lokal)
|
||||||
|
22885 127.0.0.1 Battle.net lokal
|
||||||
|
26822 127.0.0.1 MSI.TerminalServer MSI Center
|
||||||
|
27036 0.0.0.0 steam Steam Remote Play (0.0.0.0 - offen!)
|
||||||
|
27060 127.0.0.1 steam Steam lokal
|
||||||
|
32683 127.0.0.1 MSI.CentralServer MSI Center
|
||||||
|
33683 127.0.0.1 MSI.CentralServer MSI Center
|
||||||
|
38810 fd7a:... tailscaled
|
||||||
|
49553 100.78.133.37 tailscaled
|
||||||
|
50123 127.0.0.1 iCUE Corsair lokal
|
||||||
|
51037 127.0.0.1 RazerAppEngine
|
||||||
|
55316 127.0.0.1 RazerAppEngine
|
||||||
|
59686 127.0.0.1 steam
|
||||||
|
60999 127.0.0.1 Agent Claude Code
|
||||||
|
|
||||||
|
### SSH ###
|
||||||
|
~\.ssh\config: LEER (keine Host-Eintraege)
|
||||||
|
~\.ssh\id_ed25519: vorhanden (411 Bytes, erstellt 2026-04-04)
|
||||||
|
~\.ssh\id_ed25519.pub: vorhanden (97 Bytes)
|
||||||
|
~\.ssh\known_hosts: vorhanden (4719 Bytes, zuletzt 2026-06-04)
|
||||||
|
~\.ssh\known_hosts.old + .pre-port222-Backup: vorhanden
|
||||||
|
|
||||||
|
KEY PERMISSIONS id_ed25519:
|
||||||
|
NT-AUTORITAET\SYSTEM FullControl Allow
|
||||||
|
VORDEFINIERT\Administratoren FullControl Allow
|
||||||
|
baerchen\michi FullControl Allow
|
||||||
|
BEFUND: Zu viele Berechtigungen - Admins-Gruppe hat FullControl auf Private Key.
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
### DEV TOOLCHAIN ###
|
||||||
|
git: 2.54.0.windows.1
|
||||||
|
python: 3.13.13
|
||||||
|
node: 24.16.0 (LTS)
|
||||||
|
go: 1.26.4 windows/amd64
|
||||||
|
|
||||||
|
### GIT CONFIG ###
|
||||||
|
user.name: michaelkaleschke-spec
|
||||||
|
user.email: michaelkaleschke@googlemail.com
|
||||||
|
commit.gpgsign: nicht gesetzt (Commits nicht signiert)
|
||||||
|
|
||||||
|
### WSL ###
|
||||||
|
Ubuntu Stopped Version 2
|
||||||
|
docker-desktop Running Version 2
|
||||||
|
|
||||||
|
### DOCKER CONTEXTS ###
|
||||||
|
default npipe:////./pipe/docker_engine (nicht aktiv)
|
||||||
|
desktop-linux* npipe:////./pipe/dockerDesktopLinuxEngine (aktiv)
|
||||||
|
|
||||||
|
### KUBECTL ###
|
||||||
|
Keine Contexts konfiguriert.
|
||||||
|
|
||||||
|
### WINGET INVENTAR (158 Pakete, Auswahl) ###
|
||||||
|
CPUID CPU-Z MSI 2.20.1
|
||||||
|
CPUID HWMonitor 1.63
|
||||||
|
CrystalDiskInfo 9.9.1
|
||||||
|
Docker Desktop 4.76.0
|
||||||
|
Git 2.54.0
|
||||||
|
AusweisApp 2.5.1
|
||||||
|
Node.js LTS 24.16.0
|
||||||
|
Corsair iCUE5 5.46.67
|
||||||
|
NVIDIA App 11.0.7.247 / Treiber 610.47
|
||||||
|
WISO Steuer 2026 33.07.3410
|
||||||
|
Go 1.26.4
|
||||||
|
Microsoft Edge 148.0.3967.96
|
||||||
|
Microsoft OneDrive 23.038 (Update verfuegbar: 26.078)
|
||||||
|
RivaTuner Statistics Server 7.3.7
|
||||||
|
Razer Synapse 4.0.683
|
||||||
|
Steam 2.10.91.91
|
||||||
|
Banking4 Home
|
||||||
|
Battle.net / Hearthstone / Overwatch / World of Warcraft
|
||||||
|
Microsoft 365 16.0.20026.20140
|
||||||
|
|
||||||
|
### AUTOSTART ###
|
||||||
|
HKCU\Run:
|
||||||
|
BraveSoftware Update -> BraveUpdateCore.exe
|
||||||
|
Steam -> E:\Steam\steam.exe -silent
|
||||||
|
RazerAppEngine -> Synapse autoStart
|
||||||
|
Docker Desktop -> Docker Desktop.exe
|
||||||
|
|
||||||
|
HKLM\Run:
|
||||||
|
SecurityHealth -> SecurityHealthSystray.exe
|
||||||
|
Corsair iCUE5 -> iCUE Launcher.exe --autorun
|
||||||
|
RtkAudUService -> Realtek Audio Service
|
||||||
|
|
||||||
|
Startup-Ordner (User): Ollama.lnk
|
||||||
|
Startup-Ordner (Alle): Tailscale.lnk
|
||||||
|
|
||||||
|
### GEPLANTE TASKS (nicht-Microsoft, aktiv) ###
|
||||||
|
OneDrive Reporting Task
|
||||||
|
OneDrive Startup Task
|
||||||
|
OneDrive Per-Machine Standalone Update Task
|
||||||
|
PostponeDeviceSetupToast
|
||||||
|
BraveSoftwareUpdateTask (2x User-Varianten)
|
||||||
|
NVIDIA App SelfUpdate
|
||||||
|
SoftLanding\CreativeManagementTask [UNBEKANNT - pruefen]
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
### HARDWARE ###
|
||||||
|
CPU: Intel Core i5-14600KF, 14 Cores / 20 Threads, 3500 MHz Base
|
||||||
|
RAM: 31.79 GB
|
||||||
|
MB: MSI MS-7D32
|
||||||
|
Energieplan: Ausbalanciert (381b4222) - aktiv
|
||||||
|
Verfuegbare Plaene: Ausbalanciert, Ultimative Leistung, Hoechstleistung, Energiesparmodus
|
||||||
|
|
||||||
|
### PHYSICAL DISKS (SMART) ###
|
||||||
|
INTEL SSDSC2BW180A3L SSD Healthy OK (Disk 0, C:)
|
||||||
|
INTEL SSDSC2BW180A3L SSD Healthy OK (Disk 1, D:)
|
||||||
|
Samsung SSD 980 PRO 1TB SSD Healthy OK (Disk 2, E:)
|
||||||
|
WDC WDS100T2B0C SSD Healthy OK (Disk 3, G:)
|
||||||
|
asmedia ASM235 Unspecified Healthy OK (Disk 4, H:)
|
||||||
|
Get-StorageReliabilityCounter: keine Ausgabe (Wear-Daten nicht via WMI verfuegbar - typisch fuer SATA SSDs und USB)
|
||||||
|
|
||||||
|
### GERAETE MIT STATUS "Unknown" (PnP) ###
|
||||||
|
MyBookLiveDuo (SoftwareDevice) - Netzwerkgeraet, nicht angebunden - erwartet
|
||||||
|
HID-Tastatur (Keyboard) - ghosted device - harmlos
|
||||||
|
Dell S2722DGM (DP) (Monitor) - Display-Enumeration Artefakt
|
||||||
|
Generic Monitor x2 - Display-Enumeration Artefakt
|
||||||
|
[LG] webOS TV OLED65G48LW x2 - Netzwerkgeraet, nicht lokal - erwartet
|
||||||
|
Standard-Volumeschattenkopie x3 - VSS Snapshots - erwartet
|
||||||
|
KEINE echten Fehlercodes (kein gelbes Ausrufezeichen).
|
||||||
|
|
||||||
|
### EVENT LOG FEHLER seit Installation (2026-05-10) ###
|
||||||
|
ID 20 (70x): Defender KB4052623 Installation fehlgeschlagen (0x80240016)
|
||||||
|
-> Timing-Problem bei Update-Kaskade, harmlos wenn aktuell
|
||||||
|
ID 10010 (15x): DCOM Server-Timeout {3E11DF0F-...}
|
||||||
|
-> bekanntes Windows-Hintergrundrauschen, harmlos
|
||||||
|
ID 7000 (3x): Steam Client Service Start fehlgeschlagen
|
||||||
|
-> Steam war beim Boot noch nicht bereit, harmlos
|
||||||
|
ID 7023 (3x): Windows Modules Installer beendet mit Fehler
|
||||||
|
-> Update-Installationsabbrueche, pruefbar nach Analyse der Zeitstempel
|
||||||
|
ID 6008 (2x): Unerwartetes Herunterfahren am 2026-05-19 13:56:56
|
||||||
|
-> Einmaliger Vorfall (BSOD oder Stromausfall) kurz nach Installation
|
||||||
|
ID 7034 (2x): MSI Center Service unerwartet beendet
|
||||||
|
-> bekannte Instabilitaet MSI Center, harmlos wenn kein Datenverlust
|
||||||
|
ID 7043 (1x): Dienst konnte nicht gestoppt werden
|
||||||
|
ID 1012 (3x): unbekannte ID - weitere Analyse noetig
|
||||||
|
ID 36 (2x): unbekannte ID - weitere Analyse noetig
|
||||||
|
|
||||||
|
### CRASH DUMPS ###
|
||||||
|
C:\Windows\Minidump: nicht vorhanden
|
||||||
|
C:\Windows\MEMORY.DMP: nicht vorhanden
|
||||||
|
Bewertung: kein BSOD-Dump vorhanden (ggf. Dump-Einstellung "automatisch neu starten" ohne Dump-Schreiben)
|
||||||
+26
-6
@@ -1,6 +1,6 @@
|
|||||||
# AI Context
|
# AI Context
|
||||||
|
|
||||||
Stand: 2026-06-01
|
Stand: 2026-06-05
|
||||||
|
|
||||||
Kurzer Kontext fuer KI-Agenten. Nicht als Ersatz fuer die echten Runbooks lesen.
|
Kurzer Kontext fuer KI-Agenten. Nicht als Ersatz fuer die echten Runbooks lesen.
|
||||||
|
|
||||||
@@ -43,18 +43,38 @@ Kurzer Kontext fuer KI-Agenten. Nicht als Ersatz fuer die echten Runbooks lesen.
|
|||||||
|
|
||||||
## Aktuelle Restpunkte
|
## Aktuelle Restpunkte
|
||||||
|
|
||||||
Authoritativ: `docs/AUDIT_2026-05-25_TODO.md`.
|
Authoritativ: `docs/MASTER_TODO.md`.
|
||||||
|
|
||||||
Kurzfassung:
|
Kurzfassung:
|
||||||
|
|
||||||
- Alt-Volumes fruehestens ab 2026-06-02 freigeben
|
|
||||||
- Hetzner-Account-Hygiene und Borg `append-only` pruefen
|
|
||||||
- FRITZ!Box-Servicefenster fuer Update, Config-Backup und IPv6-Exposure planen
|
|
||||||
- Auth-/OIDC-/CrowdSec-/Hermes-Themen bewusst geparkt
|
- Auth-/OIDC-/CrowdSec-/Hermes-Themen bewusst geparkt
|
||||||
|
- Wochenend-Sprint 2026-06-05: `docs/WEEKEND_EXECUTION_PLAN_2026-06-05.md`
|
||||||
|
und `docs/WEEKEND_STATUS_2026-06-05.md`
|
||||||
|
|
||||||
Letzte Bestaetigung:
|
Letzte Bestaetigung:
|
||||||
|
|
||||||
|
- Windows-Image `baerchen`: Veeam Agent Free Job `baerchen-c-image` auf
|
||||||
|
`\\kallilabcore\backups\windows-images\baerchen`, erster Full-Backup-Lauf
|
||||||
|
2026-06-05 erfolgreich, GUI-Wert 53,8 GB, Dauer 0:11:31. Recovery-USB ist
|
||||||
|
erstellt; Boot-/SMB-/Restore-Point-Test ohne Restore ist noch offen.
|
||||||
|
- Veeam Storage Encryption ist beim ersten Full-Lauf nicht aktiv
|
||||||
|
(`StorageEncryptionEnabled=False`); nachtraegliche Aktivierung ist eine
|
||||||
|
Operator-Entscheidung, weil sie Passwort- und Restore-Prozess aendert.
|
||||||
|
- BitLocker fuer `baerchen` ist bewusst nicht aktiviert und bleibt
|
||||||
|
Operator-Entscheidung.
|
||||||
|
- Tailscale-Inventar 2026-06-05 real gemessen: `Kallilabcore`
|
||||||
|
`100.80.98.33`, IPv6 `fd7a:115c:a1e0::2c01:62b2`, kein Exit Node, aber
|
||||||
|
aktiver Subnet Router fuer `192.168.178.0/24`. Dadurch ist die Tailnet-ACL
|
||||||
|
sicherheitsrelevant; Entscheidung Default-Allow vs tag-basierte ACL offen.
|
||||||
|
- Unraid-Flash-Backup-Artefaktpruefung: `ops/maintenance/check-unraid-flash-backup.sh`
|
||||||
|
prueft Artefakt, SHA256, Alter und Kern-Configs. Test 2026-06-05 gegen Host
|
||||||
|
erfolgreich laut `docs/MASTER_TODO.md`.
|
||||||
- Borg-Nachlauf 2026-06-01 erfolgreich: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, Freshness Critical 0 / Warnings 0.
|
- Borg-Nachlauf 2026-06-01 erfolgreich: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, Freshness Critical 0 / Warnings 0.
|
||||||
- H:/ Nearline-Pull 2026-06-01 repariert: Borg-Dumps werden kuratiert kopiert, Gitea-Bundles aktuell.
|
- H:/ Nearline-Pull 2026-06-01 repariert: Borg-Dumps werden kuratiert kopiert, Gitea-Bundles aktuell.
|
||||||
- Family-Status-Dashboard liegt als `monitoring/grafana/dashboards/family-status.json` im Repo.
|
- Family-Status-Dashboard liegt als `monitoring/grafana/dashboards/family-status.json` im Repo.
|
||||||
- Alt-Volume-Freigabe ist per `ops/maintenance/release-alt-volumes.sh` vorbereitet; `--execute` nicht vor 2026-06-02.
|
- Alt-Volumes nach PG18/VectorChord-Burn-in sind seit 2026-06-02 reversibel archiviert unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602`; die alten Originalpfade sind nicht mehr aktiv gemountet.
|
||||||
|
- Family-Onboarding ist auf drei Nutzungsziele fokussiert: Vaultwarden, Immich und Mealie; praktischer Ablauf in `docs/FAMILY_ONBOARDING.md`.
|
||||||
|
- Externer Betreibercheck: `ops/maintenance/check-external-operator.sh`; FRITZ!Box 7590 meldet FRITZ!OS `154.08.25`, DNS fuer Public Apps hat keine AAAA-Records, Host hat keine globale Provider-IPv6.
|
||||||
|
- FRITZ!Box-UI 2026-06-01: Remote-HTTPS auf FRITZ!Box-UI aus, FTP/FTPS auf Speichermedien aus, WAN-Freigabe nur `443/tcp`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
|
||||||
|
- FRITZ!Box-Konfig-Backup 2026-06-01 extern/off-system in Vaultwarden abgelegt; Datei und Kennwort bleiben ausserhalb des Repos.
|
||||||
|
- Hetzner-Account-Hygiene 2026-06-01 erledigt: 2FA aktiv, Recovery Key offline gedruckt, Zahlung ok; Storage Box SSH-only, Maintenance-Key in Vaultwarden. Append-only forced-command brach Key-Auth und wurde per Passwort-Recovery zurueckgesetzt; Operator-Entscheidung: fuer dieses Homelab bewusst nicht umsetzen.
|
||||||
|
|||||||
+4
-2
@@ -1,6 +1,6 @@
|
|||||||
# Alert Rules
|
# Alert Rules
|
||||||
|
|
||||||
Stand: 2026-05-31
|
Stand: 2026-06-05
|
||||||
|
|
||||||
Diese Datei beschreibt die produktiven Alarmwege und wichtigsten Regeln. Die
|
Diese Datei beschreibt die produktiven Alarmwege und wichtigsten Regeln. Die
|
||||||
Konfiguration selbst liegt in `monitoring/prometheus/alerts.yml` und in den
|
Konfiguration selbst liegt in `monitoring/prometheus/alerts.yml` und in den
|
||||||
@@ -49,4 +49,6 @@ Die Liste der ueberwachten Critical-Container steht in
|
|||||||
- Kein Inode-Alarm. Bei Paperless/Immich spaeter sinnvoll, aber aktuell kein
|
- Kein Inode-Alarm. Bei Paperless/Immich spaeter sinnvoll, aber aktuell kein
|
||||||
dokumentierter Vorfall.
|
dokumentierter Vorfall.
|
||||||
- Container-Memory-Limits werden erst nach realen Peak-Daten gesetzt; OOM/kill
|
- Container-Memory-Limits werden erst nach realen Peak-Daten gesetzt; OOM/kill
|
||||||
wird bereits ueber `docker-critical-events.sh` gemeldet.
|
wird ueber `docker-critical-events.sh` gemeldet, sobald der Host-Watcher per
|
||||||
|
Unraid User Script aktiviert ist. Start/Stop/Status/Smoke laufen ueber
|
||||||
|
`services/posture-check/docker-critical-events-supervisor.sh`.
|
||||||
|
|||||||
@@ -3,32 +3,61 @@
|
|||||||
Status: **kompakte Restliste**. Die erledigten Sprint-Tabellen und langen
|
Status: **kompakte Restliste**. Die erledigten Sprint-Tabellen und langen
|
||||||
Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Git.
|
Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Git.
|
||||||
|
|
||||||
|
Letzter Sync mit `docs/MASTER_TODO.md`: 2026-06-05. Offene Punkte sind deckungsgleich;
|
||||||
|
neue Restore-Runbook-Stubs (Unraid Flash / AdGuard / Tailscale / Redis 8) wurden
|
||||||
|
in `docs/RESTORE_MATRIX.md` ergaenzt.
|
||||||
|
|
||||||
## Aktuell offene Punkte
|
## Aktuell offene Punkte
|
||||||
|
|
||||||
| Prioritaet | Punkt | Naechster Schritt |
|
| Prioritaet | Punkt | Naechster Schritt |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| P0 | Alt-Volumes nach Burn-in freigeben | Ab 2026-06-02 `ops/maintenance/release-alt-volumes.sh --dry-run` pruefen, danach nur bei sauberem Ergebnis mit `--execute` freigeben |
|
| P2 | Family-Onboarding praktisch starten | Fokus: Vaultwarden als Passwortbasis, Immich-Mobile-Backup auf jedem Handy, Mealie mit erstem Rezept/Einkaufsliste; Ablauf steht in `docs/FAMILY_ONBOARDING.md` |
|
||||||
| P1 | Hetzner-Account-Hygiene | Starkes einzigartiges Passwort, Backup-Zahlungsweg und Login-Benachrichtigungen extern bestaetigen |
|
|
||||||
| P1 | Borg `--append-only` fuer Hetzner pruefen | Rollback-faehigen Test fuer `borg serve --append-only` in Hetzner `authorized_keys` planen |
|
## Restore-Audit Backlog (Stand 2026-06-03)
|
||||||
| P1 | FRITZ!Box-Servicefenster | FRITZ!OS-Update, FRITZ!Box-Konfig-Backup und IPv6-Exposure-Pruefung gemeinsam erledigen |
|
|
||||||
| P2 | Family-Onboarding praktisch starten | Familienkonten, Vaultwarden-Organisation und Immich-Mobile-Backup gemeinsam einrichten |
|
Ergebnis des Restore-Skills-Audits (Session 2026-06-02/03). Die kritischen Bugfixes (Cron-OR-Semantik, ntfy-Race, Cleanup-Trap, Pfad-Inkonsistenz, Vaultwarden-Token, Paperless-Retry, Header-Validierung, Authelia-Test) sind erledigt und committed. Die folgenden Punkte sind bewusst offener Backlog:
|
||||||
|
|
||||||
|
| Prioritaet | Punkt | Status | Naechster Schritt |
|
||||||
|
|---|---|---|---|
|
||||||
|
| P1 | Nextcloud-Restore-Test | **erledigt 2026-06-03** | Borg-Extract + pg_restore (126 Tabellen) + HTTP 200 + `occ status maintenance:false`. Quelle: `hetzner_borg_appdata_critical`, Archiv `Taegliche-Sicherung-2026-06-03T04:30:41.432`. Zwei Skript-Bugs im Zuge des Laufs gefixt (`check_data_directory_permissions: false` patchen, `.ncdata`-Marker anlegen). Report `/mnt/user/backups/restore-reports/nextcloud-2026-06-03.md`. |
|
||||||
|
| P1 | Shared PostgreSQL 18 Cluster Restore Drill | **erledigt 2026-06-03** | Globals + 5 DBs (paperless 72t, mailarchiver 1t, authelia 25t, nextcloud 126t, mealie 66t), `data_checksums=on`, Report `/mnt/user/backups/restore-reports/shared-pg-cluster-2026-06-03.md` |
|
||||||
|
| P1 | Komodo-Mongo-Daten-Restore | **erledigt 2026-06-03** | 86904 Dokumente erfolgreich restored, Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`. Nebenbefund: Dump von Mongo 8.0.23, Test auf 7.0.32 — Cross-Version-Warning, fuer Lesetest harmlos |
|
||||||
|
| P2 | Mailarchiver-Restore-Test | **erledigt 2026-06-03** | Data-Protection-Keys + 645M pg_restore + HTTP 200. Report `/mnt/user/backups/restore-reports/mailarchiver-2026-06-03.md` |
|
||||||
|
| P2 | Mealie-Restore-Test | **erledigt 2026-06-03** | Borg-Data + pg_restore + HTTP 200, 3 Rezepte. Report `/mnt/user/backups/restore-reports/mealie-2026-06-03.md` |
|
||||||
|
| P2 | Traefik-Restore-Test | **erledigt 2026-06-03** | dynamic/ + letsencrypt/ aus Borg, File-Provider + Ping 200. CF-Token bewusst nicht im Smoke. Report `/mnt/user/backups/restore-reports/traefik-2026-06-03.md` |
|
||||||
|
| P3 | Negativ-Test fuer Frische-Check | offen | Einmal pro Quartal bewusst kaputten Dump einfuettern und pruefen ob `homelab-alerts` feuert |
|
||||||
|
| P3 | End-to-end-DR-Drill | offen | Komplett-Bootstrap Phase 1-5 auf einem Wegwerf-Host; realistisch nur mit zweiter Hardware |
|
||||||
|
|
||||||
## Bewusst geparkt
|
## Bewusst geparkt
|
||||||
|
|
||||||
| Punkt | Entscheidung |
|
| Punkt | Entscheidung |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Authelia 2FA fuer Operator-UIs | In diesem Zyklus nicht umgesetzt; erst mit finaler Auth-Policy |
|
| Authelia 2FA fuer Operator-UIs (Rest) | Tier-1-Operator-UIs sind 2026-06-03 auf `two_factor` gehoben (`files`, `scrutiny`, `borg`, `code`). Restliche Admin-UIs (`monitoring`, `glances`, `glance`, `speedtest`, `paperless-gpt`, `pdf`, `mail`, `hermes`, `sp`) bleiben bewusst auf `one_factor`, bis die finale Auth-Policy steht. |
|
||||||
| Authelia OIDC fuer Apps | Geparkt bis klare Familien-/SSO-Entscheidung |
|
| Authelia OIDC fuer Apps | Geparkt bis klare Familien-/SSO-Entscheidung |
|
||||||
| CrowdSec vor Traefik | Erst nach Auth-Policy neu bewerten |
|
| CrowdSec vor Traefik | Bewusst nicht umgesetzt: einzige WAN-Tuer ist `443/tcp`, Operator-Pfad ist Tailscale, Authelia-`regulation:` deckt Auth-Brute-Force ab. Neu bewerten bei breiterer Attack Surface. |
|
||||||
| Nextcloud 2FA/Brute-Force-Haertung | Gemeinsam mit OIDC/Familienkonten entscheiden |
|
| Nextcloud 2FA/Brute-Force-Haertung | UI-Schritt fuer Operator-Account (`twofactor_totp` aktivieren) bleibt offen. App-weite Familien-Policy gemeinsam mit OIDC entscheiden. |
|
||||||
| Hermes-Agent | NAS-Stack bleibt deaktiviert; Review-Deadline 2026-07-25 |
|
| Hermes-Agent | NAS-Stack bleibt deaktiviert; Review-Deadline 2026-07-25 |
|
||||||
| USV | Anschaffung verschoben; Power-Loss-Risiko bewusst akzeptiert |
|
| USV | Anschaffung verschoben; Power-Loss-Risiko bewusst akzeptiert |
|
||||||
| Zweites Off-site-Ziel | Bewusst nicht umgesetzt; neu bewerten bei Hetzner-Problemen, stark wachsendem Datenwert oder geaenderter Betreiber-Praeferenz |
|
| Zweites Off-site-Ziel | Bewusst nicht umgesetzt; neu bewerten bei Hetzner-Problemen, stark wachsendem Datenwert oder geaenderter Betreiber-Praeferenz |
|
||||||
|
| Borg `append-only` auf Hetzner | Operator-Entscheidung 2026-06-01: nicht umgesetzt. Der forced-command-Test auf der Storage Box brach Key-Auth und wurde per Passwort-Recovery zurueckgesetzt; Nutzen steht fuer dieses Homelab nicht im Verhaeltnis zum Betriebsrisiko. |
|
||||||
|
|
||||||
## Zuletzt geschlossen
|
## Zuletzt geschlossen
|
||||||
|
|
||||||
|
- DR-Workstation Bare-Metal-Kit abgeschlossen (2026-06-06): WSL2 Ubuntu 24.04, SSH/Git, Borg 1.2.8, DR-Key-Arbeitskopien `~/.ssh/dr-readonly` und `~/.ssh/dr-hetzner`, `~/dr-smoke.sh`. Finaler Operator-Smoke erfolgreich: GitHub HEAD `3a263a4...`, Hetzner Storage Box Repos sichtbar (`backup`, `backup2`, `hetzner_borg_appdata`, `hetzner_borg_appdata_critical`), Borg-Repo `hetzner_borg_appdata_critical` gelesen, Repository ID `5dd9b949...`, encrypted `Yes (repokey)`, `DR-Smoke OK (2026-06-06 10:05:30)`. Borg-Passphrase wurde nur interaktiv eingegeben und nicht gespeichert.
|
||||||
|
- Nextcloud-Restore-Test 2026-06-03 erfolgreich (Tier-2 damit komplett belegt). Drei Laeufe noetig: Lauf 1 schlug an `chmod()` der data-Dir auf shfs fehl (`OC_Util.php:486`), Lauf 2 an fehlender `.ncdata`-Marker-Datei, Lauf 3 sauber durch. Beide Bug-Fixes ins Skript `ops/restore-tests/nextcloud-restore-test.sh` integriert. Endresultat: HTTP 200 auf `/status.php`, `occ status` ok, 126 Tabellen in der DB. Source: `hetzner_borg_appdata_critical`, Archiv `Taegliche-Sicherung-2026-06-03T04:30:41.432`. Report unter `/mnt/user/backups/restore-reports/nextcloud-2026-06-03.md`.
|
||||||
|
- Hetzner Storage Box DR-SSH-Key `dr-hetzner-2026-06-03` (ed25519, Passphrase-frei) angelegt: Pubkey via `install-ssh-key` auf der Storage Box autorisiert, passwortloser Login erfolgreich (Borg-Repos `backup`, `backup2`, `hetzner_borg_appdata`, `hetzner_borg_appdata_critical` sichtbar), Private-Key offline neben KOMODO_*-Notiz und GitHub-Deploy-Key abgelegt, Arbeitsplatz-Kopie geloescht. Damit ist Bare-Metal-Borg-Zugang von der DR-Workstation moeglich, sobald WSL2+Borg installiert sind.
|
||||||
|
- Fix Common Problems Plugin (FCP) 2026-06-03 deinstalliert. Befund: drei `grep -R ... /usr/local/emhttp`-Prozesse aus einem FCP-Daily-Scan hingen seit ~7 Tagen in einem Symlink-Loop (`/usr/local/emhttp/mnt -> /mnt`, gesamte Array). 3 Cores dauerhaft 100 %, IOWAIT bis 55 %, USB-Flash unter Dauer-IO. Plugin via `plugin remove` entfernt, Cron + /tmp-Reste sauber, Load von 14.6 auf 1.08 gefallen. FCP wird bewusst nicht wieder installiert (Begruendung siehe `HOMELAB_ARCHITECTURE_MASTER_V2.md` Sektion 13). Bekannte Risiken decken Scrutiny, Monitoring, Posture-Check und Critical-Events-Watcher bereits ab.
|
||||||
|
- GitHub-Mirror Read-Only Deploy-Key `DR Read-Only 2026-06-03` (ed25519, Passphrase-frei) angelegt: GitHub Repo Settings -> Deploy Keys ohne Write-Access, Smoke `git ls-remote` erfolgreich (HEAD `d947c7f` = master), Private-Key offline neben der KOMODO_*-Notiz abgelegt, Arbeitsplatz-Kopie nach USB-Transfer geloescht. Damit ist der DR-Read-Pfad zum privaten Mirror ohne Operator-Browser-Login moeglich.
|
||||||
|
- KOMODO_*-Notiz offline gesichert (Operator-Bestaetigung 2026-06-03). Quelle bleibt host-seitige `.env` unter `/mnt/user/services/stacks/komodo/.env` bzw. die Drift-Recovery-Kopie unter `/mnt/user/appdata/secrets/_komodo_stack_env_recovery_2026-05-04.env`. Damit ist der Bare-Metal-Komodo-Bootstrap ohne Vaultwarden moeglich. Eintrag in `docs/EXTERNAL_DEPENDENCIES.md` Reviews und Pflichtbestandteil im DR-Workstation-Kit nachgezogen.
|
||||||
|
- DR-Tabletop 2026-06-03 durchgelaufen, Findings in `docs/DR_DRILL_2026-06-03.md` (23 Befunde: 1 CRITICAL, 11 HIGH, 8 MED, 3 LOW). Reine Doku-Fixes in DR.md (Phase 0 Mirror-Klarstellung, neue Phase 4 Stufe 0 Docker-Netze, LE-Staging-Hinweis, Komodo-Stolperfallen, App-DB-Verify in Phase 5) und in `EXTERNAL_DEPENDENCIES.md` (DR-Workstation-Kit, KOMODO_*-Notiz und GitHub-Read-PAT als offene Bootstrap-Bloecke) sind im selben Aenderungsblock erledigt. Operator-Aufgaben (Notiz/PAT/WSL-Setup) wandern als P1 in die offenen Punkte.
|
||||||
|
- Authelia ACL: `borg.kaleschke.info` und `code.kaleschke.info` 2026-06-03 in den `two_factor`-Block der Repo-Baseline aufgenommen. Beide UIs haben effektiv Host-/Backup-Zugriff (Borg-Restore-Scope inkl. `/local/secrets`, code-server mit Workspaces). Wirkung erst nach manuellem Merge in `/mnt/user/appdata/authelia/config/configuration.yml`, `docker restart authelia` und Smoke-Test auf einer der vier 2FA-Domains; `services/authelia-diff.sh` muss `exit 0` liefern. TOTP-Enrollment des Operator-Accounts ist Voraussetzung, sonst Login-Sperre.
|
||||||
|
- Alt-Volumes nach Burn-in freigegeben und reversibel archiviert: Shared PG17, Mealie PG17, Nextcloud PG17 und Immich pgvecto.rs liegen seit 2026-06-02 unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602`; Manifest auf dem Host: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/MANIFEST.txt`. Keine harte Loeschung, keine aktiven Container-Mounts auf die alten Pfade.
|
||||||
|
- Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; Live-Baseline am 2026-06-01: FRITZ!OS `154.08.25`, keine Public-AAAA-Records fuer `*.kaleschke.info`, Host ohne globale Provider-IPv6, WAN `443/tcp` offen und `80/tcp`/`222/tcp` geschlossen.
|
||||||
|
- FRITZ!Box-Servicefenster UI-seitig abgeschlossen: FRITZ!Box-Dienste aus dem Internet sind aus (HTTPS auf FRITZ!Box-UI, FTP/FTPS auf Speichermedien), aktive WAN-Freigabe bleibt nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
|
||||||
|
- FRITZ!Box-Konfig-Backup exportiert und extern/off-system in Vaultwarden abgelegt: `Einstellungen_FRITZ.Box_7590_154.08.25_01.06.26_1318.export`; Kennwort und Datei bleiben ausserhalb des Repos.
|
||||||
|
- Hetzner-Account-Hygiene erledigt: externe Kontakt-/Rechnungs-Mail bestaetigt, Zahlung ok, 2FA mit Google Authenticator aktiv, Recovery Key offline ausgedruckt.
|
||||||
|
- Hetzner Storage Box geprueft: `storage-box-1`, `u565255.your-storagebox.de`, SSH-Port `23`, SSH aktiv, SMB/WebDAV aus, 64,94 GB / 1 TB belegt; Borg-UI-Key und separater Maintenance-Key funktionieren wieder nach Passwort-Recovery. Borg `append-only` ist bewusst nicht umgesetzt.
|
||||||
- Family-View Dashboard ist repo-seitig gebaut: `monitoring/grafana/dashboards/family-status.json` zeigt Family-App-Uptime, Backup-Alter, TLS-Restlaufzeit, Critical-Container und Image-Drift.
|
- Family-View Dashboard ist repo-seitig gebaut: `monitoring/grafana/dashboards/family-status.json` zeigt Family-App-Uptime, Backup-Alter, TLS-Restlaufzeit, Critical-Container und Image-Drift.
|
||||||
- Alt-Volume-Freigabe ist vorbereitet: `ops/maintenance/release-alt-volumes.sh --dry-run` validiert aktive Pfade, Container-Health, Restore-Freshness und gemountete Altpfade; Test am 2026-06-01 fand vier Kandidaten und keine Blocker, Ausfuehrung bleibt wegen Cutoff bis 2026-06-02 gesperrt.
|
|
||||||
- Borg-Nachlauf nach dem 2026-05-31-Sprint ist belegt: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, 101669 Dateien, `rc=0`; Freshness-Check am 2026-06-01: Critical 0, Warnings 0.
|
- Borg-Nachlauf nach dem 2026-05-31-Sprint ist belegt: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, 101669 Dateien, `rc=0`; Freshness-Check am 2026-06-01: Critical 0, Warnings 0.
|
||||||
- H:/ Nearline-Pull am 2026-06-01 repariert und manuell validiert: kuratierte Borg-Dumps Exit 0, Gitea-Bundles Exit 1 (Robocopy-Erfolg mit Kopien), Report `nearline-pull-2026-06-01-082553.md`.
|
- H:/ Nearline-Pull am 2026-06-01 repariert und manuell validiert: kuratierte Borg-Dumps Exit 0, Gitea-Bundles Exit 1 (Robocopy-Erfolg mit Kopien), Report `nearline-pull-2026-06-01-082553.md`.
|
||||||
- Immich-, Paperless-, Gitea- und Vaultwarden-Restore-Pfade sind belegt.
|
- Immich-, Paperless-, Gitea- und Vaultwarden-Restore-Pfade sind belegt.
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Authelia OIDC fuer Apps - Plan & Runbook
|
||||||
|
|
||||||
|
Stand: 2026-06-06. Authelia-Version: **v4.39.20**.
|
||||||
|
|
||||||
|
Ziel: App-uebergreifendes Single-Sign-On ueber Authelia als OpenID-Connect-Provider
|
||||||
|
(`https://auth.kaleschke.info`). Statt pro App eigener Logins meldet man sich einmal
|
||||||
|
bei Authelia an (inkl. 2FA) und wird per OIDC an die App durchgereicht.
|
||||||
|
|
||||||
|
> **Status:** aktives Runbook. Grafana und Mealie sind seit 2026-06-06 live
|
||||||
|
> und per Login-Smoke verifiziert. Der weitere Rollout bleibt additiv: lokale
|
||||||
|
> App-Logins bleiben als Fallback aktiv.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Grundregeln (wichtig)
|
||||||
|
|
||||||
|
- **Secrets gehoeren nie ins Repo.** OIDC-Client-Secrets (Klartext und pbkdf2-Hash)
|
||||||
|
liegen ausschliesslich in der Host-Config `/mnt/user/appdata/authelia/config/configuration.yml`
|
||||||
|
(Hash) und im jeweiligen App-Stack (Klartext, via Komodo Stack-ENV / Secret-Datei),
|
||||||
|
plus optional Vaultwarden. Dieses Dokument enthaelt nur Schema und Variablennamen.
|
||||||
|
- **OIDC-Clients leben host-seitig**, wie der bestehende `beszel`-Client. Die Repo-Baseline
|
||||||
|
`security/authelia/configuration.yml` haelt nur die nicht-geheime Struktur
|
||||||
|
(`access_control` etc.); `services/authelia-diff.sh` vergleicht standardmaessig nur
|
||||||
|
`access_control`, OIDC-Clients auf dem Host loesen also keinen Drift-Alarm aus.
|
||||||
|
- **Issuer/Endpoints** (Authelia OIDC):
|
||||||
|
- Issuer: `https://auth.kaleschke.info`
|
||||||
|
- Authorization: `https://auth.kaleschke.info/api/oidc/authorization`
|
||||||
|
- Token: `https://auth.kaleschke.info/api/oidc/token`
|
||||||
|
- Userinfo: `https://auth.kaleschke.info/api/oidc/userinfo`
|
||||||
|
- JWKS: `https://auth.kaleschke.info/jwks.json`
|
||||||
|
- Discovery: `https://auth.kaleschke.info/.well-known/openid-configuration`
|
||||||
|
- **PKCE an, wo moeglich** (`require_pkce: true`, `S256`), wie beim Beszel-Client.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client-Schema (Authelia v4.39, gespiegelt vom bestehenden `beszel`-Client)
|
||||||
|
|
||||||
|
Pro App ein Block unter `identity_providers.oidc.clients` in der **Host-Config**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
identity_providers:
|
||||||
|
oidc:
|
||||||
|
clients:
|
||||||
|
- client_id: '<app>'
|
||||||
|
client_name: '<App-Name>'
|
||||||
|
client_secret: '<pbkdf2-sha512-Hash - NUR auf dem Host>'
|
||||||
|
public: false
|
||||||
|
authorization_policy: 'two_factor' # admin-Apps: two_factor; Familien-Apps: s.u.
|
||||||
|
require_pkce: true
|
||||||
|
pkce_challenge_method: 'S256'
|
||||||
|
redirect_uris:
|
||||||
|
- 'https://<app>.kaleschke.info/<oidc-callback-pfad>'
|
||||||
|
scopes:
|
||||||
|
- 'openid'
|
||||||
|
- 'profile'
|
||||||
|
- 'email'
|
||||||
|
- 'groups'
|
||||||
|
response_types:
|
||||||
|
- 'code'
|
||||||
|
grant_types:
|
||||||
|
- 'authorization_code'
|
||||||
|
token_endpoint_auth_method: 'client_secret_basic'
|
||||||
|
userinfo_signed_response_alg: 'none'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client-Secret erzeugen (auf dem Host)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec authelia authelia crypto hash generate pbkdf2 \
|
||||||
|
--variant sha512 --random --random.length 72 --random.charset rfc3986
|
||||||
|
```
|
||||||
|
|
||||||
|
- Ausgabe: **Random Password** (Klartext) + **Digest** (pbkdf2-Hash).
|
||||||
|
- **Hash** -> Host-Config `client_secret`.
|
||||||
|
- **Klartext** -> App-Stack (Komodo Stack-ENV/Secret) + optional Vaultwarden.
|
||||||
|
- Klartext **nicht** ins Repo, nicht in Logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reihenfolge / Rollout
|
||||||
|
|
||||||
|
| Stufe | App | Domain | OIDC-Support | Policy | Risiko | Begruendung |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **1 (Proof) ERLEDIGT 2026-06-06** | Grafana (monitoring) | `monitoring.kaleschke.info` | nativ (`generic_oauth`) | `two_factor` | niedrig | **Live + Login verifiziert.** Authelia-Client `grafana` (host), Secret als Datei `/mnt/user/appdata/secrets/grafana_oidc_client_secret` via `__FILE`, ForwardAuth-Middleware durch OIDC ersetzt, lokaler Admin bleibt Fallback |
|
||||||
|
| 2 | Immich | `immich.kaleschke.info` | nativ (Admin-UI/Config-File) | s. u. (Familie) | mittel | **GEPARKT bis Onboarding (Entscheidung 2026-06-06):** nur `micha` hat Authelia-Account, Familien-SSO-Nutzen entsteht erst mit Familien-Accounts; Immich ist mobil-lastig (hoechste Stoeranfaelligkeit) und braucht UI/Config-File. Erst nach Onboarding gezielt. Runbook bereit. |
|
||||||
|
| 3 | Nextcloud | `cloud.kaleschke.info` | App `user_oidc` (+occ) | s. u. | mittel | **GEPARKT bis Onboarding (Entscheidung 2026-06-06):** wie Immich; braucht `user_oidc`-App-Install + `occ`. Lokaler Login bleibt. Erst nach Onboarding. Runbook bereit. |
|
||||||
|
| **4 ERLEDIGT 2026-06-06** | Mealie | `mealie.kaleschke.info` | nativ | `one_factor` | niedrig | **Live + Login verifiziert.** OIDC-Env additiv (lokaler Login bleibt), Secret als Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}`, `extra_hosts` noetig (s. Gotchas) |
|
||||||
|
| 5 | Paperless-ngx | `paperless.kaleschke.info` | `django-allauth` (Umgebungsvariablen) | `two_factor` | mittel | dokumentenlastig, Operator-nah |
|
||||||
|
|
||||||
|
**Nicht OIDC:** Vaultwarden hat kein Standard-Endnutzer-OIDC (SSO ist Enterprise/Bitwarden-Feature) -> bleibt eigener Login. ntfy bleibt wie gehabt.
|
||||||
|
|
||||||
|
### Policy Familien-Apps
|
||||||
|
|
||||||
|
- Admin-Apps (Grafana, Paperless): `authorization_policy: two_factor`.
|
||||||
|
- Familien-Apps (Immich, Nextcloud, Mealie): Start mit `one_factor` und lokalen
|
||||||
|
App-Logins als Fallback. 2FA fuer Familie erst spaeter, sobald TOTP-Enrollment
|
||||||
|
pro Person eingerichtet ist; sonst entsteht unnoetiges Lockout-Risiko.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stufe 1 konkret: Grafana (empfohlener Erststart)
|
||||||
|
|
||||||
|
### A) Authelia (Host) - Client anlegen
|
||||||
|
1. Secret erzeugen (Befehl oben). Klartext + Hash notieren.
|
||||||
|
2. In `/mnt/user/appdata/authelia/config/configuration.yml` unter
|
||||||
|
`identity_providers.oidc.clients` neuen Block einfuegen:
|
||||||
|
```yaml
|
||||||
|
- client_id: 'grafana'
|
||||||
|
client_name: 'Grafana'
|
||||||
|
client_secret: '<HASH>'
|
||||||
|
public: false
|
||||||
|
authorization_policy: 'two_factor'
|
||||||
|
require_pkce: true
|
||||||
|
pkce_challenge_method: 'S256'
|
||||||
|
redirect_uris:
|
||||||
|
- 'https://monitoring.kaleschke.info/login/generic_oauth'
|
||||||
|
scopes: ['openid', 'profile', 'email', 'groups']
|
||||||
|
response_types: ['code']
|
||||||
|
grant_types: ['authorization_code']
|
||||||
|
token_endpoint_auth_method: 'client_secret_basic'
|
||||||
|
userinfo_signed_response_alg: 'none'
|
||||||
|
```
|
||||||
|
3. `docker restart authelia`, Health + Log pruefen (`Startup complete`, keine Fehler).
|
||||||
|
|
||||||
|
### B) Grafana (Komodo Stack-ENV) - generic_oauth
|
||||||
|
Im `monitoring`-Stack (Grafana) setzen (Klartext-Secret aus Schritt A):
|
||||||
|
```
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ENABLED=true
|
||||||
|
GF_AUTH_GENERIC_OAUTH_NAME=Authelia
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_ID=grafana
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=<KLARTEXT-SECRET>
|
||||||
|
GF_AUTH_GENERIC_OAUTH_SCOPES=openid profile email groups
|
||||||
|
GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.kaleschke.info/api/oidc/authorization
|
||||||
|
GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.kaleschke.info/api/oidc/token
|
||||||
|
GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.kaleschke.info/api/oidc/userinfo
|
||||||
|
GF_AUTH_GENERIC_OAUTH_USE_PKCE=true
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true
|
||||||
|
# optional Rollen-Mapping ueber groups:
|
||||||
|
# GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(groups[*], 'admins') && 'Admin' || 'Viewer'
|
||||||
|
```
|
||||||
|
- `GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET` als Stack-ENV-only (kein `_FILE`-Support) -> in
|
||||||
|
`docs/SECRETS_MAP.md` als `grafana_oidc_client_secret` (Stack-ENV) nachziehen.
|
||||||
|
|
||||||
|
### C) Test + Rollback
|
||||||
|
- Test: `monitoring.kaleschke.info` -> "Sign in with Authelia" -> Authelia-Login (2FA) -> zurueck in Grafana, eingeloggt.
|
||||||
|
- **Fallback bleibt:** lokaler Grafana-Admin-Login (`/login`) ist weiter aktiv -> kein Lockout.
|
||||||
|
- Rollback: `GF_AUTH_GENERIC_OAUTH_ENABLED=false` (Grafana redeploy) und/oder Client-Block in Authelia entfernen + `docker restart authelia`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Doku-Nachzug bei jedem neuen Client
|
||||||
|
|
||||||
|
- `docs/SECRETS_MAP.md`: pro App `<app>_oidc_client_secret` (Stack-ENV) + Hinweis "Hash in Authelia-Host-Config".
|
||||||
|
- `docs/SERVICE_CATALOG.md`: App-Zeile um "OIDC via Authelia" ergaenzen.
|
||||||
|
- Dieses Dokument: Rollout-Tabelle abhaken.
|
||||||
|
- `docs/MASTER_TODO.md`: Fortschritt im OIDC-Punkt nachziehen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas (aus dem realen Rollout 2026-06-06)
|
||||||
|
|
||||||
|
- **`extra_hosts` ist Pflicht fuer App-Container, die selbst zu Authelia connecten**
|
||||||
|
(OIDC-Discovery/Token sind Server-zu-Server): Der App-Container loest
|
||||||
|
`auth.kaleschke.info` per Docker-DNS oft nicht auf -> `httpx.ConnectTimeout` /
|
||||||
|
500 beim OAuth-Start. Fix wie Komodo:
|
||||||
|
```yaml
|
||||||
|
extra_hosts:
|
||||||
|
- "auth.kaleschke.info:192.168.178.58"
|
||||||
|
```
|
||||||
|
Cert validiert weiter (SNI/Hostname bleibt gleich, nur die IP wird gemappt).
|
||||||
|
Gilt fuer Mealie (bestaetigt) und sehr wahrscheinlich Paperless/Immich/Nextcloud.
|
||||||
|
- **Additiv heisst additiv:** OIDC als zusaetzlichen Login aktivieren, lokalen
|
||||||
|
Login NICHT abschalten, `AUTO_REDIRECT`/Force-OIDC aus -> kein Lockout.
|
||||||
|
- **Account-Linking per E-Mail:** Apps verknuepfen den OIDC-User i. d. R. per
|
||||||
|
E-Mail-Claim. Stimmt die Authelia-E-Mail mit dem App-Account, wird verknuepft;
|
||||||
|
sonst legt die App (bei aktivem Signup) einen neuen User an.
|
||||||
|
- **Secret-Mechanik je App verschieden:** Grafana `__FILE` (Docker-Secret),
|
||||||
|
Mealie Stack-ENV `${...}`. Hash immer in der Authelia-Host-Config, Klartext nie ins Repo.
|
||||||
|
|
||||||
|
## Spaetere Feinschliffe vor breitem Rollout
|
||||||
|
|
||||||
|
1. Gruppen/Rollen-Mapping: braucht es Authelia-Gruppen (z. B. `admins`, `family`) fuer
|
||||||
|
App-Rollen (Grafana Admin/Viewer, Nextcloud-Gruppen)? Wenn ja, in der Authelia
|
||||||
|
User-Datenbank Gruppen pflegen.
|
||||||
|
2. Familien-2FA spaeter neu bewerten, nachdem echte Familien-Accounts in Authelia
|
||||||
|
angelegt und TOTP pro Person verstanden ist.
|
||||||
+103
-11
@@ -62,7 +62,8 @@ Diese Punkte sollten **vor** einem echten Ausfall geklaert sein:
|
|||||||
|
|
||||||
| Thema | Sollzustand |
|
| Thema | Sollzustand |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Repo-Zugang ausserhalb von Gitea | privater GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` und lokaler aktueller Clone vorhanden |
|
| Repo-Zugang ausserhalb von Gitea | privater GitHub-Push-Mirror `michaelkaleschke-spec/homelab-infra` und lokaler aktueller Clone vorhanden; fuer Bare-Metal-DR zusaetzlich Read-Only-PAT/Deploy-Key offline im DR-Kit |
|
||||||
|
| Operator-DR-Workstation | Gaming-PC mit aktuellem Repo-Clone, WSL2 + Borg-Client, SSH-Key fuer Hetzner Storage Box, Offline-Kopie Borg-Passphrase; Bestandteile siehe `docs/EXTERNAL_DEPENDENCIES.md` Abschnitt "DR-Workstation Bare-Metal-Kit" |
|
||||||
| Unraid USB-/Flash-Backup | `unraid-flash-config.tar.gz` wird vor Borg unter `/mnt/user/backups/borg/dumps/latest` erzeugt und nach Hetzner/Borg gesichert; Unraid-Connect-Cloud-Backup optional zusaetzlich |
|
| Unraid USB-/Flash-Backup | `unraid-flash-config.tar.gz` wird vor Borg unter `/mnt/user/backups/borg/dumps/latest` erzeugt und nach Hetzner/Borg gesichert; Unraid-Connect-Cloud-Backup optional zusaetzlich |
|
||||||
| Borg-Ziel | nicht nur lokal auf demselben Ausfallpfad |
|
| Borg-Ziel | nicht nur lokal auf demselben Ausfallpfad |
|
||||||
| Borg-Passphrase | Host-Secret-Datei vorhanden und fuer Borg-Zugriff verifiziert; externe Offline-Hinterlegung vom Operator am 2026-05-26 bestaetigt |
|
| Borg-Passphrase | Host-Secret-Datei vorhanden und fuer Borg-Zugriff verifiziert; externe Offline-Hinterlegung vom Operator am 2026-05-26 bestaetigt |
|
||||||
@@ -87,9 +88,15 @@ Deshalb gilt:
|
|||||||
|
|
||||||
Verfuegbare Wege:
|
Verfuegbare Wege:
|
||||||
|
|
||||||
- externer Push-Mirror: `https://github.com/michaelkaleschke-spec/homelab-infra`
|
- externer Push-Mirror: `https://github.com/michaelkaleschke-spec/homelab-infra` (privat, Read-PAT/Deploy-Key noetig — siehe `docs/EXTERNAL_DEPENDENCIES.md` Abschnitt "DR-Workstation Bare-Metal-Kit")
|
||||||
- lokaler Bare-Clone auf dem PC
|
- lokaler Bare-Clone auf der Operator-DR-Workstation (Standardweg)
|
||||||
- normaler lokaler Arbeits-Clone auf dem PC
|
- normaler lokaler Arbeits-Clone auf der Operator-DR-Workstation
|
||||||
|
|
||||||
|
Operativer Pfad fuer den Repo auf den frisch installierten Unraid-Host:
|
||||||
|
|
||||||
|
1. Operator-DR-Workstation holt den aktuellen Clone (lokaler Stand oder per `git clone` aus dem GitHub-Mirror mit dem offline gesicherten Read-PAT/Deploy-Key).
|
||||||
|
2. Kopie via USB, SMB oder `rsync ueber SSH/Tailscale` nach `/mnt/user/services/homelab-infra/` auf dem Unraid-Host.
|
||||||
|
3. Stand pruefen: `git -C /mnt/user/services/homelab-infra log --oneline -1` zeigt einen plausibel aktuellen Commit.
|
||||||
|
|
||||||
Wenn **weder GitHub-Mirror noch lokaler Repo-Clone** verfuegbar sind, ist `services/gitea/data` selbst ein kritischer Restore-Pfad.
|
Wenn **weder GitHub-Mirror noch lokaler Repo-Clone** verfuegbar sind, ist `services/gitea/data` selbst ein kritischer Restore-Pfad.
|
||||||
|
|
||||||
@@ -148,6 +155,12 @@ Erwartete Basis unter `/mnt/user/appdata/secrets/`:
|
|||||||
- `redis_password.txt`
|
- `redis_password.txt`
|
||||||
- `borg_repo_passphrase.txt`
|
- `borg_repo_passphrase.txt`
|
||||||
- `vaultwarden_admin_token.txt`
|
- `vaultwarden_admin_token.txt`
|
||||||
|
- `homelab_smtp_password.txt`
|
||||||
|
- `n8n_encryption_key.txt`
|
||||||
|
- `monitoring_grafana_admin_password.txt`
|
||||||
|
- `monitoring_grafana_influxdb_token.txt`
|
||||||
|
- `influxdb3_admin_token.json`
|
||||||
|
- `filebrowser_admin_password.txt`
|
||||||
- `hermes_runner_id_ed25519`
|
- `hermes_runner_id_ed25519`
|
||||||
|
|
||||||
Weitere relevante Secret-Pfade:
|
Weitere relevante Secret-Pfade:
|
||||||
@@ -241,17 +254,52 @@ Besonders kritisch:
|
|||||||
|
|
||||||
**Nicht blind alles extrahieren**, wenn nur einzelne Pfade oder Dienste betroffen sind.
|
**Nicht blind alles extrahieren**, wenn nur einzelne Pfade oder Dienste betroffen sind.
|
||||||
|
|
||||||
|
### 7.3 Borg-Extract ohne `borg-ui`-Container
|
||||||
|
|
||||||
|
Im Bare-Metal-Fall ist `borg-ui` selbst kalt. Der initiale Borg-Extract laeuft deshalb nicht ueber den Container, sondern wahlweise ueber:
|
||||||
|
|
||||||
|
1. **Operator-DR-Workstation** (Standardweg) - WSL2 + `borgbackup` extrahieren gezielt nach `/mnt/user/backups/restore-lab/...` oder per `rsync`/SMB auf den Unraid-Host.
|
||||||
|
2. **Native Docker-Variante auf Unraid** - `docker run --rm -e BORG_PASSPHRASE=... -v /mnt/user/backups/restore-lab:/restore -v ~/.ssh:/root/.ssh:ro borgbackup/borg:1.4 ...`.
|
||||||
|
|
||||||
|
Erst nach Stufe 5 Phase 4 ist `borg-ui` produktiv und uebernimmt den weiteren Betrieb. Die Borg-Passphrase wird interaktiv aus der Offline-Sicherung eingegeben, nicht in Skripte/Tickets kopiert.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Phase 4 - Bootstrap-Reihenfolge der Stacks
|
## 8. Phase 4 - Bootstrap-Reihenfolge der Stacks
|
||||||
|
|
||||||
**Nie alle Stacks gleichzeitig starten.**
|
**Nie alle Stacks gleichzeitig starten.**
|
||||||
|
|
||||||
|
### Stufe 0 - Docker-Grundlage
|
||||||
|
|
||||||
|
Vor dem ersten `docker compose up` muss sichergestellt sein:
|
||||||
|
|
||||||
|
1. `docker info` antwortet ohne Fehler.
|
||||||
|
2. Externe Docker-Netze existieren. Wenn nicht vorhanden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create --driver bridge frontend_net
|
||||||
|
docker network create --driver bridge --internal backend_net
|
||||||
|
docker network create --driver bridge monitoring_net
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Pfad `/mnt/user/appdata/traefik/dynamic/` enthaelt `middlewares.yml`, `tls.yml`, `dashboards.yml` (Sonderregel siehe Sektion 10). Ohne diese Dateien startet Traefik ohne Middleware-Definitionen und alle Authelia-geschuetzten Routen brechen still.
|
||||||
|
|
||||||
|
Erfolgskriterium: `docker network ls` zeigt `frontend_net`, `backend_net`, `monitoring_net`; Traefik-`dynamic/`-Dateien sind vorhanden und valide.
|
||||||
|
|
||||||
### Stufe 1 - Netz und Zugang
|
### Stufe 1 - Netz und Zugang
|
||||||
|
|
||||||
1. `traefik/`
|
1. `traefik/`
|
||||||
2. `host-services/Adguard/`
|
2. `host-services/Adguard/`
|
||||||
3. `host-services/tailscale/`
|
|
||||||
|
> **Tailscale-Hinweis:** Tailscale laeuft als **natives Unraid-Plugin**
|
||||||
|
> (`tailscale.plg`, Interface `tailscale1`, State `/boot/config/plugins/tailscale/state`,
|
||||||
|
> im Flash-Backup gesichert) und ist der Subnet-Router fuer `192.168.178.0/24`.
|
||||||
|
> Es ist **kein** Compose-/Komodo-Stack mehr und kommt mit dem Host hoch — daher
|
||||||
|
> nicht in dieser Bootstrap-Liste. Der frueher hier gelistete Docker-Stack
|
||||||
|
> `host-services/tailscale/` (userspace-only, redundant) wurde am 2026-06-06
|
||||||
|
> entfernt (siehe `docs/NETWORK_INVENTORY.md`).
|
||||||
|
|
||||||
|
**LE-Rate-Limit-Vorsicht:** Wenn `/mnt/user/appdata/traefik/letsencrypt/acme.json` verloren oder unklar ist, zuerst gegen Let's Encrypt Staging ausstellen lassen (`--certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory`). Erst nach gruenem Smoke wieder auf Production-CA. Hintergrund: 50 Zertifikate pro Domain pro Woche reicht bei einem hektischen Wiederanlauf nicht, wenn man die Sub-Domains mehrfach hochzieht.
|
||||||
|
|
||||||
Ziel:
|
Ziel:
|
||||||
|
|
||||||
@@ -290,6 +338,13 @@ Ziel:
|
|||||||
- Periphery verbindet sich wieder
|
- Periphery verbindet sich wieder
|
||||||
- Stacks koennen wieder aus Git konsumiert werden
|
- Stacks koennen wieder aus Git konsumiert werden
|
||||||
|
|
||||||
|
**Wichtige Stolperfallen in Stufe 3:**
|
||||||
|
|
||||||
|
- **KOMODO_*-Werte sind nicht aus dem eigenen Mongo-Dump rekonstruierbar.** Pflichtquelle im Bare-Metal: offline gesicherte Operator-Notiz (Status 2026-06-03: noch nicht angelegt, siehe `docs/EXTERNAL_DEPENDENCIES.md` und Audit-Restliste). Vaultwarden ist erst in Stufe 4 verfuegbar.
|
||||||
|
- **Mongo-Datadir und `komodo_mongo_password.txt` muessen aus demselben Snapshot stammen.** Bei Mismatch akzeptiert Mongo den Login nicht und der Stack startet nicht. Auswege: entweder die zur Datadir passende Secret-Datei aus dem gleichen Borg-Stand restaurieren, oder Datadir leeren, neu initialisieren und Daten via `mongorestore --archive --gzip` aus `komodo-mongo.archive.gz` einspielen (Drill belegt 2026-06-03).
|
||||||
|
- **`extra_hosts: git.kaleschke.info:192.168.178.58`** in `ops/komodo/docker-compose.yml` ist hardgecodet. Bei geaenderter Host-LAN-IP auf der Recovery-Hardware den Wert vor `compose up` anpassen, sonst kann Komodo-Core das interne Gitea nicht erreichen.
|
||||||
|
- **Stack-ENV-Werte fuer Apps in Stufe 4** (Paperless/Immich/Mailarchiver/Speedtest) sind in Stufe 3 noch leer. Zwei Wege: (a) optionaler `mongorestore` aus `komodo-mongo.archive.gz` direkt nach Komodo-Start, dann sind alle Stack-ENVs zurueck; (b) Werte manuell in der Komodo-UI eintragen, sobald Vaultwarden in Stufe 4 verfuegbar ist (was Paperless/Immich/Mailarchiver hinter Vaultwarden zwingt, nicht parallel).
|
||||||
|
|
||||||
### Stufe 4 - Kritische Anwendungen
|
### Stufe 4 - Kritische Anwendungen
|
||||||
|
|
||||||
9. `security/vaultwarden/`
|
9. `security/vaultwarden/`
|
||||||
@@ -342,6 +397,7 @@ Ziel:
|
|||||||
- Mealie startet
|
- Mealie startet
|
||||||
- Mail-Archiver startet
|
- Mail-Archiver startet
|
||||||
- Nextcloud startet und sieht Dateien
|
- Nextcloud startet und sieht Dateien
|
||||||
|
- Pro App: `docker logs <container>` zeigt keine `password authentication failed`-, `FATAL: role does not exist`- oder `Connection refused`-Eintraege (verifiziert, dass Stack-ENV-Werte und DB-Rollen passen)
|
||||||
|
|
||||||
### 9.4 Backup-/Beobachtungsebene
|
### 9.4 Backup-/Beobachtungsebene
|
||||||
|
|
||||||
@@ -382,7 +438,7 @@ Vor dem Start muessen vorhanden sein:
|
|||||||
- `/mnt/user/appdata/secrets/authelia_smtp_password.txt`
|
- `/mnt/user/appdata/secrets/authelia_smtp_password.txt`
|
||||||
- SMTP-Zugang fuer `michideheld@gmx.de`
|
- SMTP-Zugang fuer `michideheld@gmx.de`
|
||||||
|
|
||||||
Beim Smoke-Test muss `authelia validate-config` erfolgreich sein; der SMTP-Startup-Check darf den Start nicht blockieren.
|
Beim Smoke-Test muss `authelia config validate` erfolgreich sein; der SMTP-Startup-Check darf den Start nicht blockieren.
|
||||||
|
|
||||||
### `nextcloud`
|
### `nextcloud`
|
||||||
|
|
||||||
@@ -440,11 +496,11 @@ Aktive Datenpfade:
|
|||||||
- Mealie PostgreSQL: `/mnt/user/appdata/mealie/postgres18`
|
- Mealie PostgreSQL: `/mnt/user/appdata/mealie/postgres18`
|
||||||
- Nextcloud PostgreSQL: `/mnt/user/appdata/nextcloud/postgres18`
|
- Nextcloud PostgreSQL: `/mnt/user/appdata/nextcloud/postgres18`
|
||||||
|
|
||||||
Rollback-Altstaende, bis zur separaten Loeschfreigabe nicht entfernen:
|
Rollback-Altstaende wurden nach Burn-in am 2026-06-02 reversibel archiviert:
|
||||||
|
|
||||||
- Shared PostgreSQL 17: `/mnt/user/appdata/postgresql17`
|
- Shared PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`
|
||||||
- Mealie PostgreSQL 17: `/mnt/user/appdata/mealie/postgres`
|
- Mealie PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`
|
||||||
- Nextcloud PostgreSQL 17: `/mnt/user/appdata/nextcloud/postgres`
|
- Nextcloud PostgreSQL 17: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`
|
||||||
|
|
||||||
Restore-Reihenfolge fuer den Shared-Cluster:
|
Restore-Reihenfolge fuer den Shared-Cluster:
|
||||||
|
|
||||||
@@ -454,7 +510,7 @@ Restore-Reihenfolge fuer den Shared-Cluster:
|
|||||||
4. Datenbanken anlegen und Custom-Format-Dumps mit `pg_restore` einspielen.
|
4. Datenbanken anlegen und Custom-Format-Dumps mit `pg_restore` einspielen.
|
||||||
5. Restore-Logs auf echte `ERROR`, `FATAL` und `PANIC` pruefen.
|
5. Restore-Logs auf echte `ERROR`, `FATAL` und `PANIC` pruefen.
|
||||||
|
|
||||||
Immich ist bewusst nicht Teil dieses PostgreSQL-18-Laufs: Die produktive DB bleibt auf PostgreSQL 14 und nutzt das Immich-Postgres-Image mit VectorChord/pgvector. VectorChord-Backups brauchen zum Restore ein Image mit VectorChord; der alte pgvecto.rs-Datenpfad `/mnt/user/appdata/immich_postgres` bleibt bis zur separaten Loeschfreigabe als Rollback-Altstand erhalten.
|
Immich ist bewusst nicht Teil dieses PostgreSQL-18-Laufs: Die produktive DB bleibt auf PostgreSQL 14 und nutzt das Immich-Postgres-Image mit VectorChord/pgvector. VectorChord-Backups brauchen zum Restore ein Image mit VectorChord; der alte pgvecto.rs-Datenpfad ist als Rollback-Altstand unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs` archiviert.
|
||||||
|
|
||||||
### Hermes Agent
|
### Hermes Agent
|
||||||
|
|
||||||
@@ -473,6 +529,40 @@ Smoke-Test: `hermes-gateway` healthcheck ist gruen, `hermes.kaleschke.info` leit
|
|||||||
|
|
||||||
`Micha/homelab-infra` wird als privater GitHub-Push-Mirror gespiegelt. Dieser Mirror ist der bevorzugte Repo-Bootstrap, falls Gitea selbst nach einem Ausfall noch nicht laeuft. Wenn weder GitHub-Mirror noch lokaler Clone verfuegbar sind, ist `services/gitea/data` selbst Teil des kritischen Wiederanlaufs.
|
`Micha/homelab-infra` wird als privater GitHub-Push-Mirror gespiegelt. Dieser Mirror ist der bevorzugte Repo-Bootstrap, falls Gitea selbst nach einem Ausfall noch nicht laeuft. Wenn weder GitHub-Mirror noch lokaler Clone verfuegbar sind, ist `services/gitea/data` selbst Teil des kritischen Wiederanlaufs.
|
||||||
|
|
||||||
|
### Windows-Workstation `baerchen`
|
||||||
|
|
||||||
|
`baerchen` ist die Operator-Workstation und haelt den lokalen Clone unter
|
||||||
|
`G:\Gitea_Clone\homelab-infra`. Fuer einen schnellen Windows-Bare-Metal-Restore
|
||||||
|
existiert ein Veeam-Agent-Image-Workflow.
|
||||||
|
|
||||||
|
Wichtige Pfade und Artefakte:
|
||||||
|
|
||||||
|
- Runbook: `ops/windows-reinstall/docs/windows-image-backup-baseline.md`
|
||||||
|
- Backup-Ziel: `\\kallilabcore\backups\windows-images\baerchen`
|
||||||
|
- Host-Pfad: `/mnt/user/backups/windows-images/baerchen/`
|
||||||
|
- Recovery-Medium: USB-Stick `VEEAMRE`, beschriftet
|
||||||
|
`baerchen Veeam Recovery - 2026-06-05`
|
||||||
|
- Veeam Job: `baerchen-c-image`
|
||||||
|
- Veeam Storage Encryption: erster Full-Lauf 2026-06-05 laut Job-Log
|
||||||
|
unverschluesselt (`StorageEncryptionEnabled=False`); falls spaeter aktiviert,
|
||||||
|
Passwort in Vaultwarden Secure Note `Veeam baerchen backup encryption password`
|
||||||
|
sichern
|
||||||
|
|
||||||
|
Restore-Kurzpfad:
|
||||||
|
|
||||||
|
1. Von `VEEAMRE` booten.
|
||||||
|
2. SMB-Ziel `\\kallilabcore\backups\windows-images\baerchen` oeffnen.
|
||||||
|
3. Mit bestehendem SMB-User `micha` authentifizieren.
|
||||||
|
4. Restore Point auswaehlen.
|
||||||
|
5. Falls der Restore Point verschluesselt ist: Veeam-Encryption-Passwort aus
|
||||||
|
Vaultwarden eingeben.
|
||||||
|
6. Bare-Metal-Restore nur auf die Windows-Systemdisk ausfuehren.
|
||||||
|
|
||||||
|
BitLocker ist am 2026-06-05 bewusst noch nicht aktiv. Falls BitLocker spaeter
|
||||||
|
aktiviert wird, muss der Recovery-Key vor dem naechsten Restore-Drill in
|
||||||
|
Vaultwarden, unter `D:\30_Finanzen\BitLocker-RecoveryKey-baerchen-<DATUM>.txt`
|
||||||
|
und physisch ausserhalb des Rechners abgelegt sein.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Offene Vorbereitungs-To-dos
|
## 11. Offene Vorbereitungs-To-dos
|
||||||
@@ -482,6 +572,8 @@ Smoke-Test: `hermes-gateway` healthcheck ist gruen, `hermes.kaleschke.info` leit
|
|||||||
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
|
- Komodo Stack-ENV-Werte zentral ausserhalb von Komodo dokumentieren
|
||||||
- regelmaessige automatisierte Restore-Smoke-Tests fuer Vaultwarden, Gitea und Paperless etablieren
|
- regelmaessige automatisierte Restore-Smoke-Tests fuer Vaultwarden, Gitea und Paperless etablieren
|
||||||
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
|
- `komodo-mongo`-Dump nach Major-Upgrades gezielt kontrollieren
|
||||||
|
- `baerchen` Recovery-USB-Boot-/SMB-Test nach erfolgreichem erstem Full-Lauf
|
||||||
|
verifizieren
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,504 @@
|
|||||||
|
# DR Tabletop Drill - 2026-06-03
|
||||||
|
|
||||||
|
Trockenlauf gegen `docs/DISASTER_RECOVERY.md` Phase 0 bis 5 plus referenzierte
|
||||||
|
Runbooks (`SERVICES_RECOVERY.md`, `RESTORE_MATRIX.md`, `SECRETS_MAP.md`,
|
||||||
|
`RESTORE_HANDBOOK.md`, `EXTERNAL_DEPENDENCIES.md`).
|
||||||
|
|
||||||
|
Szenario: Bare-Metal-Ausfall. Unraid-Host und alle lokalen Festplatten sind
|
||||||
|
weg. Operator hat: Laptop, Hetzner-Account, Vaultwarden-Export, Repo-Doku.
|
||||||
|
Soft-Recovery (Host laeuft, Appdata futsch) ist eine Teilmenge dieser
|
||||||
|
Findings.
|
||||||
|
|
||||||
|
Methode: kalter Lesetest. Kein Container gestartet, keine Skripte
|
||||||
|
ausgefuehrt. Jeder Befund ist mit Repo-Datei und Zeile belegt. Spekulative
|
||||||
|
"vielleicht unklar"-Befunde sind weggelassen.
|
||||||
|
|
||||||
|
Severity:
|
||||||
|
|
||||||
|
- **CRITICAL** - blockiert Wiederanlauf, ohne Workaround nicht loesbar
|
||||||
|
- **HIGH** - blockiert eine Phase, Workaround moeglich aber undokumentiert
|
||||||
|
- **MED** - kostet Zeit oder fuehrt zu vermeidbarem Fehler
|
||||||
|
- **LOW** - Konsistenz / Stil
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
| ID | Phase | Severity | Thema |
|
||||||
|
|---|---|---|---|
|
||||||
|
| P0-1 | 0 | HIGH | Brueckenpfad Windows-Clone -> frischer Unraid-Host fehlt |
|
||||||
|
| P0-2 | 0 | HIGH | GitHub-Mirror-Zugang im DR ist nicht eigenstaendig dokumentiert |
|
||||||
|
| P1-1 | 1 | CRITICAL | Unraid-Flash-Restore: kein dokumentierter Extract-Pfad ohne laufenden Host |
|
||||||
|
| P1-2 | 1 | MED | Unraid-OS-Flash-Restore-Test laut Matrix nie real getestet |
|
||||||
|
| P2-1 | 2 | HIGH | KOMODO_* externe Operator-Notiz ist Pflichtquelle, Existenz nicht verifizierbar |
|
||||||
|
| P2-2 | 2 | HIGH | DR.md Phase 4 vs. SERVICES_RECOVERY.md Bootstrap-Reihenfolge widerspruechlich |
|
||||||
|
| P2-3 | 2 | MED | `homelab_smtp_password.txt` fehlt in DR.md Phase 2.6.1 |
|
||||||
|
| P2-4 | 2 | MED | `n8n_encryption_key.txt` fehlt in DR.md Phase 2.6.1 |
|
||||||
|
| P2-5 | 2 | LOW | Monitoring-/Filebrowser-Secrets fehlen in DR.md Phase 2.6.1 |
|
||||||
|
| P3-1 | 3 | HIGH | Borg-Client ohne `borg-ui`-Container ist nicht dokumentiert |
|
||||||
|
| P3-2 | 3 | HIGH | Borg-Passphrase-Bootstrap aus Offline-Sicherung nicht als expliziter Schritt |
|
||||||
|
| P3-3 | 3 | MED | Hetzner-Maintenance-Key aus Vaultwarden ist Henne-Ei im Bare-Metal |
|
||||||
|
| P4-1 | 4 | HIGH | Externe Docker-Netze in DR.md Phase 4 Stufe 1 nicht erwaehnt |
|
||||||
|
| P4-2 | 4 | HIGH | Cloudflare-LE-Rate-Limit-Risiko bei verlorenem `letsencrypt`-State |
|
||||||
|
| P4-3 | 4 | MED | `traefik/dynamic/*` als Phase-4-Pre-Check fehlt in der Reihenfolge |
|
||||||
|
| P4-4 | 4 | HIGH | Authelia "frische Postgres ohne Dump"-Pfad nicht beschrieben |
|
||||||
|
| P4-5 | 4 | LOW | Gitea in Stufe 2 hinter Postgres ist faktisch nicht noetig (SQLite) |
|
||||||
|
| P4-6 | 4 | HIGH | Komodo-Mongo Passwort-Lockout-Risiko bei restauriertem Datadir |
|
||||||
|
| P4-7 | 4 | MED | Komodo `extra_hosts` mit hardgecodeter LAN-IP bricht bei IP-Wechsel |
|
||||||
|
| P4-8 | 4 | HIGH | Stack-ENV-Wiederherstellung in Komodo praktisch nur manueller UI-Eintrag |
|
||||||
|
| P5-1 | 5 | LOW | Smoke-Tests in Phase 5 weniger streng als RESTORE_MATRIX |
|
||||||
|
| P5-2 | 5 | MED | Kein Verifikationspunkt fuer App-zu-DB-Verbindung nach Stack-ENV-Restore |
|
||||||
|
| X-1 | uebergreifend | HIGH | Nextcloud-Restore-Skript ist da, aber noch nie real ausgefuehrt |
|
||||||
|
|
||||||
|
## Phase 0 - Repo-Zugang
|
||||||
|
|
||||||
|
### P0-1 (HIGH) - Brueckenpfad Windows-Clone -> frischer Unraid fehlt
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:88-93` listet als Repo-Quellen: GitHub-Mirror,
|
||||||
|
lokaler Bare-Clone, lokaler Arbeits-Clone. `SERVICES_RECOVERY.md:67-68`
|
||||||
|
nennt den lokalen Operator-Clone unter `G:\Gitea_Clone\homelab-infra\` als
|
||||||
|
Vorzug.
|
||||||
|
|
||||||
|
Luecke: der Pfad "wie kommt der Windows-Clone auf einen frisch installierten
|
||||||
|
Unraid-Host" ist nicht beschrieben. Implizit: SMB-Share, USB-Stick, scp ueber
|
||||||
|
LAN. Aber auf einem frisch aufgesetzten Unraid existiert noch keine
|
||||||
|
funktionierende SMB-Konfiguration; SSH-Key vom Operator-PC ist nicht
|
||||||
|
vorbereitet.
|
||||||
|
|
||||||
|
Vorschlag: Zwei Saetze in `DISASTER_RECOVERY.md` Phase 0 ergaenzen, wie der
|
||||||
|
Operator-Clone konkret zum Host kommt (USB-Stick + `mkdir -p
|
||||||
|
/mnt/user/services/homelab-infra && rsync -a` aus Operator-Windows-PC, oder
|
||||||
|
direkt vom GitHub-Mirror per `git clone https://github.com/...` auf dem
|
||||||
|
Unraid-Host).
|
||||||
|
|
||||||
|
### P0-2 (HIGH) - GitHub-Mirror-Zugang im DR
|
||||||
|
|
||||||
|
`SECRETS_MAP.md:42` sagt, der GitHub-Push-Mirror-PAT liegt in den
|
||||||
|
Gitea-Mirror-Settings persistent unter `/mnt/user/services/gitea/data`.
|
||||||
|
`EXTERNAL_DEPENDENCIES.md:18` nennt den Mirror als `michaelkaleschke-spec/
|
||||||
|
homelab-infra` und betont "privater" Push-Mirror.
|
||||||
|
|
||||||
|
Luecke: Wenn der Mirror **privat** ist, scheitert ein anonymer `git clone`
|
||||||
|
im DR-Bootstrap. Es gibt keine dokumentierte Notfall-Quelle fuer einen
|
||||||
|
Read-PAT/SSH-Key, der lokal beim Operator (nicht in Gitea, nicht im Repo)
|
||||||
|
verfuegbar ist.
|
||||||
|
|
||||||
|
Vorschlag in `EXTERNAL_DEPENDENCIES.md`: entweder explizit dokumentieren,
|
||||||
|
dass der Mirror lesend `Public` ist (DR-fit), oder einen Read-PAT in der
|
||||||
|
Vaultwarden-/Offline-Notiz neben der Borg-Passphrase als Bootstrap-Voraussetzung
|
||||||
|
benennen.
|
||||||
|
|
||||||
|
## Phase 1 - Unraid und Shares
|
||||||
|
|
||||||
|
### P1-1 (CRITICAL) - Unraid-Flash-Restore ohne laufenden Host
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:107` sagt: "Primaere lokale/off-site
|
||||||
|
Restore-Quelle fuer die bestehende Flash-Konfiguration ist das
|
||||||
|
Borg-Artefakt `unraid-flash-config.tar.gz` aus
|
||||||
|
`/mnt/user/backups/borg/dumps/latest`."
|
||||||
|
|
||||||
|
Henne-Ei: der Pfad ist auf den verlorenen Shares oder auf Hetzner. Hetzner-
|
||||||
|
Zugriff braucht einen funktionierenden Linux-Host mit Borg-Client und
|
||||||
|
Passphrase. Im Bare-Metal-Fall ist genau das nicht da. RESTORE_MATRIX.md
|
||||||
|
Tier 1 Zeile `Unraid OS Flash` (`docs/RESTORE_MATRIX.md:29`) sagt nur "Unraid
|
||||||
|
USB Flash Creator / neuer Boot-Stick" - das beschreibt die Stick-Erzeugung,
|
||||||
|
nicht den Extract des Borg-Artefakts.
|
||||||
|
|
||||||
|
Operativ: Operator braucht einen Laptop mit Borg-Client + Passphrase +
|
||||||
|
SSH-Key fuer die Hetzner-Storage-Box. Das ist eine **separat zu pflegende
|
||||||
|
Operator-Workstation-Voraussetzung** und ist in keinem Repo-Dokument als
|
||||||
|
DR-Vorbedingung gelistet.
|
||||||
|
|
||||||
|
Vorschlag: In `EXTERNAL_DEPENDENCIES.md` oder `DISASTER_RECOVERY.md`
|
||||||
|
Abschnitt 3 als Pflichtposten aufnehmen: "Operator-Laptop mit installiertem
|
||||||
|
Borg-Client, SSH-Key fuer Hetzner und Zugriff auf die offline gesicherte
|
||||||
|
Passphrase". Inklusive Test, dass der Operator den Extract tatsaechlich
|
||||||
|
durchfuehren kann.
|
||||||
|
|
||||||
|
### P1-2 (MED) - Unraid-OS-Flash-Restore-Test nie gelaufen
|
||||||
|
|
||||||
|
`docs/RESTORE_MATRIX.md:140` Spalte "Letzter Restore-Test" fuer Unraid OS
|
||||||
|
Flash: `-` (kein Test). Das ist die Grundlage fuer Phase 1 und ist nie als
|
||||||
|
Smoke verifiziert. Empfehlung: einmaliger Test, der die Tar-Archiv-Struktur
|
||||||
|
gegen die erwarteten Flash-Pfade prueft (kein echter Boot-Test noetig).
|
||||||
|
|
||||||
|
## Phase 2 - Secrets und Stack-ENV
|
||||||
|
|
||||||
|
### P2-1 (HIGH) - KOMODO_* externe Operator-Notiz als Pflichtquelle
|
||||||
|
|
||||||
|
`docs/SECRETS_MAP.md:132,138-143` macht den Komodo-Sonderfall klar: die
|
||||||
|
KOMODO_*-Secrets sind aus dem eigenen Mongo-Dump nicht rekonstruierbar,
|
||||||
|
solange Komodo nicht laeuft. Quellen: Vaultwarden ODER externe Notiz.
|
||||||
|
|
||||||
|
Im Bare-Metal-Fall ist Vaultwarden in DR.md Phase 4 Stufe 4, Komodo in
|
||||||
|
Phase 4 Stufe 3. Damit ist die **externe Operator-Notiz** die einzige
|
||||||
|
Pflichtquelle in der Reihenfolge.
|
||||||
|
|
||||||
|
Luecke: ob diese Notiz wirklich existiert und die 5 Werte
|
||||||
|
(KOMODO_SECRET_KEY, KOMODO_WEBHOOK_SECRET, KOMODO_JWT_SECRET,
|
||||||
|
KOMODO_MONGO_PASSWORD, KOMODO_PERIPHERY_PASSKEY) enthaelt, ist in keinem
|
||||||
|
Repo-Dokument bestaetigt. Die Borg-Passphrase ist als "Operator-Bestaetigung
|
||||||
|
2026-05-26" dokumentiert; eine analoge Bestaetigung fuer die KOMODO_*-Notiz
|
||||||
|
fehlt.
|
||||||
|
|
||||||
|
Vorschlag: gleiche Form wie Borg-Passphrase - eine Zeile in
|
||||||
|
`EXTERNAL_DEPENDENCIES.md` "Komodo-Stack-ENV-Notiz offline gesichert,
|
||||||
|
Operator-Bestaetigung YYYY-MM-DD".
|
||||||
|
|
||||||
|
### P2-2 (HIGH) - Reihenfolgen-Inkonsistenz DR vs. SERVICES_RECOVERY
|
||||||
|
|
||||||
|
`docs/SERVICES_RECOVERY.md:102` (Stufe C, Komodo-Bootstrap): "Vaultwarden
|
||||||
|
(sobald restauriert), externe Operator-Notiz, oder Komodo-Mongo-Dump (nur
|
||||||
|
wenn Mongo separat bereits gestartet ...)".
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:247-301` (Phase 4): Stufe 3 = Komodo, Stufe 4 =
|
||||||
|
Vaultwarden.
|
||||||
|
|
||||||
|
Wenn ein Leser sich an DR.md Phase 4 haelt, ist Vaultwarden nach Komodo
|
||||||
|
fertig. Aber SERVICES_RECOVERY.md Stufe C setzt Vaultwarden als optionale
|
||||||
|
Vorab-Quelle voraus. Ohne externe Notiz heisst das praktisch: Komodo kann
|
||||||
|
nicht starten. Die Konsequenz steht nirgendwo explizit in DR.md.
|
||||||
|
|
||||||
|
Vorschlag: In `DISASTER_RECOVERY.md` Phase 4 Stufe 3 einen Hinweisblock
|
||||||
|
ergaenzen: "KOMODO_*-Werte muessen vor Stufe 3 aus externer Notiz oder
|
||||||
|
einer in Stufe 2 voraus gezogenen Vaultwarden-Instanz vorliegen. Default-
|
||||||
|
Pfad: externe Notiz."
|
||||||
|
|
||||||
|
### P2-3 (MED) - `homelab_smtp_password.txt` fehlt in DR.md 6.1
|
||||||
|
|
||||||
|
`docs/SECRETS_MAP.md:20` listet `/mnt/user/appdata/secrets/
|
||||||
|
homelab_smtp_password.txt` fuer Vaultwarden-SMTP. In `DISASTER_RECOVERY.md`
|
||||||
|
Abschnitt 6.1 (`docs/DISASTER_RECOVERY.md:136-151`) ist sie nicht
|
||||||
|
aufgefuehrt. Vaultwarden startet ohne, kann aber keine Einladungs-/
|
||||||
|
Benachrichtigungs-Mails versenden. Klein, aber unsichtbarer Folgefehler im
|
||||||
|
Familien-Onboarding-Pfad.
|
||||||
|
|
||||||
|
### P2-4 (MED) - `n8n_encryption_key.txt` fehlt in DR.md 6.1
|
||||||
|
|
||||||
|
`docs/SECRETS_MAP.md:58` listet `/mnt/user/appdata/secrets/
|
||||||
|
n8n_encryption_key.txt`. In DR.md 6.1 fehlt sie komplett.
|
||||||
|
`SECRETS_MAP.md:135` macht die Folgen explizit: "Bei Verlust aller
|
||||||
|
Quellen: n8n startet, aber alle gespeicherten Credentials sind unbrauchbar".
|
||||||
|
Da n8n den GMX-Mail-Workflow fuer das Gitea-`Micha/mails`-Repo betreibt,
|
||||||
|
ist das ein direkter Workflow-Ausfall.
|
||||||
|
|
||||||
|
### P2-5 (LOW) - Monitoring-/Filebrowser-Secrets fehlen in DR.md 6.1
|
||||||
|
|
||||||
|
`docs/SECRETS_MAP.md:53-55`: `influxdb3_admin_token.json`,
|
||||||
|
`monitoring_grafana_admin_password.txt`,
|
||||||
|
`monitoring_grafana_influxdb_token.txt` sowie
|
||||||
|
`filebrowser_admin_password.txt` sind nicht in DR.md 6.1. Tier-3-Apps,
|
||||||
|
Folge ist nur ein UI-Initialisierungs-Schritt nach Wiederanlauf. Keine
|
||||||
|
Critical-Konsequenz, aber Inkonsistenz.
|
||||||
|
|
||||||
|
## Phase 3 - Borg-Extract
|
||||||
|
|
||||||
|
### P3-1 (HIGH) - Borg-Client ohne `borg-ui`-Container
|
||||||
|
|
||||||
|
`docs/RESTORE_HANDBOOK.md:30-33` sagt explizit: "Borg-Zugriff laeuft ueber
|
||||||
|
den vorhandenen `borg-ui`-Container".
|
||||||
|
|
||||||
|
Im Bare-Metal-Fall ist `borg-ui` selbst kalt (Tier 3, DR.md Phase 4 Stufe 5).
|
||||||
|
Es gibt keinen dokumentierten Pfad, wie der erste Borg-Extract ohne diesen
|
||||||
|
Container laeuft. Implizite Optionen: nativer Borg auf Unraid (Plugin),
|
||||||
|
`docker run --rm borgbackup/borg`, oder Operator-Laptop. Keine davon ist
|
||||||
|
benannt.
|
||||||
|
|
||||||
|
Vorschlag: In `RESTORE_HANDBOOK.md` Abschnitt 2 einen "Bare-Metal-Vorlauf"
|
||||||
|
ergaenzen, der den initialen Borg-Extract ohne borg-ui-Container
|
||||||
|
beschreibt - z. B. `docker run --rm -v
|
||||||
|
/mnt/user/backups/restore-lab:/restore borgbackup/borg ...`.
|
||||||
|
|
||||||
|
### P3-2 (HIGH) - Borg-Passphrase-Bootstrap nicht als expliziter Schritt
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:68`: "Host-Secret-Datei vorhanden und fuer
|
||||||
|
Borg-Zugriff verifiziert; externe Offline-Hinterlegung vom Operator am
|
||||||
|
2026-05-26 bestaetigt."
|
||||||
|
|
||||||
|
Praktisch heisst das: im Bare-Metal-Fall liest der Operator die Passphrase
|
||||||
|
aus einem analogen Medium und tippt sie in den Borg-Client. Das ist ein
|
||||||
|
**Bootstrap-Schritt**, der nicht als Schritt dokumentiert ist. Er steckt
|
||||||
|
implizit in "extern bestaetigt".
|
||||||
|
|
||||||
|
Vorschlag: Ein nummerierter Bullet in `DISASTER_RECOVERY.md` Phase 3 ("Wenn
|
||||||
|
echte Daten aus Borg benoetigt werden"): "Schritt 1: Borg-Passphrase aus
|
||||||
|
Offline-Sicherung beschaffen. Wert wird nicht in Skripte oder Tickets
|
||||||
|
kopiert; nur in den interaktiven Borg-Aufruf eingegeben."
|
||||||
|
|
||||||
|
### P3-3 (MED) - Hetzner-Maintenance-Key im Bare-Metal
|
||||||
|
|
||||||
|
`docs/EXTERNAL_DEPENDENCIES.md:17`: "Maintenance-Key liegt in Vaultwarden".
|
||||||
|
|
||||||
|
Im Bare-Metal-Bootstrap ist Vaultwarden Phase 4 Stufe 4. Damit ist der Key
|
||||||
|
fuer die initiale Phase-3-Hetzner-Verbindung nicht zugaenglich. Implizit
|
||||||
|
muss er ebenfalls offline gesichert sein (analog Borg-Passphrase).
|
||||||
|
|
||||||
|
Vorschlag: gleiche Form wie Borg-Passphrase - eine Operator-Bestaetigung
|
||||||
|
in `EXTERNAL_DEPENDENCIES.md`, dass der Hetzner-SSH-Key auch ausserhalb von
|
||||||
|
Vaultwarden offline verfuegbar ist. Sonst ist die "Vaultwarden"-Aussage
|
||||||
|
fuer Bare-Metal eine Falle.
|
||||||
|
|
||||||
|
## Phase 4 - Bootstrap-Reihenfolge
|
||||||
|
|
||||||
|
### P4-1 (HIGH) - Externe Docker-Netze in DR.md Phase 4 Stufe 1 nicht erwaehnt
|
||||||
|
|
||||||
|
`docs/SERVICES_RECOVERY.md:82-84` Stufe A schreibt explizit: "Externe
|
||||||
|
Docker-Netze existieren oder werden erzeugt (`frontend_net`, `backend_net`).
|
||||||
|
Wenn nicht vorhanden: `docker network create --driver bridge frontend_net`
|
||||||
|
bzw. `... --internal backend_net`."
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:252-260` Phase 4 Stufe 1 nennt nur Traefik,
|
||||||
|
AdGuard, Tailscale. Kein Hinweis auf externe Netze.
|
||||||
|
|
||||||
|
`traefik/docker-compose.yml:70-76` deklariert `frontend_net`, `backend_net`,
|
||||||
|
`monitoring_net` als `external: true`. Ohne vorab erstellte Netze scheitert
|
||||||
|
der erste `docker compose up` mit "network frontend_net not found".
|
||||||
|
|
||||||
|
Vorschlag: In `DISASTER_RECOVERY.md` Phase 4 vor Stufe 1 einen Vorlauf
|
||||||
|
"Stufe 0 - Docker-Grundlage" einfuegen, der die Netzwerk-Erzeugung wie in
|
||||||
|
`SERVICES_RECOVERY.md` Stufe A explizit listet.
|
||||||
|
|
||||||
|
### P4-2 (HIGH) - Cloudflare-LE-Rate-Limit-Risiko
|
||||||
|
|
||||||
|
`docs/RESTORE_MATRIX.md:30` markiert `letsencrypt` korrekt als
|
||||||
|
Restore-relevant. `docs/DISASTER_RECOVERY.md:240` listet
|
||||||
|
`/mnt/user/appdata/traefik/letsencrypt` ebenfalls als kritischen
|
||||||
|
Borg-Restore-Pfad.
|
||||||
|
|
||||||
|
Luecke: kein Hinweis auf den Praxisfall "LE-State verloren, frischer
|
||||||
|
Acme-Run". Let's Encrypt hat ein Rate-Limit von 50 Zertifikaten/Domain/
|
||||||
|
Woche und 5 Duplicate-Zertifikate/Woche. Bei einer Multi-Sub-Domain-
|
||||||
|
Konstellation wie `*.kaleschke.info` (15+ Hostnames) ist das beim
|
||||||
|
hektischen DR-Bootstrap erreichbar.
|
||||||
|
|
||||||
|
Vorschlag: In `DISASTER_RECOVERY.md` Phase 4 Stufe 1 einen Hinweis: "Bei
|
||||||
|
verlorenem oder unklarem `acme.json` zuerst gegen
|
||||||
|
`acme-staging-v02.api.letsencrypt.org` ausstellen lassen, erst nach
|
||||||
|
gruenem Smoke auf Production-CA umschalten." Ist eine Praesentations-
|
||||||
|
Aenderung in den Compose-Args, kein neuer Code.
|
||||||
|
|
||||||
|
### P4-3 (MED) - `traefik/dynamic/*` als Pre-Check fehlt
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:357-365` Sektion 10 beschreibt die manuelle
|
||||||
|
Sonderregel fuer `traefik/dynamic/*`. Korrekt.
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:252-260` Phase 4 Stufe 1 verweist nicht auf
|
||||||
|
diese Sonderregel. Wer der Reihenfolge folgt und Sektion 10 nicht liest,
|
||||||
|
startet Traefik ohne Middlewares - alle 2FA-Routen brechen still.
|
||||||
|
|
||||||
|
Vorschlag: Cross-Reference in Phase 4 Stufe 1: "Vor `docker compose up
|
||||||
|
traefik` pruefen, dass `/mnt/user/appdata/traefik/dynamic/middlewares.yml`,
|
||||||
|
`tls.yml`, `dashboards.yml` vorhanden sind (Sonderregel Sektion 10)."
|
||||||
|
|
||||||
|
### P4-4 (HIGH) - Authelia "frische Postgres ohne Dump"-Pfad fehlt
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:267-275` Phase 4 Stufe 2 startet Postgres und
|
||||||
|
Authelia. Authelia erwartet eine Rolle `authelia` mit dem Passwort aus
|
||||||
|
`authelia_postgres_password.txt`. Im Restore-Pfad mit `pg_dumpall --globals-
|
||||||
|
only` ist die Rolle abgedeckt.
|
||||||
|
|
||||||
|
Bei einem **fresh-start** (keine alten Daten, nur Container hochfahren) ist
|
||||||
|
die Rolle nicht da. Postgres-Image legt sie nicht automatisch an. Authelia
|
||||||
|
schlaegt mit "FATAL: role authelia does not exist" fehl.
|
||||||
|
|
||||||
|
Luecke: Der Initialisierungspfad fuer eine frische Postgres ohne
|
||||||
|
pg_dumpall ist in der Doku nicht beschrieben. Im echten DR mit Borg ist
|
||||||
|
das unwahrscheinlich, aber im Soft-Recovery oder Migrations-Drill schon.
|
||||||
|
|
||||||
|
Vorschlag: In `DISASTER_RECOVERY.md` Phase 4 Stufe 2 eine optionale
|
||||||
|
Anweisung: "Falls Postgres frisch ist (kein Dump-Restore), `infra/
|
||||||
|
postgresql17/init/`-Skripte oder manuelle `CREATE ROLE`/`CREATE DATABASE`-
|
||||||
|
Schritte ergaenzen."
|
||||||
|
|
||||||
|
### P4-5 (LOW) - Gitea nach Postgres ist faktisch unnoetig
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:267-275` Phase 4 Stufe 2 ordnet Gitea hinter
|
||||||
|
Postgres ein. Gitea nutzt SQLite (`gitea.sqlite.dump`), nicht den shared
|
||||||
|
Postgres. Reihenfolge ist nicht falsch, aber irrefuehrend. Nicht kritisch.
|
||||||
|
|
||||||
|
### P4-6 (HIGH) - Komodo-Mongo Passwort-Lockout-Risiko
|
||||||
|
|
||||||
|
`ops/komodo/docker-compose.yml:18-20` zeigt: `komodo-mongo` initialisiert
|
||||||
|
sich bei leerem Datadir mit `MONGO_INITDB_ROOT_PASSWORD_FILE` aus
|
||||||
|
`/mnt/user/appdata/secrets/komodo_mongo_password.txt`.
|
||||||
|
|
||||||
|
Restore-Fall: Datadir aus Borg restauriert, Secret-Datei aus Borg
|
||||||
|
restauriert - beide aus demselben Snapshot. OK.
|
||||||
|
|
||||||
|
Riskanter Fall: Datadir aus Borg, aber Secret-Datei aus einer anderen
|
||||||
|
(neueren oder aelteren) Quelle. Mongo akzeptiert den Login nicht, Komodo
|
||||||
|
laeuft nicht. Lockout. Doku erwaehnt diesen Pin-Punkt nicht.
|
||||||
|
|
||||||
|
Vorschlag: Hinweis in `DISASTER_RECOVERY.md` Phase 4 Stufe 3: "Mongo-
|
||||||
|
Datadir und `komodo_mongo_password.txt` muessen aus demselben Snapshot
|
||||||
|
kommen. Bei Mismatch: leeren Datadir und Re-Init, dann Daten aus
|
||||||
|
`komodo-mongo.archive.gz` per `mongorestore`."
|
||||||
|
|
||||||
|
### P4-7 (MED) - Hardgecodete LAN-IP in `extra_hosts`
|
||||||
|
|
||||||
|
`ops/komodo/docker-compose.yml:50` und `:101` haben:
|
||||||
|
`"git.kaleschke.info:192.168.178.58"`.
|
||||||
|
|
||||||
|
Bare-Metal-Recovery auf anderer Hardware oder veraenderter LAN-IP fuehrt
|
||||||
|
zu stummem Fehler: Komodo-Core kann Gitea nicht ueber den Override
|
||||||
|
erreichen, faellt auf AdGuard-DNS zurueck (wenn der schon laeuft) oder
|
||||||
|
scheitert.
|
||||||
|
|
||||||
|
Vorschlag: kurzer Hinweis in `DISASTER_RECOVERY.md` Phase 4 Stufe 3: "Bei
|
||||||
|
geaenderter Host-LAN-IP `extra_hosts`-Werte in `ops/komodo/docker-compose.
|
||||||
|
yml` vor `compose up` anpassen oder ueber `.env` parametrisieren."
|
||||||
|
|
||||||
|
### P4-8 (HIGH) - Stack-ENV-Wiederherstellung praktisch manuell
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:188-195` sagt: "Wenn `komodo-mongo.archive.gz`
|
||||||
|
frisch ist, koennen die Werte beim Komodo-Restart aus dem Dump
|
||||||
|
zurueckgespielt werden, ohne dass jemand sie sieht."
|
||||||
|
|
||||||
|
`docs/RESTORE_HANDBOOK.md:73-74` und `docs/AUDIT_2026-05-25_TODO.md:20`
|
||||||
|
machen den Daten-Mongo-Restore als "erledigt 2026-06-03" sichtbar - aber
|
||||||
|
NICHT als Teil des DR-Bootstraps. Komodo-Bootstrap im Trockenlauf benutzt
|
||||||
|
Wegwerf-Werte.
|
||||||
|
|
||||||
|
Praktisch heisst das: Im DR-Bootstrap (Phase 4 Stufe 3) startet Komodo
|
||||||
|
**ohne** den Mongo-Daten-Restore. Die `KOMODO_*` kommen aus externer
|
||||||
|
Notiz. Aber die Stack-ENVs fuer `paperless`/`immich`/`mail-archiver`/
|
||||||
|
`speedtest` (PAPERLESS_DBPASS etc.) **muessen vor Stufe 4** wieder in
|
||||||
|
Komodo eingetragen sein. Wenn der Mongo-Daten-Restore nicht direkt nach
|
||||||
|
Komodo-Start passiert, gehen diese Werte manuell in die Komodo-UI.
|
||||||
|
|
||||||
|
Vorschlag: Klarstellung in `DISASTER_RECOVERY.md` Phase 4 zwischen Stufe
|
||||||
|
3 und Stufe 4: "Optionaler Mongo-Daten-Restore aus `komodo-mongo.archive.
|
||||||
|
gz` per `ops/restore-tests/komodo-mongo-restore-test.sh`-Muster - dann
|
||||||
|
sind alle Stack-ENVs zurueck. Alternativ: Stack-ENVs manuell in Komodo-
|
||||||
|
UI eintragen, Quelle Vaultwarden (sobald Stufe 4 Vaultwarden laeuft -
|
||||||
|
Henne-Ei mit Paperless: Paperless-Start dann erst nach Vaultwarden, nicht
|
||||||
|
parallel)."
|
||||||
|
|
||||||
|
## Phase 5 - Verifikation
|
||||||
|
|
||||||
|
### P5-1 (LOW) - Smoke-Tests in DR.md weniger streng als Matrix
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:337-345` Phase 5.3 sagt z. B. "Vaultwarden
|
||||||
|
startet und ist erreichbar". `docs/RESTORE_MATRIX.md:39` sagt: "Login-
|
||||||
|
Seite erreichbar, Tresor-Daten sichtbar". Das zweite ist faktisch der
|
||||||
|
echte Smoke-Test.
|
||||||
|
|
||||||
|
Geschmackssache, kein Bug. Empfehlung: DR.md auf die Matrix-Smokes
|
||||||
|
verweisen statt eigene Kurzversion.
|
||||||
|
|
||||||
|
### P5-2 (MED) - Kein Verifikationspunkt App-zu-DB-Verbindung
|
||||||
|
|
||||||
|
`docs/DISASTER_RECOVERY.md:337-345` prueft App-Start, nicht DB-Auth-
|
||||||
|
Erfolg. Bei falschem `PAPERLESS_DBPASS`-Stack-ENV startet Paperless
|
||||||
|
moeglicherweise mit Error-Log und ist via Traefik nicht antwortend - aber
|
||||||
|
das fehlt als Pruefpunkt.
|
||||||
|
|
||||||
|
Vorschlag: Phase 5.3 ergaenzen: "Pro App: `docker logs <app>` zeigt keine
|
||||||
|
`password authentication failed`/`FATAL: role does not exist`-Eintraege."
|
||||||
|
|
||||||
|
## Uebergreifende Findings
|
||||||
|
|
||||||
|
### X-1 (HIGH) - Nextcloud-Restore-Skript existiert, ist aber ungetestet
|
||||||
|
|
||||||
|
`ops/restore-tests/nextcloud-restore-test.sh` und
|
||||||
|
`ops/restore-tests/nextcloud-compose.test.yml` existieren im Repo.
|
||||||
|
`docs/RESTORE_MATRIX.md:147` Spalte "Letzter Restore-Test" fuer Nextcloud:
|
||||||
|
`-`, naechster Lauf `**hoechste Prio**`. `docs/AUDIT_2026-05-25_TODO.md:18`
|
||||||
|
fuehrt es als P1 "offen".
|
||||||
|
|
||||||
|
Damit ist der echte Tabletop-Gewinn: der Test ist nicht "noch zu bauen",
|
||||||
|
sondern "noch nie ausgefuehrt". Ein `bash /mnt/user/services/homelab-
|
||||||
|
infra/ops/restore-tests/nextcloud-restore-test.sh` schliesst die letzte
|
||||||
|
Tier-2-Luecke.
|
||||||
|
|
||||||
|
## Nicht-Findings
|
||||||
|
|
||||||
|
Was ich gepruft und als sauber verifiziert habe:
|
||||||
|
|
||||||
|
- Referenzierte Skripte existieren alle: `pre-backup-dumps.sh`,
|
||||||
|
`gitea-bundle-mirror.sh`, `run-restore-checks.sh`,
|
||||||
|
`komodo-bootstrap-test.sh`, `posture-check.sh`, alle Restore-Test-
|
||||||
|
Skripte fuer Tier-1 und Tier-2.
|
||||||
|
- Pfadverweise zwischen DR.md, RESTORE_MATRIX.md, SECRETS_MAP.md,
|
||||||
|
SERVICES_RECOVERY.md sind konsistent (Borg-Dumps unter `/mnt/user/
|
||||||
|
backups/borg/dumps/latest`, Secrets unter `/mnt/user/appdata/secrets`).
|
||||||
|
- Drift-Erkennung Authelia (`services/authelia-diff.sh`) ist in
|
||||||
|
`posture-check` integriert (`WORKFLOW.md:292`).
|
||||||
|
- GitHub-Mirror-Pfad und Gitea-Bundle-Mirror als Repo-Bootstrap-Quellen
|
||||||
|
sind dreifach abgesichert (lokaler Clone, GitHub, Bundle).
|
||||||
|
- Tier-1-Postgres-Restore-Drill ist 2026-06-03 erfolgreich gelaufen
|
||||||
|
(`AUDIT_2026-05-25_TODO.md:19`).
|
||||||
|
- `ops/komodo/docker-compose.yml` ist als Recovery-Anker getestet
|
||||||
|
(`SERVICES_RECOVERY.md:142-166`).
|
||||||
|
- Borg-Passphrase und Hetzner-Account-Hygiene sind Operator-bestaetigt
|
||||||
|
(`AUDIT_2026-05-25_TODO.md:46-47`).
|
||||||
|
|
||||||
|
## Vorschlag fuer Reihenfolge der Folge-Arbeit
|
||||||
|
|
||||||
|
1. **CRITICAL P1-1 zuerst** - Operator-Laptop-Voraussetzung als
|
||||||
|
DR-Pflichtposten dokumentieren. Eine Dokuzeile.
|
||||||
|
2. **HIGH P0-2 + P3-3** - klaeren, ob GitHub-Mirror lesend public ist und
|
||||||
|
wo der Hetzner-Maintenance-Key offline liegt. Zwei Dokuzeilen oder
|
||||||
|
eine echte Setup-Entscheidung.
|
||||||
|
3. **HIGH P2-1** - Operator-Bestaetigung "KOMODO_*-Notiz offline
|
||||||
|
gesichert YYYY-MM-DD" in `EXTERNAL_DEPENDENCIES.md` ergaenzen (sobald
|
||||||
|
real angelegt).
|
||||||
|
4. **HIGH P4-1 + P4-2** - Vorlauf "Stufe 0 - Docker-Grundlage" und
|
||||||
|
LE-Staging-Hinweis in DR.md Phase 4 einfuegen. Etwa 10 Zeilen Doku.
|
||||||
|
5. **HIGH X-1** - `nextcloud-restore-test.sh` einmal scharf ausfuehren.
|
||||||
|
Vermutlich ein Vormittag inklusive Report-Review.
|
||||||
|
6. **HIGH P2-2 + P4-8** - Reihenfolgen-Konsistenz Komodo/Vaultwarden in
|
||||||
|
DR.md eindeutig aufloesen.
|
||||||
|
7. Rest in der Reihenfolge der Tabelle.
|
||||||
|
|
||||||
|
Punkte 1-4 sind reine Doku-Arbeit, keine Compose-/Runtime-Aenderung.
|
||||||
|
Punkt 5 ist ein echter Restore-Lauf mit Report. Punkt 6 ist die
|
||||||
|
substanziellste Doku-Aenderung in DR.md.
|
||||||
|
|
||||||
|
## Folge-Iteration 2026-06-03 (Doku-Fixes im selben Aenderungsblock)
|
||||||
|
|
||||||
|
Direkt nach dem Drill und nach Operator-Antworten auf vier offene Fragen wurden folgende Findings im Repo adressiert. Operator-Aufgaben, die ich nicht selbst tun kann, sind als P1 in `docs/AUDIT_2026-05-25_TODO.md` aufgenommen.
|
||||||
|
|
||||||
|
| ID | Massnahme |
|
||||||
|
|---|---|
|
||||||
|
| P0-1 | DR.md Phase 0 ergaenzt um "Operativer Pfad fuer den Repo auf den frisch installierten Unraid-Host" (USB/SMB/rsync); DR.md Abschnitt 3 mit Zeile "Operator-DR-Workstation"; `EXTERNAL_DEPENDENCIES.md` neuer Abschnitt "DR-Workstation Bare-Metal-Kit" |
|
||||||
|
| P0-2 | `EXTERNAL_DEPENDENCIES.md` GitHub-Mirror-Zeile praezisiert (privat, Read-PAT/Deploy-Key Pflicht); DR.md Phase 0 verweist explizit darauf; offene Operator-Aufgabe in Audit-Restliste |
|
||||||
|
| P1-1 | Operator-DR-Workstation als Voraussetzung in DR.md Abschnitt 3 und in `EXTERNAL_DEPENDENCIES.md`; konkrete Pflichtbestandteile (WSL2, Borg, SSH-Key) gelistet |
|
||||||
|
| P1-2 | Bleibt offen als P3-Test in Restore-Backlog (kein Doku-Fix moeglich) |
|
||||||
|
| P2-1 | KOMODO_*-Notiz als kritische Secret-Zeile in `EXTERNAL_DEPENDENCIES.md` mit Status "noch nicht angelegt"; Operator-Aufgabe in Audit-Restliste |
|
||||||
|
| P2-2 | DR.md Phase 4 Stufe 3 ergaenzt um expliziten Hinweis "KOMODO_* aus externer Notiz oder voraus gezogener Vaultwarden" |
|
||||||
|
| P2-3 | DR.md Abschnitt 6.1 um `homelab_smtp_password.txt` erweitert |
|
||||||
|
| P2-4 | DR.md Abschnitt 6.1 um `n8n_encryption_key.txt` erweitert |
|
||||||
|
| P2-5 | DR.md Abschnitt 6.1 um Monitoring-Grafana/InfluxDB-/Filebrowser-Secrets erweitert |
|
||||||
|
| P3-1 | DR.md neuer Abschnitt 7.3 "Borg-Extract ohne `borg-ui`-Container" mit DR-Workstation- und Docker-Variante |
|
||||||
|
| P3-2 | DR.md Abschnitt 7.3 nennt Passphrase-Eingabe explizit als interaktiven Bootstrap-Schritt |
|
||||||
|
| P3-3 | `EXTERNAL_DEPENDENCIES.md` Review-Zeile 2026-06-03: Hetzner-Maintenance-Key auch offline bestaetigt |
|
||||||
|
| P4-1 | DR.md Phase 4 neue Stufe 0 "Docker-Grundlage" mit `docker network create` Befehlen |
|
||||||
|
| P4-2 | DR.md Phase 4 Stufe 1 LE-Staging-Hinweis bei verlorenem `acme.json` |
|
||||||
|
| P4-3 | DR.md Phase 4 Stufe 0 nennt `traefik/dynamic/*` als Pre-Check |
|
||||||
|
| P4-4 | Wird mit fresh-Postgres-Initialisierungsskripten ohne Doku-Aenderung nicht sinnvoll abgedeckt; bleibt als Doku-Hinweis offen, ist im realen Restore-Pfad mit `pg_dumpall --globals-only` abgedeckt |
|
||||||
|
| P4-5 | LOW, nicht angepasst (Reihenfolge nicht falsch, nur irrefuehrend) |
|
||||||
|
| P4-6 | DR.md Phase 4 Stufe 3 "Wichtige Stolperfallen": Mongo-Datadir/Secret-Mismatch und Re-Init-Pfad |
|
||||||
|
| P4-7 | DR.md Phase 4 Stufe 3 "Wichtige Stolperfallen": `extra_hosts`-Anpassung bei IP-Wechsel |
|
||||||
|
| P4-8 | DR.md Phase 4 Stufe 3 "Wichtige Stolperfallen": Stack-ENV-Wiederherstellung per `mongorestore` oder manuell |
|
||||||
|
| P5-1 | LOW, nicht angepasst |
|
||||||
|
| P5-2 | DR.md Phase 5.3 um `docker logs`-Verifikation der App-zu-DB-Verbindung erweitert |
|
||||||
|
| X-1 | **erledigt 2026-06-03**: Nextcloud-Restore-Test scharf gelaufen, drei Iterationen (zwei Skript-Bugs gefixt), Endresultat SUCCESS mit HTTP 200, occ status ok, 126 DB-Tabellen. Damit ist Tier-2 vollstaendig belegt. |
|
||||||
|
|
||||||
|
Nicht angefasst: P1-2 (kein Doku-Fix moeglich), P4-4 (im echten Restore-Pfad ohnehin abgedeckt), P4-5 und P5-1 (LOW). Die offenen Operator-Aufgaben (KOMODO_*-Notiz, Read-PAT, DR-Workstation, Nextcloud-Restore) stehen jetzt in `docs/AUDIT_2026-05-25_TODO.md` als P1.
|
||||||
|
|
||||||
|
## Reproduktion dieses Drills
|
||||||
|
|
||||||
|
```text
|
||||||
|
Methode: kalter Lesetest gegen
|
||||||
|
- docs/DISASTER_RECOVERY.md
|
||||||
|
- docs/RESTORE_MATRIX.md
|
||||||
|
- docs/SECRETS_MAP.md
|
||||||
|
- docs/SERVICES_RECOVERY.md
|
||||||
|
- docs/RESTORE_HANDBOOK.md
|
||||||
|
- docs/EXTERNAL_DEPENDENCIES.md
|
||||||
|
- ops/komodo/docker-compose.yml
|
||||||
|
- traefik/docker-compose.yml
|
||||||
|
Verifizierte Skript-Existenz: ops/borg-ui/scripts/*, ops/restore-tests/*,
|
||||||
|
services/posture-check/*
|
||||||
|
Kein Container gestartet, kein Skript ausgefuehrt, keine produktiven
|
||||||
|
Pfade beruehrt.
|
||||||
|
```
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
# DR-Workstation Setup-Runbook
|
||||||
|
|
||||||
|
Stand: 2026-06-03
|
||||||
|
|
||||||
|
Konkrete Schritte, um den Operator-Gaming-PC als DR-Workstation einzurichten. Der Endzustand ist in `docs/EXTERNAL_DEPENDENCIES.md` Abschnitt "DR-Workstation Bare-Metal-Kit" beschrieben; dieses Dokument ist der Weg dahin.
|
||||||
|
|
||||||
|
Vorbedingung: Repo-Clone unter `G:\Gitea_Clone\homelab-infra`, Hetzner-DR-SSH-Key und GitHub-Deploy-Key liegen offline auf USB.
|
||||||
|
|
||||||
|
Aufwand: einmalig ~30-60 Min interaktiv.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 1 - WSL2 + Ubuntu installieren (~15 Min)
|
||||||
|
|
||||||
|
PowerShell als **Administrator** oeffnen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wsl --install -d Ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
|
- Bei "Virtualization nicht aktiviert"-Fehler: BIOS rein, Intel VT-x / AMD-V einschalten, neu starten, Befehl wiederholen.
|
||||||
|
- Nach Install: Ubuntu startet automatisch und fragt nach Username + Passwort. Username egal (z. B. `dr`), Passwort merken (wird fuer `sudo` gebraucht).
|
||||||
|
- Reboot kann noetig sein - PowerShell sagt es.
|
||||||
|
|
||||||
|
Verifikation in Ubuntu (oeffnet sich automatisch):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lsb_release -a
|
||||||
|
uname -r
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `Ubuntu 24.04 LTS`, Kernel beginnt mit `5.x` oder `6.x` und enthaelt `microsoft-standard-WSL2`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 2 - Borg-Client installieren (~3 Min)
|
||||||
|
|
||||||
|
In der Ubuntu-Shell:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y borgbackup openssh-client
|
||||||
|
borg --version
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: `borg 1.2.x` oder `1.4.x`. Beides reicht fuer das produktive Borg-Repo auf Hetzner.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 3 - Hetzner-DR-SSH-Key in WSL ablegen (~5 Min)
|
||||||
|
|
||||||
|
Wichtig: der Private-Key liegt offline auf USB. Fuer die Workstation-Routine wird er auf das WSL-Filesystem kopiert - **das ist die Arbeitskopie**, nicht die Offline-Sicherung. Wenn die WSL kaputtgeht, kommt der Key zurueck vom USB; das Offline-Original bleibt unangetastet.
|
||||||
|
|
||||||
|
USB einstecken. In WSL kopieren (Pfad anpassen je nach Laufwerksbuchstabe):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
cp /mnt/<USB-Buchstabe>/dr-hetzner-2026-06-03/dr-hetzner ~/.ssh/dr-hetzner
|
||||||
|
chmod 600 ~/.ssh/dr-hetzner
|
||||||
|
```
|
||||||
|
|
||||||
|
`<USB-Buchstabe>` ist meistens `e`, `f` oder `g` - Windows-Laufwerke werden in WSL unter `/mnt/<buchstabe>` gemountet.
|
||||||
|
|
||||||
|
Smoke-Test:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -i ~/.ssh/dr-hetzner -o IdentitiesOnly=yes -p 23 \
|
||||||
|
u565255@u565255.your-storagebox.de "ls"
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: vier Verzeichnisse (`backup`, `backup2`, `hetzner_borg_appdata`, `hetzner_borg_appdata_critical`), exit 0, kein Passwort-Prompt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 4 - Borg-Passphrase eingeben und `borg list` testen (~5 Min)
|
||||||
|
|
||||||
|
Borg verlangt die Passphrase beim ersten Repo-Zugriff. Die liegt offline gesichert (Operator-Bestaetigung 2026-05-26).
|
||||||
|
|
||||||
|
Einmaliger Smoke gegen das wichtige Repo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export BORG_RSH="ssh -i ~/.ssh/dr-hetzner -o IdentitiesOnly=yes -p 23"
|
||||||
|
borg list ssh://u565255@u565255.your-storagebox.de/./hetzner_borg_appdata_critical
|
||||||
|
```
|
||||||
|
|
||||||
|
Borg fragt nach der Passphrase. Eingeben (sie wird nicht angezeigt, das ist normal).
|
||||||
|
|
||||||
|
Erwartet: Liste mit Archiv-Namen, jeder im Stil `Taegliche-Sicherung-YYYY-MM-DDTHH:MM:SS.xxx`. Wenn ja: Borg-Schicht funktioniert.
|
||||||
|
|
||||||
|
**Wert wird nirgendwo gespeichert.** `BORG_PASSPHRASE`-Env-Variable wird **nicht** dauerhaft gesetzt; Passphrase wird im Notfall immer interaktiv eingegeben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 5 - GitHub-Deploy-Key in WSL ablegen (~3 Min)
|
||||||
|
|
||||||
|
Gleiches Muster wie Hetzner-Key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp /mnt/<USB-Buchstabe>/dr-readonly-2026-06-03/dr-readonly ~/.ssh/dr-readonly
|
||||||
|
chmod 600 ~/.ssh/dr-readonly
|
||||||
|
```
|
||||||
|
|
||||||
|
Smoke-Test gegen den privaten GitHub-Mirror:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GIT_SSH_COMMAND="ssh -i ~/.ssh/dr-readonly -o IdentitiesOnly=yes" \
|
||||||
|
git ls-remote git@github.com:michaelkaleschke-spec/homelab-infra.git | head -3
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartet: HEAD und mindestens ein `refs/heads/master`-Eintrag.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 6 - Quartals-Smoke als Skript ablegen (~5 Min)
|
||||||
|
|
||||||
|
Damit der "ich pruefe das vierteljaehrlich"-Schritt zur Routine wird, ein kleines Skript ins WSL-Home:
|
||||||
|
|
||||||
|
Stand 2026-06-06: Das Skript liegt zusaetzlich versioniert unter
|
||||||
|
`ops/maintenance/dr-workstation-smoke.sh` und wurde auf `baerchen` bereits nach
|
||||||
|
`~/dr-smoke.sh` in die Ubuntu-WSL kopiert. Borg 1.2.8 ist installiert, die
|
||||||
|
DR-Key-Arbeitskopien liegen unter `~/.ssh/dr-readonly` und
|
||||||
|
`~/.ssh/dr-hetzner`, GitHub-Read-Smoke und Hetzner-SSH-Smoke sind erfolgreich.
|
||||||
|
Der finale Borg-Smoke via `bash ~/dr-smoke.sh` wurde am 2026-06-06 ebenfalls
|
||||||
|
erfolgreich gefahren (`DR-Smoke OK (2026-06-06 10:05:30)`). Die Borg-Passphrase
|
||||||
|
wurde nur interaktiv eingegeben und nicht gespeichert.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat > ~/dr-smoke.sh <<'EOF'
|
||||||
|
#!/bin/bash
|
||||||
|
# DR-Workstation Quartals-Smoke
|
||||||
|
# Pruefen: GitHub-Read, Hetzner-SSH, Borg-Repo-Erreichbarkeit
|
||||||
|
# Passphrase wird interaktiv abgefragt - Skript speichert keinen Wert.
|
||||||
|
set -e
|
||||||
|
echo "=== GitHub Deploy-Key ==="
|
||||||
|
GIT_SSH_COMMAND="ssh -i ~/.ssh/dr-readonly -o IdentitiesOnly=yes" \
|
||||||
|
git ls-remote git@github.com:michaelkaleschke-spec/homelab-infra.git \
|
||||||
|
| head -1
|
||||||
|
echo
|
||||||
|
echo "=== Hetzner SSH-Login ==="
|
||||||
|
ssh -i ~/.ssh/dr-hetzner -o IdentitiesOnly=yes -p 23 \
|
||||||
|
u565255@u565255.your-storagebox.de "ls" | head -5
|
||||||
|
echo
|
||||||
|
echo "=== Borg-Repo (Passphrase wird abgefragt) ==="
|
||||||
|
export BORG_RSH="ssh -i ~/.ssh/dr-hetzner -o IdentitiesOnly=yes -p 23"
|
||||||
|
borg info ssh://u565255@u565255.your-storagebox.de/./hetzner_borg_appdata_critical | head -10
|
||||||
|
echo
|
||||||
|
echo "DR-Smoke OK ($(date '+%F %T'))"
|
||||||
|
EOF
|
||||||
|
chmod +x ~/dr-smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Aufrufen mit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ~/dr-smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Termin im Kalender: einmal pro Quartal, ~5 Min Aufwand.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Schritt 7 - Eintrag in EXTERNAL_DEPENDENCIES Review nachziehen
|
||||||
|
|
||||||
|
Nach erfolgreicher Einrichtung im Repo dokumentieren. In `docs/EXTERNAL_DEPENDENCIES.md` unter "Review":
|
||||||
|
|
||||||
|
```
|
||||||
|
| 2026-06-XX | DR-Workstation produktiv: WSL2 Ubuntu auf Gaming-PC, borgbackup installiert, Hetzner-DR-Key und GitHub-Deploy-Key in ~/.ssh, Quartals-Smoke-Skript ~/dr-smoke.sh. Bare-Metal-DR-Pillars sind damit alle vier produktionsreif. | Quartalsweise Smoke laufen lassen |
|
||||||
|
```
|
||||||
|
|
||||||
|
Audit-Restliste analog: in `docs/AUDIT_2026-05-25_TODO.md` den P1 "DR-Workstation Bare-Metal-Kit: WSL2 + Borg-Client installieren" auf erledigt setzen und unter "Zuletzt geschlossen" einen Eintrag mit Smoke-Ergebnis machen.
|
||||||
|
|
||||||
|
Wenn ich (Claude) am Tag der Einrichtung mit SSH-Zugang dabei bin, ziehe ich das nach. Sonst per `git add docs/EXTERNAL_DEPENDENCIES.md docs/AUDIT_2026-05-25_TODO.md && git commit && git push`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### `wsl --install` schlaegt fehl mit "WSL 2 requires an update"
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
wsl --update
|
||||||
|
wsl --shutdown
|
||||||
|
wsl --install -d Ubuntu
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hetzner-SSH fragt nach Passwort statt Key-Login zu akzeptieren
|
||||||
|
|
||||||
|
Permissions des Keys pruefen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -la ~/.ssh/dr-hetzner
|
||||||
|
```
|
||||||
|
|
||||||
|
Muss `-rw-------` (also `600`) sein. Wenn anders:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod 600 ~/.ssh/dr-hetzner
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei weiterhin Passwort-Prompt: Pubkey-Inhalt gegen das authorized_keys-Format der Storage Box pruefen (sollte `ssh-ed25519 AAAA...` ohne Leerzeilen sein).
|
||||||
|
|
||||||
|
### `borg list` haengt oder schlaegt mit "Connection refused" fehl
|
||||||
|
|
||||||
|
Port 23 explizit pruefen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nc -vz u565255.your-storagebox.de 23
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn das fehlschlaegt: Hetzner-Status-Page pruefen, sonst SSH-Verbindung an sich blockiert (Firewall, ISP).
|
||||||
|
|
||||||
|
### GitHub-Pull fragt nach Username/Passwort
|
||||||
|
|
||||||
|
Stelle sicher dass die URL `git@github.com:...` ist (SSH), nicht `https://github.com/...`. Bei HTTPS wuerde GitHub Username/PAT verlangen, was wir bewusst nicht eingerichtet haben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Was nach diesem Runbook gilt
|
||||||
|
|
||||||
|
Mit allen Schritten erledigt ist der vierte Bare-Metal-DR-Pillar zu (siehe `docs/EXTERNAL_DEPENDENCIES.md`). Der DR-Workstation-Status ist dann:
|
||||||
|
|
||||||
|
- WSL2 Ubuntu installiert
|
||||||
|
- borgbackup einsatzbereit
|
||||||
|
- SSH-Keys (Hetzner, GitHub) in `~/.ssh/`
|
||||||
|
- Quartals-Smoke-Skript laeuft
|
||||||
|
|
||||||
|
Damit ist im Bare-Metal-Fall der Pfad "Unraid tot -> Gaming-PC nimmt die DR-Arbeit auf" tatsaechlich gangbar, nicht nur in Doku theoretisch.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# External Dependencies - KalliLab CORE
|
# External Dependencies - KalliLab CORE
|
||||||
|
|
||||||
Status: Initiale Betreiber-Baseline 2026-05-26; konkrete Account-Recovery-Codes und Besitznachweise muessen ausserhalb des Repos bestaetigt werden.
|
Status: Betreiber-Baseline 2026-06-01; Account-Recovery, Schluessel und Besitznachweise bleiben ausserhalb des Repos.
|
||||||
|
|
||||||
## Zweck
|
## Zweck
|
||||||
|
|
||||||
@@ -11,17 +11,19 @@ Dieses Dokument beschreibt externe Anbieter und Konten, von denen Betrieb, Recov
|
|||||||
| Anbieter / System | Zweck | Kritikalitaet | Recovery-Auswirkung | Zugang / Besitz | Notfallplan |
|
| 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 |
|
| 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 |
|
| 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-Backup vom 2026-06-01 liegt extern/off-system in Vaultwarden; Reset-Pin und Account-Pfad bereithalten; Remote-HTTPS/FTP/FTPS aus dem Internet sind aus |
|
||||||
| Domain-Registrar | Besitz `kaleschke.info` | hoch | Ohne Domain brechen Public URLs/TLS-Erneuerung | Operator-Konto ausserhalb Repo, konkreten Registrar im Account pruefen | Registrar-Zugang, 2FA-Recovery und Zahlungsweg analog/off-system sichern |
|
| Domain-Registrar | Besitz `kaleschke.info` | hoch | Ohne Domain brechen Public URLs/TLS-Erneuerung | Operator-Konto ausserhalb Repo, konkreten Registrar im Account pruefen | Registrar-Zugang, 2FA-Recovery und Zahlungsweg analog/off-system sichern |
|
||||||
| Cloudflare DNS | Authoritative DNS, ACME DNS-Challenge, DDNS | hoch | Neue Zertifikate/DNS-Aenderungen blockiert | Cloudflare-Konto; API-Token liegt als Host-Secret | API-Token rotierbar halten, Account-Recovery und Zone-Besitz pruefen |
|
| Cloudflare DNS | Authoritative DNS, ACME DNS-Challenge, DDNS | hoch | Neue Zertifikate/DNS-Aenderungen blockiert | Cloudflare-Konto; API-Token liegt als Host-Secret | API-Token rotierbar halten, Account-Recovery und Zone-Besitz pruefen |
|
||||||
| Hetzner Storage Box | Off-site Borg Backup | kritisch | Restore aus Off-site ggf. nicht moeglich | Hetzner-Konto / Storage-Box-Zugang ausserhalb Repo | Borg-Passphrase ist offline gesichert; Account-Hygiene und Borg `--append-only` als Haertung pruefen |
|
| Hetzner Storage Box | Off-site Borg Backup | kritisch | Restore aus Off-site ggf. nicht moeglich | Hetzner-Konto / Storage-Box-Zugang ausserhalb Repo | Borg-Passphrase ist offline gesichert; Hetzner 2FA/Recovery/Zahlung sind bestaetigt; Storage Box ist SSH-only, Maintenance-Key liegt in Vaultwarden; Borg `append-only` wird per Operator-Entscheidung nicht umgesetzt |
|
||||||
| GitHub Mirror | Externer Repo-Mirror `michaelkaleschke-spec/homelab-infra` | mittel/hoch | Gitea-Verlust abfederbar, Repo-Bootstrap bleibt moeglich | GitHub-Konto; PAT liegt in Gitea-Mirror-Settings, nicht im Repo | Mirror-Status regelmaessig pruefen; lokalen Clone als zweite Kopie behalten |
|
| GitHub Mirror | Externer Repo-Mirror `michaelkaleschke-spec/homelab-infra` (privat) | mittel/hoch | Gitea-Verlust abfederbar, aber Bare-Metal-Bootstrap braucht Read-Zugang (PAT oder SSH-Deploy-Key); ohne diesen ist der Mirror im DR nicht klonbar | GitHub-Konto; Push-PAT liegt in Gitea-Mirror-Settings; **Read-PAT/Deploy-Key fuer DR muss zusaetzlich offline im DR-Kit liegen** | Mirror-Status regelmaessig pruefen; lokalen Clone als zweite Kopie behalten; Read-PAT mit Scope `repo:read` separat erzeugen und im DR-Kit ablegen |
|
||||||
| Tailscale | Remote-/Operator-Zugang | hoch | Remote-Zugriff erschwert, lokale Bedienung bleibt | Tailnet-Konto; Node `Kallilabcore`, IPv4 `100.80.98.33` | Break-glass per LAN und physischem Zugriff; Tailnet-Recovery-Codes sichern |
|
| Tailscale | Remote-/Operator-Zugang | hoch | Remote-Zugriff erschwert, lokale Bedienung bleibt | Tailnet-Konto; Node `Kallilabcore`, IPv4 `100.80.98.33` | Break-glass per LAN und physischem Zugriff; Tailnet-Recovery-Codes sichern |
|
||||||
| GMX SMTP | Authelia Notifier | mittel | Mail-Notifier faellt aus, Login selbst nicht zwingend | GMX-Konto; SMTP-Secret liegt hostseitig | ntfy/zweiter SMTP als Fallback pruefen |
|
| GMX SMTP | Authelia Notifier, Vaultwarden-Einladungen, Ops-Report-Mail | mittel | Mail-Notifier und Vaultwarden-Einladungen fallen aus; Login selbst nicht zwingend | GMX-Konto; SMTP-Secrets liegen hostseitig | ntfy/zweiter SMTP als Fallback pruefen |
|
||||||
|
| OpenAI API | Paperless-GPT LLM und Vision-OCR | mittel | Automatische Dokument-Titel, Tags, Korrespondenten und LLM-OCR fallen aus; Paperless selbst laeuft weiter | OpenAI-Projekt/API-Key ausserhalb Repo | Key in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten |
|
||||||
| Let's Encrypt | TLS-Zertifikate | hoch | Cert-Erneuerung faellt aus | automatisch via Traefik und Cloudflare DNS-Challenge | Cert-Expiry Alert einrichten; Cloudflare-Token und Traefik-Storage pruefen |
|
| Let's Encrypt | TLS-Zertifikate | hoch | Cert-Erneuerung faellt aus | automatisch via Traefik und Cloudflare DNS-Challenge | Cert-Expiry Alert einrichten; Cloudflare-Token und Traefik-Storage pruefen |
|
||||||
| Container Registries | Image Pulls von Docker Hub, GHCR, LSCR, Gitea Registry u. a. | mittel | Redeploy/Update blockiert | ueberwiegend oeffentlich; keine produktiven Registry-Tokens im Repo | Gepinnte Digests und lokale Runtime helfen kurzfristig; Updates geplant und einzeln deployen |
|
| Container Registries | Image Pulls von Docker Hub, GHCR, LSCR, Gitea Registry u. a. | mittel | Redeploy/Update blockiert | ueberwiegend oeffentlich; keine produktiven Registry-Tokens im Repo | Gepinnte Digests und lokale Runtime helfen kurzfristig; Updates geplant und einzeln deployen |
|
||||||
| Plex Konto/Remote Access | Plex native Auth, ggf. Remote Access und Claim | mittel | Plex-Clients/Remote-Funktionen koennen ausfallen | Plex-Konto ausserhalb Repo; `PLEX_CLAIM` nur fuer Setup | LAN-Medienpfade bleiben lokal; Konto-Recovery separat sichern |
|
| Plex Konto | Plex native Auth, Claim und Client-Zugriff ueber `plex.kaleschke.info` | mittel | Plex-Web/App-Login und Clients koennen ausfallen; LAN-Medienpfade bleiben lokal | Plex-Konto ausserhalb Repo; `PLEX_CLAIM` nur fuer Setup | Plex Remote Access bleibt aus; externer Zugriff laeuft ueber Traefik/443. Konto-Recovery separat sichern |
|
||||||
| Mobile Push | ntfy und ggf. mobile Plattform-Pushes | niedrig/mittel | Alerts erreichen Mobilgeraete ggf. nicht | App-/Device-seitig | Kritische Alerts zusaetzlich in Grafana/Glance sichtbar halten |
|
| Mobile Push | ntfy und ggf. mobile Plattform-Pushes | niedrig/mittel | Alerts erreichen Mobilgeraete ggf. nicht | App-/Device-seitig | Kritische Alerts zusaetzlich in Grafana/Glance sichtbar halten |
|
||||||
|
| Operator-DR-Workstation | Bare-Metal-Recovery-Arbeitsplatz (Gaming-PC Windows, lokaler Repo-Clone `G:\Gitea_Clone\homelab-infra`) | kritisch | Ohne Workstation kein Borg-Extract, kein Hetzner-Zugriff, kein Repo-Bootstrap; der Unraid-Host ist im Bare-Metal-Fall gerade weg | Operator-PC, WSL2 + Borg-Client, SSH-Key fuer Hetzner Storage Box, Offline-Kopie der Borg-Passphrase | Setup als bewusste DR-Vorbedingung pflegen (siehe Abschnitt "DR-Workstation Bare-Metal-Kit") |
|
||||||
|
|
||||||
## Kritische Secrets ausserhalb des Repos
|
## Kritische Secrets ausserhalb des Repos
|
||||||
|
|
||||||
@@ -33,9 +35,28 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng
|
|||||||
| Cloudflare DNS API Token | ACME DNS-Challenge | Token-Rotation und Scope pruefen |
|
| Cloudflare DNS API Token | ACME DNS-Challenge | Token-Rotation und Scope pruefen |
|
||||||
| GitHub Mirror Token | Push-Mirror | In Gitea/GitHub verwaltet, nicht im Repo |
|
| GitHub Mirror Token | Push-Mirror | In Gitea/GitHub verwaltet, nicht im Repo |
|
||||||
| Tailscale Account Recovery | Tailnet-Zugang | Account-2FA/Recovery Codes sichern |
|
| Tailscale Account Recovery | Tailnet-Zugang | Account-2FA/Recovery Codes sichern |
|
||||||
| SMTP Passwort | Authelia Mail | In Host-Secret, Fallback pruefen |
|
| SMTP Passwort | Authelia Mail, Vaultwarden-Einladungen, Ops-Report-Mail | In Host-Secrets, Fallback pruefen |
|
||||||
| Domain-Registrar Recovery | Domain-Besitz und Zahlung | Account, 2FA und Zahlungsweg ausserhalb des Homelabs sichern |
|
| Domain-Registrar Recovery | Domain-Besitz und Zahlung | Account, 2FA und Zahlungsweg ausserhalb des Homelabs sichern |
|
||||||
| Hetzner Storage Box Zugang | Off-site Backup-Ziel | SSH-/Web-Zugang und Zahlungsweg extern sichern |
|
| Hetzner Storage Box Zugang | Off-site Backup-Ziel | Account 2FA aktiv, Recovery Key offline gedruckt, Zahlungsweg ok; Maintenance-Key und Storage-Box-Passwort in Vaultwarden |
|
||||||
|
| OpenAI API Key | Paperless-GPT GPT-Zugriff | Als Stack ENV / Vaultwarden-Eintrag sichern; bei Verdacht auf Leak rotieren |
|
||||||
|
| KOMODO_* Stack-ENV-Notiz | Offline-Sicherung der 5 Komodo-Werte (`KOMODO_SECRET_KEY`, `KOMODO_WEBHOOK_SECRET`, `KOMODO_JWT_SECRET`, `KOMODO_MONGO_PASSWORD`, `KOMODO_PERIPHERY_PASSKEY`) | **Status 2026-06-03: offline gesichert (Operator-Bestaetigung)**. Quelle der Werte ist die host-seitige Self-Stack-`.env` (`/mnt/user/services/stacks/komodo/.env`) bzw. die Drift-Recovery-Kopie unter `/mnt/user/appdata/secrets/_komodo_stack_env_recovery_2026-05-04.env`. Nicht im Repo, nicht in ntfy, nicht in Logs |
|
||||||
|
| GitHub-Mirror Read-Only Deploy-Key | DR-Read-Zugang zum privaten Mirror `michaelkaleschke-spec/homelab-infra` | **Status 2026-06-03: offline gesichert (Operator-Bestaetigung).** SSH-Deploy-Key `dr-readonly-2026-06-03` (ed25519, Passphrase-frei), Title in GitHub Repo Settings -> Deploy Keys: `DR Read-Only 2026-06-03`, Write-Access bewusst deaktiviert. Private Key liegt offline neben der KOMODO_*-Notiz. Smoke `git ls-remote` am 2026-06-03 erfolgreich. |
|
||||||
|
|
||||||
|
## DR-Workstation Bare-Metal-Kit
|
||||||
|
|
||||||
|
Der Operator-Gaming-PC ist im Bare-Metal-Fall die einzige Stelle, von der aus Recovery starten kann. Folgende Bestandteile gehoeren zum minimalen DR-Kit auf diesem Rechner:
|
||||||
|
|
||||||
|
| Bestandteil | Zweck | Pruefen |
|
||||||
|
|---|---|---|
|
||||||
|
| Repo-Clone `G:\Gitea_Clone\homelab-infra` (master, gefetcht) | Recovery-Anker fuer `ops/komodo/docker-compose.yml`, Restore-Skripte | `git -C G:\Gitea_Clone\homelab-infra log --oneline -1` plausibel aktuell |
|
||||||
|
| Read-Zugang zum privaten GitHub-Mirror | Fallback, falls lokaler Clone defekt | SSH-Deploy-Key `dr-readonly-2026-06-03` (ed25519, Passphrase-frei) offline im DR-Kit, ein Test-Clone pro Quartal mit `GIT_SSH_COMMAND="ssh -i <pfad-zum-key> -o IdentitiesOnly=yes" git ls-remote git@github.com:michaelkaleschke-spec/homelab-infra.git` |
|
||||||
|
| WSL2 mit Borg-Client (`apt install borgbackup`) | Borg-Extract von Hetzner Storage Box ohne laufenden Unraid-Host | `borg --version` antwortet; ein `borg list` gegen Hetzner-Repo laeuft |
|
||||||
|
| SSH-Key fuer Hetzner Storage Box | Login auf `u565255.your-storagebox.de:23` | **Status 2026-06-03: ed25519-DR-Key `dr-hetzner-2026-06-03` offline gesichert.** Pubkey via `install-ssh-key` auf der Storage Box autorisiert, passwortloser Login erfolgreich, `ls` zeigt vier Borg-Repos (`backup`, `backup2`, `hetzner_borg_appdata`, `hetzner_borg_appdata_critical`). Private Key liegt offline neben KOMODO_*-Notiz und GitHub-Deploy-Key |
|
||||||
|
| Offline-Kopie Borg-Passphrase | Entschluesselung des Borg-Repos | Operator-Bestaetigung 2026-05-26; bei Reviews nur Auffindbarkeit pruefen |
|
||||||
|
| Offline-Kopie KOMODO_* Stack-ENV | Komodo-Bootstrap ohne Vaultwarden | **Status 2026-06-03: offline gesichert (Operator-Bestaetigung)** |
|
||||||
|
| Vaultwarden Master-Passwort offline | Zugriff auf Vaultwarden-Export im DR | Operator-Wissen, ggf. analog gesichert |
|
||||||
|
|
||||||
|
Operative Regel: Die DR-Workstation wird nicht als Test-/Spiel-PC betrachtet. WSL und das DR-Kit duerfen nicht unbemerkt unbrauchbar werden. Quartalsweise minimaler Trockenlauf: `borg list <hetzner-repo>` muss antworten und der Repo-Clone muss fetchbar bleiben.
|
||||||
|
|
||||||
## Ausfall-Szenarien
|
## Ausfall-Szenarien
|
||||||
|
|
||||||
@@ -64,7 +85,7 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng
|
|||||||
- Lokale LAN-Apps (Plex, AdGuard-DNS, lokales Borg-Dump-Repository) bleiben verfuegbar, solange Host und Switch laufen.
|
- 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.
|
- 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.
|
- 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.
|
- FRITZ!OS ist am 2026-06-01 auf 8.25 (`154.08.25`) beobachtet; weitere Updates nur in einem geplanten Service-Fenster einspielen, weil Reboot WAN/Tailscale-Aufbau unterbricht.
|
||||||
|
|
||||||
### Domain verloren oder Registrar-Zugriff verloren
|
### Domain verloren oder Registrar-Zugriff verloren
|
||||||
|
|
||||||
@@ -76,5 +97,14 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng
|
|||||||
| Datum | Ergebnis | Naechste Aktion |
|
| Datum | Ergebnis | Naechste Aktion |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 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 | 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 | FRITZ!OS-Update im Service-Fenster pruefen |
|
| 2026-05-26 | Telekom-DSL und FRITZ!Box 7590 (damals FRITZ!OS 8.21) als WAN-/Router-Abhaengigkeit aufgenommen; Ausfallschutz nicht eingerichtet | FRITZ!OS-Update am 2026-06-01 als `154.08.25` beobachtet |
|
||||||
| 2026-05-28 | FRITZ!Box-Portfreigaben bereinigt: aktiv bleibt nur `443/tcp`; `80/tcp` entfernt, `222/tcp` bewusst nicht angelegt; UPnP-Recht fuer VONETS-Bridge deaktiviert | IPv6-Exposure bei naechstem WAN-/Router-Review pruefen |
|
| 2026-05-28 | FRITZ!Box-Portfreigaben bereinigt: aktiv bleibt nur `443/tcp`; `80/tcp` entfernt, `222/tcp` bewusst nicht angelegt; UPnP-Recht fuer VONETS-Bridge deaktiviert | IPv6-/Dienste-Review am 2026-06-01 nachgezogen |
|
||||||
|
| 2026-06-01 | Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; FRITZ!Box meldet per TR-064 FRITZ!OS `154.08.25`, Public DNS hat keine AAAA-Records, Host hat keine globale Provider-IPv6 | Account-Hygiene am 2026-06-01 nachgezogen |
|
||||||
|
| 2026-06-01 | FRITZ!Box-UI gegengeprueft und Konfig-Backup extern/off-system in Vaultwarden abgelegt; Remote-HTTPS auf FRITZ!Box-UI aus, FTP/FTPS auf Speichermedien aus, nur `443/tcp -> 192.168.178.58`, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus | Bei naechstem Router-Update erneut exportieren |
|
||||||
|
| 2026-06-01 | Hetzner-Account-Hygiene erledigt: externe Mail ok, Zahlung ok, 2FA aktiv, Recovery Key offline gedruckt. Storage Box: SSH aktiv, SMB/WebDAV aus, Maintenance-Key in Vaultwarden, Borg-Repo-Zugriff nach Recovery geprueft. Borg `append-only` wird bewusst nicht umgesetzt. | Keine Folgeaktion |
|
||||||
|
| 2026-06-03 | Hetzner Storage Box Maintenance-Key zusaetzlich offline gesichert bestaetigt (Operator-Antwort im DR-Tabletop 2026-06-03). Damit ist der Hetzner-Zugang im Bare-Metal-Fall ohne Vaultwarden moeglich. | Keine Folgeaktion |
|
||||||
|
| 2026-06-03 | DR-Tabletop ergibt drei offene Bootstrap-Bloecke: KOMODO_*-Notiz nicht offline, GitHub-Mirror-Read-PAT/Deploy-Key nicht angelegt, DR-Workstation nicht als DR-Kit konfiguriert. Details in `docs/DR_DRILL_2026-06-03.md` und Folge-Tasks in `docs/AUDIT_2026-05-25_TODO.md`. | KOMODO_*-Notiz erzeugen, Read-PAT erzeugen, WSL2+Borg auf Gaming-PC einrichten |
|
||||||
|
| 2026-06-03 | KOMODO_*-Notiz offline gesichert (Operator-Bestaetigung im DR-Tabletop-Followup). Quelle bleibt host-seitige `.env` (`/mnt/user/services/stacks/komodo/.env`) bzw. Drift-Recovery-Kopie vom 2026-05-04. Bare-Metal-Komodo-Bootstrap ist damit ohne Vaultwarden moeglich. | Restliche P1-Operator-Aufgaben: GitHub-Read-PAT, DR-Workstation-Setup, Nextcloud-Restore-Test |
|
||||||
|
| 2026-06-03 | GitHub-Mirror Read-Only Deploy-Key `DR Read-Only 2026-06-03` (ed25519, Passphrase-frei) erzeugt, in GitHub Repo Settings ohne Write-Access hinterlegt, Smoke `git ls-remote` erfolgreich (`d947c7f` matched master HEAD), Private-Key offline neben KOMODO_*-Notiz abgelegt, Arbeitsplatz-Kopie geloescht. | Restliche P1-Operator-Aufgaben: DR-Workstation-Setup, Nextcloud-Restore-Test |
|
||||||
|
| 2026-06-03 | Hetzner Storage Box DR-SSH-Key `dr-hetzner-2026-06-03` (ed25519, Passphrase-frei) erzeugt, via `install-ssh-key` auf Storage Box `u565255.your-storagebox.de:23` autorisiert, passwortloser Login erfolgreich (Borg-Repos sichtbar), Private-Key offline neben KOMODO_*-Notiz und GitHub-Deploy-Key abgelegt, Arbeitsplatz-Kopie geloescht. Bare-Metal-Borg-Restore von der DR-Workstation ist damit moeglich, sobald WSL2 + Borg-Client installiert sind. | Restliche P1-Operator-Aufgaben: WSL2 + Borg-Client auf DR-Workstation installieren, Nextcloud-Restore-Test |
|
||||||
|
| 2026-06-06 | DR-Workstation produktiv: WSL2 Ubuntu 24.04 vorhanden, SSH/Git und Borg 1.2.8 in WSL vorhanden, DR-Key-Arbeitskopien unter `~/.ssh/dr-readonly` und `~/.ssh/dr-hetzner`, GitHub-Read-Smoke und Hetzner-SSH-Smoke erfolgreich, `ops/maintenance/dr-workstation-smoke.sh` nach `~/dr-smoke.sh` kopiert. Finaler Operator-Smoke erfolgreich: GitHub HEAD `3a263a4...`, Hetzner Storage Box Repos sichtbar, Borg-Repo `hetzner_borg_appdata_critical` gelesen, Repository ID `5dd9b949...`, encrypted `Yes (repokey)`, `DR-Smoke OK (2026-06-06 10:05:30)`. | Quartalsweise `bash ~/dr-smoke.sh`; Borg-Passphrase weiterhin nur interaktiv eingeben und nicht speichern |
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
# External Operator Runbook
|
||||||
|
|
||||||
|
Stand: 2026-06-01
|
||||||
|
|
||||||
|
Dieses Runbook schliesst die Betreiber-Aufgaben, die nicht vollstaendig aus dem
|
||||||
|
Repo automatisierbar sind: Hetzner-Account-Hygiene, Borg-Append-Only und
|
||||||
|
FRITZ!Box-Servicefenster. Keine Secret-Werte ins Repo schreiben.
|
||||||
|
|
||||||
|
## 1. Vorher pruefen
|
||||||
|
|
||||||
|
Auf dem Unraid-Host:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/maintenance/check-external-operator.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwarteter Stand vom 2026-06-01:
|
||||||
|
|
||||||
|
- FRITZ!Box 7590 meldet FRITZ!OS `154.08.25`.
|
||||||
|
- FRITZ!Box IPv6-Firewall meldet `FirewallEnabled=1`; `InboundPinholeAllowed=1` bedeutet, dass IPv6-Freigaben technisch moeglich sind und in der UI gegengeprueft werden muessen.
|
||||||
|
- Public DNS fuer `*.kaleschke.info` liefert A-Records auf `217.249.115.154`, keine AAAA-Records.
|
||||||
|
- Host hat keine globale Provider-IPv6-Adresse; sichtbar ist nur Tailscale-IPv6 `fd7a:115c:a1e0::2c01:62b2`.
|
||||||
|
- WAN-Smoke gegen die Public-IP: `443/tcp` offen, `80/tcp` und `222/tcp` geschlossen.
|
||||||
|
- FRITZ!Box-UI-Gegencheck vom 2026-06-01: Remote-HTTPS auf die FRITZ!Box ist aus, FTP/FTPS auf Speichermedien ist aus, nur `443/tcp -> 192.168.178.58` ist als WAN-Freigabe sichtbar, keine aktive IPv6-Freigabe sichtbar, UPnP-Selbstfreigaben aus.
|
||||||
|
- FRITZ!Box-Konfig-Backup vom 2026-06-01 ist extern/off-system in Vaultwarden abgelegt; Datei und Kennwort nicht ins Repo schreiben.
|
||||||
|
- Borg UI nutzt `borg 1.4.x`; Repository `appdata-critical` liegt auf Hetzner Storage Box `ssh://...your-storagebox.de:23/./hetzner_borg_appdata_critical`.
|
||||||
|
- Hetzner-Account-Hygiene vom 2026-06-01: 2FA aktiv, Recovery Key offline gedruckt, Zahlung ok.
|
||||||
|
- Storage Box vom 2026-06-01: SSH aktiv, SMB/WebDAV aus, separater Maintenance-Key in Vaultwarden, produktiver Borg-UI-Key und Maintenance-Key nach Passwort-Recovery getestet.
|
||||||
|
- Restore-Freshness: `Critical 0`, `Warnings 0`.
|
||||||
|
|
||||||
|
## 2. Hetzner Account-Hygiene
|
||||||
|
|
||||||
|
Im Hetzner-/Storage-Box-Konto pruefen und extern/off-system dokumentieren:
|
||||||
|
|
||||||
|
| Punkt | Soll |
|
||||||
|
|---|---|
|
||||||
|
| Passwort | stark, eindeutig, im Passwortmanager |
|
||||||
|
| 2FA | aktiv, Recovery Key offline auffindbar |
|
||||||
|
| Kontakt-E-Mail | aktuell und ohne Homelab-Abhaengigkeit erreichbar |
|
||||||
|
| Zahlungsweg | gueltig, Fallback bekannt |
|
||||||
|
| Storage Box | Produkt, Benutzer und Rechnungsstatus sichtbar |
|
||||||
|
| SSH/SFTP/WebDAV/SMB | nur benoetigte Protokolle aktiv |
|
||||||
|
| Recovery | Kundennummer, Login-Pfad und Support-Pfad extern notiert |
|
||||||
|
|
||||||
|
Im Repo nur das Datum der Bestaetigung dokumentieren, nie Zugangsdaten.
|
||||||
|
|
||||||
|
## 3. Borg Append-Only
|
||||||
|
|
||||||
|
Status: **bewusst nicht umgesetzt**.
|
||||||
|
|
||||||
|
Ziel der Haertung waere gewesen: Der produktive Backup-Client darf neue Archive
|
||||||
|
schreiben, aber nicht normal prune/delete/compact als unbeschraenkter Client
|
||||||
|
ausfuehren.
|
||||||
|
|
||||||
|
Hetzner dokumentiert Borg-Zugriff auf Storage Boxen inklusive `--remote-path`
|
||||||
|
fuer Borg-Versionen; fuer Borg 1.4 wird `--remote-path=borg-1.4` empfohlen.
|
||||||
|
Hetzner bestaetigt auch, dass append-only moeglich ist. Borg selbst setzt
|
||||||
|
append-only pro SSH-Key typischerweise ueber einen forced command in
|
||||||
|
`authorized_keys` um.
|
||||||
|
|
||||||
|
Getestetes Zielmodell, aber **nicht auf der produktiven Storage Box aktiv**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
command="borg-1.4 serve --append-only --restrict-to-repository /home/hetzner_borg_appdata_critical",restrict ssh-ed25519 <backup-public-key> borg-ui-append-only
|
||||||
|
ssh-ed25519 <maintenance-public-key> borg-maintenance
|
||||||
|
```
|
||||||
|
|
||||||
|
Hinweise:
|
||||||
|
|
||||||
|
- Stand 2026-06-01: Ein forced-command-Versuch auf der produktiven
|
||||||
|
Storage-Box-`authorized_keys` brach die Key-Authentifizierung. Recovery
|
||||||
|
erfolgte per Storage-Box-Passwort und Upload einer bereinigten
|
||||||
|
`authorized_keys` mit Borg-UI-Key und Maintenance-Key.
|
||||||
|
- Operator-Entscheidung 2026-06-01: Append-only wird fuer dieses Homelab nicht
|
||||||
|
umgesetzt. Der zusaetzliche Schutz steht hier nicht im Verhaeltnis zum
|
||||||
|
Betriebsrisiko und zur Komplexitaet.
|
||||||
|
- Pfad auf der Storage Box vor dem Eintragen pruefen. Bei Hetzner werden Pfade
|
||||||
|
im Borg-Repo haeufig relativ als `./repo-name` verwendet; in
|
||||||
|
`authorized_keys` muss der serverseitige Pfad zur Storage-Box-Home-Struktur
|
||||||
|
passen.
|
||||||
|
- Der produktive Borg-UI-Key bleibt bewusst uneingeschraenkt, damit die
|
||||||
|
produktiven Backups laufen.
|
||||||
|
- Ein separater Maintenance-Key bleibt fuer bewusste Retention/Prune/Compact
|
||||||
|
noetig und liegt in Vaultwarden; lokale temporare Key-Dateien wurden geloescht.
|
||||||
|
- Append-only verhindert nicht, dass ein kompromittierter Client Archive als
|
||||||
|
geloescht markiert; es verhindert die unmittelbare physische Entfernung.
|
||||||
|
Nach einem Vorfall keine unbeschraenkte Schreiboperation ausfuehren, bevor
|
||||||
|
die Borg-Transaktionen bewertet wurden.
|
||||||
|
|
||||||
|
Nach Aenderung:
|
||||||
|
|
||||||
|
1. Einen regulaeren Borg-Lauf abwarten oder manuell starten.
|
||||||
|
2. `check-external-operator.sh` ausfuehren.
|
||||||
|
3. In `docs/AUDIT_2026-05-25_TODO.md` nur das Ergebnis dokumentieren.
|
||||||
|
|
||||||
|
## 4. FRITZ!Box-Servicefenster
|
||||||
|
|
||||||
|
Vor dem Fenster:
|
||||||
|
|
||||||
|
1. Familie informieren: Internet/Telefonie koennen kurz weg sein.
|
||||||
|
2. Aktuellen Repo-Stand und Borg-Freshness pruefen.
|
||||||
|
3. FRITZ!Box-Konfig exportieren: `System -> Sicherung -> Sichern`.
|
||||||
|
4. Sicherungsdatei nicht ins Repo legen; im Passwortmanager/off-system ablegen.
|
||||||
|
|
||||||
|
In der FRITZ!Box:
|
||||||
|
|
||||||
|
| Bereich | Soll |
|
||||||
|
|---|---|
|
||||||
|
| `System -> Update` | FRITZ!OS aktuell; am 2026-06-01 per TR-064 `154.08.25` beobachtet |
|
||||||
|
| `Internet -> Freigaben -> Portfreigaben` | nur `443/tcp -> 192.168.178.58:443` |
|
||||||
|
| `Internet -> Freigaben -> FRITZ!Box-Dienste` | Remote-HTTPS auf FRITZ!Box-UI aus; FTP/FTPS auf Speichermedien aus |
|
||||||
|
| IPv6-Portfreigaben | keine aktiven Freigaben; insbesondere kein `222/tcp`, kein Admin-Port |
|
||||||
|
| Selbststaendige Portfreigaben/UPnP | fuer `Kallilabcore` aus; neue Geraete nur bewusst erlauben |
|
||||||
|
| Gastnetz | bleibt aus, solange keine Gastnetz-Policy gepflegt wird |
|
||||||
|
| Ausfallschutz | bewusst aus; nur neu bewerten, wenn ein Mobilfunk-Fallback gewuenscht ist |
|
||||||
|
|
||||||
|
Nach dem Fenster:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/maintenance/check-external-operator.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Dann in `docs/NETWORK_INVENTORY.md` aktualisieren:
|
||||||
|
|
||||||
|
- FRITZ!OS-Version
|
||||||
|
- IPv6-Status
|
||||||
|
- aktive Portfreigaben
|
||||||
|
- FRITZ!Box-Dienste aus dem Internet
|
||||||
|
- Datum der Konfig-Sicherung
|
||||||
|
|
||||||
|
## Quellen
|
||||||
|
|
||||||
|
- Hetzner Docs: Storage Box Zugriff mit SSH/rsync/BorgBackup, inklusive
|
||||||
|
Borg-Versionen, `--remote-path` und Append-Only-Hinweis:
|
||||||
|
<https://docs.hetzner.com/storage/storage-box/access/access-ssh-rsync-borg/>
|
||||||
|
- BorgBackup Docs: `borg serve --append-only` und forced commands in
|
||||||
|
`authorized_keys`:
|
||||||
|
<https://borgbackup.readthedocs.io/en/stable/deployment/pull-backup.html>
|
||||||
|
- AVM FRITZ!Box Hilfe: IPv6-Portfreigaben werden separat verwaltet; eingehende
|
||||||
|
Zugriffe sind standardmaessig nicht offen:
|
||||||
|
<https://help.avm.de/fritzbox.php?hardware=145&language=en&oem=avme&set=009&topic=hilfe_internet_freigabe_ipv6>
|
||||||
|
- AVM FRITZ!Box Hilfe: Sicherung der FRITZ!Box-Einstellungen:
|
||||||
|
<https://help.avm.de/fritzbox.php?hardware=145&language=en&oem=avme&set=009&topic=hilfe_system_export>
|
||||||
+121
-14
@@ -1,6 +1,6 @@
|
|||||||
# Familien-Willkommen - KalliLab CORE
|
# Familien-Willkommen - KalliLab CORE
|
||||||
|
|
||||||
Status: **Final-Stand vor Wochenend-Einladung** (2026-05-27). Zielgruppe: Familie. Kein Technik-Wortschatz noetig.
|
Status: **Praxis-Onboarding** (2026-06-01). Zielgruppe: Familie. Kein Technik-Wortschatz noetig.
|
||||||
|
|
||||||
Diese Seite richtet sich an alle, die zuhause unsere eigenen Apps nutzen. Du brauchst kein Technikwissen. Wenn etwas unklar ist: einfach Michi fragen.
|
Diese Seite richtet sich an alle, die zuhause unsere eigenen Apps nutzen. Du brauchst kein Technikwissen. Wenn etwas unklar ist: einfach Michi fragen.
|
||||||
|
|
||||||
@@ -25,12 +25,44 @@ Nachteile, ehrlich gesagt: Wenn der Server zuhause aus ist, sind die Apps weg, b
|
|||||||
| **Vaultwarden** | Passwoerter sicher speichern und auf jedem Geraet nachschauen | Bitwarden-App (kostenlos), beim ersten Start Server-URL auf `vault.kaleschke.info` aendern 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 |
|
| **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 |
|
| **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 |
|
| **Plex** | Filme und Musik auf Fernseher, Handy und Tablet | Web `https://plex.kaleschke.info` oder 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.
|
> 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Unser erster gemeinsamer Ablauf
|
||||||
|
|
||||||
|
Wir richten nicht alles auf einmal perfekt ein. Wichtig ist, dass drei Dinge
|
||||||
|
wirklich benutzt werden:
|
||||||
|
|
||||||
|
1. **Vaultwarden**: Passwoerter landen dort, nicht im Browser.
|
||||||
|
2. **Immich**: Handy-Fotos werden automatisch gesichert.
|
||||||
|
3. **Mealie**: Rezepte und Einkaufsliste werden gemeinsam ausprobiert.
|
||||||
|
|
||||||
|
Wenn diese drei Dinge laufen, ist das Familien-Onboarding praktisch erfolgreich.
|
||||||
|
|
||||||
|
## Vaultwarden zuerst
|
||||||
|
|
||||||
|
Vaultwarden ist die Grundlage fuer alle anderen Logins.
|
||||||
|
|
||||||
|
1. App **Bitwarden Passwortmanager** auf Handy und ggf. PC installieren.
|
||||||
|
2. Beim ersten Start die Server-URL auf `https://vault.kaleschke.info` setzen.
|
||||||
|
3. Mit dem persoenlichen Konto anmelden.
|
||||||
|
4. Master-Passwort gemeinsam festlegen und merken. Das Master-Passwort wird
|
||||||
|
nicht bei Michi gespeichert.
|
||||||
|
5. Browser-Passwortspeicher fuer neue Homelab-Passwoerter nicht verwenden.
|
||||||
|
6. Test: Einen neuen Eintrag "Test KalliLab" anlegen und wiederfinden.
|
||||||
|
|
||||||
|
Was in Vaultwarden gehoert:
|
||||||
|
|
||||||
|
- Homelab-App-Passwoerter
|
||||||
|
- wichtige Familien-Logins
|
||||||
|
- Recovery-Codes, wenn eine App welche zeigt
|
||||||
|
- keine losen Passwort-Zettel und keine Screenshots von Passwoertern
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Wie du dich anmeldest
|
## Wie du dich anmeldest
|
||||||
|
|
||||||
Beim ersten Mal bekommst du von Michi:
|
Beim ersten Mal bekommst du von Michi:
|
||||||
@@ -57,13 +89,40 @@ Das ist die App, die deinen Eltern wahrscheinlich am meisten bringt.
|
|||||||
1. App **Immich** im App-Store / Play Store installieren.
|
1. App **Immich** im App-Store / Play Store installieren.
|
||||||
2. Beim ersten Start nach Server fragen lassen: `https://immich.kaleschke.info`.
|
2. Beim ersten Start nach Server fragen lassen: `https://immich.kaleschke.info`.
|
||||||
3. Mit deinem Login anmelden.
|
3. Mit deinem Login anmelden.
|
||||||
4. In den App-Einstellungen "Hintergrund-Backup" aktivieren — am besten nur ueber WLAN.
|
4. In den App-Einstellungen "Hintergrund-Backup" aktivieren - am besten nur
|
||||||
5. Fertig. Neue Fotos landen automatisch zuhause auf dem Server.
|
ueber WLAN.
|
||||||
|
5. Das richtige Album / die normale Kamera-Galerie auswaehlen.
|
||||||
|
6. App einmal offen lassen, bis erste Fotos hochgeladen wurden.
|
||||||
|
7. Test: In der Weboberflaeche `https://immich.kaleschke.info` pruefen, ob die
|
||||||
|
ersten Fotos sichtbar sind.
|
||||||
|
8. 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.
|
> 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Rezepte und Einkaufsliste einrichten (Mealie)
|
||||||
|
|
||||||
|
Mealie soll nicht nur "auch da" sein, sondern wirklich genutzt werden.
|
||||||
|
|
||||||
|
1. `https://mealie.kaleschke.info` oeffnen.
|
||||||
|
2. Mit dem persoenlichen Konto anmelden.
|
||||||
|
3. Gemeinsam ein erstes echtes Rezept anlegen oder importieren.
|
||||||
|
4. Rezept mindestens einer Kategorie geben, z. B. `Alltag`, `Schnell`,
|
||||||
|
`Wochenende`, `Vegetarisch`.
|
||||||
|
5. Aus dem Rezept Zutaten auf die Einkaufsliste setzen.
|
||||||
|
6. Test: Einkaufsliste auf dem Handy oeffnen und einen Eintrag abhaken.
|
||||||
|
7. Optional: Einen Wochenplan fuer die naechsten 2-3 Tage anlegen.
|
||||||
|
|
||||||
|
Start-Regeln fuer Mealie:
|
||||||
|
|
||||||
|
- Rezepte nur dann speichern, wenn wir sie wirklich kochen wuerden.
|
||||||
|
- Namen schlicht halten: `Chili`, `Bolognese`, `Kartoffelsuppe`.
|
||||||
|
- Zutaten so eintragen, dass sie beim Einkaufen verstaendlich sind.
|
||||||
|
- Wenn ein Rezept nicht schmeckt: loeschen oder klar als "nicht wieder" markieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Was tun, wenn etwas nicht geht
|
## Was tun, wenn etwas nicht geht
|
||||||
|
|
||||||
### "Die Webseite oeffnet nicht."
|
### "Die Webseite oeffnet nicht."
|
||||||
@@ -134,18 +193,66 @@ Michi laesst es dich wissen, wenn ein Wartungsfenster geplant ist.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Offene Inhalte (Operator-Notiz)
|
## Erster Onboarding-Termin - Ablauf fuer Michi
|
||||||
|
|
||||||
Diese Punkte gehoeren in das Wochenend-Onboarding-Gespraech und sind nicht Teil dieser Familien-Seite:
|
Diese Sektion ist die konkrete Checkliste fuer den **ersten echten
|
||||||
|
Familien-Onboarding-Termin**. Sie ist als ein zusammenhaengender Termin von
|
||||||
|
ca. 30-45 Minuten pro Person gedacht. Keine Secret-Werte in diese Datei
|
||||||
|
schreiben.
|
||||||
|
|
||||||
| Status | Aufgabe |
|
> Operator-Eingabe vor dem Termin: festlegen, **wer** beim ersten Termin dabei
|
||||||
|---|---|
|
> ist und **welche Geraete** real vorliegen. Die Checkliste funktioniert pro
|
||||||
| offen | Pro Familien-Konto Benutzernamen und Start-Passwort persoenlich uebergeben (Vaultwarden Familien-Organisation als Uebergabeweg) |
|
> Person identisch.
|
||||||
| offen | 2FA-App-Empfehlung pro Person festlegen (zum Beispiel Bitwarden Authenticator, Aegis, 2FAS) |
|
|
||||||
| offen | Vaultwarden Familien-Organisation einrichten und Mitglieder einladen |
|
### Vorher bereitlegen (Operator-Vorbereitung)
|
||||||
| offen | Immich Mobile Backup mit jedem Familien-Geraet einmal gemeinsam ausprobieren |
|
|
||||||
| offen | Scan-/Inbox-Anleitung fuer Paperless ergaenzen, sobald der Workflow final ist |
|
Diese Dinge muessen **vor** dem Termin fertig sein, sonst stockt der Ablauf:
|
||||||
| offen | Einladungstermin Wochenende mit konkretem Datum festlegen |
|
|
||||||
|
- [ ] Pro Teilnehmer ist in **Vaultwarden** ein Benutzerkonto angelegt (Benutzername = Vorname klein).
|
||||||
|
- [ ] Pro Teilnehmer ist in **Immich** ein Benutzerkonto angelegt.
|
||||||
|
- [ ] Pro Teilnehmer ist in **Mealie** ein Benutzerkonto angelegt.
|
||||||
|
- [ ] Start-Passwoerter sind erzeugt und liegen so bereit, dass sie persoenlich uebergeben werden koennen (nicht per Chat, nicht in diese Datei).
|
||||||
|
- [ ] Die Apps `cloud`, `immich`, `vault`, `mealie` sind erreichbar (kurzer eigener Smoke-Test ueber `https://...kaleschke.info`).
|
||||||
|
- [ ] Das Familien-Handy/Geraet jedes Teilnehmers ist da, entsperrt und im **Haus-WLAN**.
|
||||||
|
- [ ] App-Store-/Play-Store-Login auf dem Geraet funktioniert (zum Installieren der Apps).
|
||||||
|
|
||||||
|
### Reihenfolge beim Termin (pro Person)
|
||||||
|
|
||||||
|
Die Reihenfolge ist bewusst gewaehlt: erst der Passwort-Speicher, dann das, was
|
||||||
|
am meisten bringt (Fotos), dann das Gemeinsame (Rezepte).
|
||||||
|
|
||||||
|
1. **Konto-Uebergabe**: Benutzername + Start-Passwort persoenlich uebergeben, Person aendert das Passwort beim ersten Login.
|
||||||
|
2. **Vaultwarden / Bitwarden** (Abschnitt "Vaultwarden zuerst"):
|
||||||
|
- Bitwarden-App installieren, Server-URL `https://vault.kaleschke.info` setzen, anmelden.
|
||||||
|
- Master-Passwort gemeinsam festlegen (wird **nicht** bei Michi gespeichert).
|
||||||
|
- Testeintrag "Test KalliLab" anlegen und wiederfinden.
|
||||||
|
3. **Immich** (Abschnitt "Foto-Backup vom Handy einrichten"):
|
||||||
|
- Immich-App installieren, Server `https://immich.kaleschke.info`, anmelden.
|
||||||
|
- Hintergrund-Backup nur ueber WLAN aktivieren, Kamera-Album auswaehlen.
|
||||||
|
- App offen lassen, bis erste Fotos hochgeladen sind; in der Weboberflaeche sichtbar pruefen.
|
||||||
|
4. **Mealie** (Abschnitt "Rezepte und Einkaufsliste einrichten"):
|
||||||
|
- `https://mealie.kaleschke.info` anmelden.
|
||||||
|
- Gemeinsam ein erstes echtes Rezept anlegen, kategorisieren, Zutaten auf die Einkaufsliste setzen.
|
||||||
|
- Einkaufsliste auf dem Handy oeffnen und einen Eintrag abhaken.
|
||||||
|
5. **Abschluss**: kurz zeigen, was bei Problemen zu tun ist (Abschnitt "Was tun, wenn etwas nicht geht"), besonders Passwort-vergessen und 2FA-verloren.
|
||||||
|
|
||||||
|
### Erfolgskriterium des ersten Termins
|
||||||
|
|
||||||
|
Der Termin gilt als erfolgreich, wenn pro Person **diese drei** Dinge real laufen:
|
||||||
|
|
||||||
|
- [ ] Vaultwarden ist eingerichtet und ein Testeintrag wurde gefunden.
|
||||||
|
- [ ] Immich sichert Handy-Fotos und die ersten Fotos sind in der Weboberflaeche sichtbar.
|
||||||
|
- [ ] In Mealie existiert ein erstes Rezept mit einer Einkaufslisten-Position.
|
||||||
|
|
||||||
|
### Bewusst spaeter (nicht im ersten Termin)
|
||||||
|
|
||||||
|
Damit der erste Termin nicht ueberladen wird, kommen diese Punkte bewusst erst
|
||||||
|
in einem Folgetermin:
|
||||||
|
|
||||||
|
- **Nextcloud** (Dateien/Kalender/Adressbuch) - erst wenn die drei Kern-Apps sitzen.
|
||||||
|
- **Paperless** (Dokumente scannen) - braucht eigenen Scan-Workflow, separater Termin.
|
||||||
|
- **Plex** (Filme/Musik) - reines Komfort-Thema, kein Onboarding-Kern.
|
||||||
|
- **App-uebergreifendes Einheits-Login (SSO/OIDC)** - nicht eingerichtet, nur als Idee notiert (siehe "Bewusst nicht versprochen").
|
||||||
|
|
||||||
## Bewusst nicht versprochen
|
## Bewusst nicht versprochen
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Guest / IoT Network Runbook
|
||||||
|
|
||||||
|
Stand: 2026-06-06
|
||||||
|
|
||||||
|
Dieses Runbook beschreibt den sicheren Weg, das FRITZ!Box-Gastnetz zu aktivieren,
|
||||||
|
ohne versehentlich Homelab-Admin-Ports aus dem Gastsegment erreichbar zu machen.
|
||||||
|
|
||||||
|
## Zielbild
|
||||||
|
|
||||||
|
- Normales LAN bleibt `192.168.178.0/24`.
|
||||||
|
- Kallilabcore bleibt im normalen LAN unter `192.168.178.58`.
|
||||||
|
- FRITZ!Box-Gast-WLAN darf Internetzugang haben, aber keinen Zugriff auf
|
||||||
|
`192.168.178.0/24`.
|
||||||
|
- Homelab-Admin-Pfade bleiben Operator-only:
|
||||||
|
- Tailscale fuer Admin-Zugriff
|
||||||
|
- Authelia/2FA fuer geschuetzte Web-UIs
|
||||||
|
- keine LAN-Admin-Ports aus dem Gastnetz
|
||||||
|
|
||||||
|
## Vorbedingungen
|
||||||
|
|
||||||
|
Vor dem Einschalten des Gast-WLANs muessen diese Preflights gruen sein:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
G:\Gitea_Clone\homelab-infra\ops\maintenance\check-guest-iot-isolation.ps1 -Mode LanPreflight
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung im normalen LAN:
|
||||||
|
|
||||||
|
- `192.168.178.58:8082` ist blockiert (AdGuard Admin nur Tailscale).
|
||||||
|
- `192.168.178.58:8181` ist blockiert (InfluxDB nicht LAN-exponiert).
|
||||||
|
- `192.168.178.58:80`, `443`, `222` koennen im normalen LAN erreichbar sein.
|
||||||
|
|
||||||
|
Auf Unraid zusaetzlich:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mnt/user/services/homelab-infra/ops/maintenance/check-guest-iot-preflight.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Validierung 2026-06-06: Host-Preflight erfolgreich, Report
|
||||||
|
`/mnt/user/backups/restore-reports/guest-iot-preflight-2026-06-06-131316.md`.
|
||||||
|
Ergebnis: FRITZ!Box 7590 per TR-064 erreichbar, `192.168.178.58:8082`
|
||||||
|
blockiert, `100.80.98.33:8082` erreichbar, `192.168.178.58:8181` blockiert.
|
||||||
|
|
||||||
|
Gast-WLAN-Smoke 2026-06-06: Operator hat ein iPhone mit `Fritzi Gastzugang`
|
||||||
|
verbunden und folgende Ziele getestet; alle waren aus dem Gast-WLAN nicht
|
||||||
|
erreichbar:
|
||||||
|
|
||||||
|
- `http://192.168.178.58:8082`
|
||||||
|
- `http://192.168.178.58:8181`
|
||||||
|
- `http://192.168.178.58:222`
|
||||||
|
- `https://192.168.178.58`
|
||||||
|
- `http://192.168.178.1`
|
||||||
|
|
||||||
|
Damit ist die Gastnetz-Isolation fuer die getesteten Homelab-/Router-Adminpfade
|
||||||
|
validiert.
|
||||||
|
|
||||||
|
## FRITZ!Box Schritte
|
||||||
|
|
||||||
|
In der FRITZ!Box UI:
|
||||||
|
|
||||||
|
1. `WLAN -> Gastzugang` oeffnen.
|
||||||
|
2. `Gastzugang aktiv` einschalten.
|
||||||
|
3. WPA2/WPA3-Verschluesselung aktiv lassen.
|
||||||
|
4. Eigenen Gast-SSID-Namen setzen, z. B. `Fritzi-Gast`.
|
||||||
|
5. Starkes Passwort setzen und in Vaultwarden ablegen.
|
||||||
|
6. Option `Geraete im Gastnetz duerfen miteinander kommunizieren` deaktiviert
|
||||||
|
lassen, sofern nicht bewusst gebraucht.
|
||||||
|
7. Option fuer Zugriff auf das Heimnetz / private Netzwerk deaktiviert lassen.
|
||||||
|
8. Gastzugang speichern.
|
||||||
|
|
||||||
|
Wichtig: Die genaue FRITZ!OS-8.25-UI-Beschriftung kann leicht variieren. Der
|
||||||
|
entscheidende Punkt ist: Gastgeraete duerfen keinen Zugriff auf das Heimnetz
|
||||||
|
haben.
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Ein Handy oder Laptop mit dem Gast-WLAN verbinden, dann auf diesem Geraet testen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
G:\Gitea_Clone\homelab-infra\ops\maintenance\check-guest-iot-isolation.ps1 -Mode Guest
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung aus dem Gast-WLAN:
|
||||||
|
|
||||||
|
- `192.168.178.58:80` blockiert
|
||||||
|
- `192.168.178.58:443` blockiert
|
||||||
|
- `192.168.178.58:222` blockiert
|
||||||
|
- `192.168.178.58:8082` blockiert
|
||||||
|
- `192.168.178.58:8181` blockiert
|
||||||
|
- `192.168.178.1:80` blockiert oder nur Gast-Gateway-Ansicht
|
||||||
|
|
||||||
|
Wenn der Test `Risk count: 0` meldet, ist die Isolation fuer die getesteten
|
||||||
|
Homelab-Admin-Pfade ausreichend.
|
||||||
|
|
||||||
|
## Betrieb
|
||||||
|
|
||||||
|
- Familien-/Gaestegeraete kommen ins Gast-WLAN, wenn sie keinen direkten Zugriff
|
||||||
|
auf LAN-Geraete brauchen.
|
||||||
|
- Homelab-Apps fuer Familie laufen perspektivisch ueber HTTPS/OIDC, nicht ueber
|
||||||
|
direkten LAN-Zugriff.
|
||||||
|
- Geraete, die lokale Discovery brauchen (z. B. manche Smart-TV/Plex-Szenarien),
|
||||||
|
bleiben im normalen LAN oder bekommen eine separate bewusste Entscheidung.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Wenn nach Aktivierung etwas Unerwartetes passiert:
|
||||||
|
|
||||||
|
1. FRITZ!Box: `WLAN -> Gastzugang` oeffnen.
|
||||||
|
2. Gastzugang deaktivieren.
|
||||||
|
3. Speichern.
|
||||||
|
4. Normalen LAN-Zugriff pruefen:
|
||||||
|
```powershell
|
||||||
|
G:\Gitea_Clone\homelab-infra\ops\maintenance\check-guest-iot-isolation.ps1 -Mode LanPreflight
|
||||||
|
```
|
||||||
|
|
||||||
|
Es werden durch dieses Runbook keine Docker-Stacks, Secrets oder produktiven
|
||||||
|
Appdaten veraendert.
|
||||||
@@ -3,8 +3,20 @@
|
|||||||
Status: Hardware-Baseline erfasst; USV/Power-Loss ist als bewusst akzeptiertes Betreiber-Risiko dokumentiert.
|
Status: Hardware-Baseline erfasst; USV/Power-Loss ist als bewusst akzeptiertes Betreiber-Risiko dokumentiert.
|
||||||
Host: `Kallilabcore`
|
Host: `Kallilabcore`
|
||||||
Letzte Pruefung: 2026-05-26
|
Letzte Pruefung: 2026-05-26
|
||||||
|
Doku-Stand Betreiberentscheidungen: 2026-06-05
|
||||||
Naechster Review: 2026-08-26
|
Naechster Review: 2026-08-26
|
||||||
|
|
||||||
|
## Betreiber-Entscheidungen (Stand 2026-06-05)
|
||||||
|
|
||||||
|
Diese drei Punkte waren bisher diffuse TBDs und sind jetzt als bewusste
|
||||||
|
Entscheidungen festgehalten. Details in den jeweiligen Abschnitten unten.
|
||||||
|
|
||||||
|
| Thema | Entscheidung | Review-Trigger |
|
||||||
|
|---|---|---|
|
||||||
|
| USV / Power Loss | **Bewusst auf Q3/2026 geparkt.** Keine Anschaffung dieses Quartal; Power-Loss bleibt akzeptiertes Risiko. | Naechstes Hardware-Upgrade, erneuter realer Stromausfall mit Datenfolge, oder Q3-Review (ab 2026-07-01) |
|
||||||
|
| Cold-Backup-Rotation | **Bewusst Hetzner-only.** Off-site bleibt allein das Hetzner-Borg-Repo; keine zweite rotierende Cold-Kopie. | Stark wachsender Datenwert, wiederholte Hetzner-Probleme, oder geaenderte Betreiber-Praeferenz |
|
||||||
|
| Stromverbrauch messen | **Bewusst ohne Messung (Entscheidung 2026-06-06).** Kein Messgeraet; Werte bleiben dauerhaft offen, kein Beschaffungs-Todo. | Nur falls spaeter doch ein Messgeraet angeschafft wird oder Strom-/Kostenfrage relevant wird |
|
||||||
|
|
||||||
## Zweck
|
## Zweck
|
||||||
|
|
||||||
Dieses Dokument beschreibt die physische Basis des Homelabs. Es ist die Grundlage fuer Capacity Planning, Restore-Zeit, Ersatzteilplanung, USV-Verhalten und Entscheidungen wie Immich-ML, Plex-Transcoding oder Storage-Erweiterung.
|
Dieses Dokument beschreibt die physische Basis des Homelabs. Es ist die Grundlage fuer Capacity Planning, Restore-Zeit, Ersatzteilplanung, USV-Verhalten und Entscheidungen wie Immich-ML, Plex-Transcoding oder Storage-Erweiterung.
|
||||||
@@ -96,7 +108,7 @@ tailscale ip -4
|
|||||||
| Disk1 | `md1p1` / physisch `sdc` | WDC WD60EFAX-68JH4N1 | `WD-WX32D90PC0V0` | 5.5T | XFS auf md1p1 | Array-Daten | SMART passed |
|
| Disk1 | `md1p1` / physisch `sdc` | WDC WD60EFAX-68JH4N1 | `WD-WX32D90PC0V0` | 5.5T | XFS auf md1p1 | Array-Daten | SMART passed |
|
||||||
| Parity | physisch `sdb` | TOSHIBA HDWG480 | `2460A03VFA3H` | 7.3T | n/a | Parity | SMART passed |
|
| Parity | physisch `sdb` | TOSHIBA HDWG480 | `2460A03VFA3H` | 7.3T | n/a | Parity | SMART passed |
|
||||||
| Boot | `sda1` | Samsung Flash Drive | `0375125090000587` | 59.8G | FAT32 | Unraid Boot | aktiv |
|
| Boot | `sda1` | Samsung Flash Drive | `0375125090000587` | 59.8G | FAT32 | Unraid Boot | aktiv |
|
||||||
| Cold Backup | TBD | TBD | TBD | TBD | TBD | Externe Rotation | offen |
|
| Cold Backup | bewusst keiner | n/a | n/a | n/a | n/a | Externe Rotation | **bewusst Hetzner-only** (Entscheidung 2026-06-05); off-site allein via Hetzner-Borg |
|
||||||
|
|
||||||
Pruefkommando:
|
Pruefkommando:
|
||||||
|
|
||||||
@@ -138,18 +150,27 @@ Bewertung:
|
|||||||
|
|
||||||
- Aktueller Befund 2026-05-26: keine funktionierende USV-Absicherung nachgewiesen.
|
- Aktueller Befund 2026-05-26: keine funktionierende USV-Absicherung nachgewiesen.
|
||||||
- `apcupsd` ist zwar auf dem System vorhanden, aber nicht aktiv.
|
- `apcupsd` ist zwar auf dem System vorhanden, aber nicht aktiv.
|
||||||
- Operator-Entscheidung 2026-05-26: aktuell keine USV-Anschaffung.
|
- **Operator-Entscheidung 2026-06-05: USV-Anschaffung bewusst auf Q3/2026 geparkt.** Keine Beschaffung in diesem Quartal.
|
||||||
- Power-Loss bleibt damit ein bewusst akzeptiertes Risiko fuer Docker-/DB-State und laufende Writes.
|
- 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.
|
- Review-Trigger (einer reicht): naechstes Hardware-Upgrade, ein erneuter realer Stromausfall mit Datenfolge, oder der Q3-Review ab 2026-07-01.
|
||||||
|
- Wenn die Entscheidung in Q3 zugunsten einer USV kippt, ist das Mindestkriterium ein USB-HID-faehiges Geraet (~600-900 VA), das von `apcupsd` erkannt wird, damit der bereits vorkonfigurierte Shutdown-Pfad ohne Zusatzsoftware greift.
|
||||||
|
|
||||||
## Stromverbrauch
|
## Stromverbrauch
|
||||||
|
|
||||||
|
**Bewusst ohne Messung (Operator-Entscheidung 2026-06-06).** Es wird kein
|
||||||
|
Messgeraet beschafft; Idle/Normal/Backup/Last bleiben dauerhaft offen. Kein
|
||||||
|
offener Todo. Falls spaeter doch eine Mess-Steckdose angeschafft wird, reicht
|
||||||
|
ein einziger Messdurchlauf, um die Tabelle zu fuellen.
|
||||||
|
|
||||||
| Zustand | Verbrauch | Messmethode | Datum |
|
| Zustand | Verbrauch | Messmethode | Datum |
|
||||||
|---|---:|---|---|
|
|---|---:|---|---|
|
||||||
| Idle | TBD | externes Messgeraet erforderlich | TBD |
|
| Idle | offen | schaltbare Mess-Steckdose, 10 min Mittelwert ohne aktive Jobs | nach Beschaffung |
|
||||||
| Normalbetrieb | TBD | externes Messgeraet erforderlich | TBD |
|
| Normalbetrieb | offen | Mess-Steckdose, typischer Tagbetrieb mit laufenden Apps | nach Beschaffung |
|
||||||
| Backup-Lauf | TBD | externes Messgeraet erforderlich | TBD |
|
| Backup-Lauf | offen | Mess-Steckdose, waehrend naechtlichem Borg-Lauf | nach Beschaffung |
|
||||||
| Last | TBD | externes Messgeraet erforderlich | TBD |
|
| Last | offen | Mess-Steckdose, unter CPU-Last (z. B. Immich-ML/Parity-Check) | nach Beschaffung |
|
||||||
|
|
||||||
|
Beschaffungs-Trigger: einfache schaltbare Energiemess-Steckdose; danach ein
|
||||||
|
einziger Messdurchlauf reicht, um diese Tabelle dauerhaft zu fuellen.
|
||||||
|
|
||||||
## Ersatzteil- und Lifecycle-Plan
|
## Ersatzteil- und Lifecycle-Plan
|
||||||
|
|
||||||
@@ -160,7 +181,7 @@ Bewertung:
|
|||||||
| Parity | Kleiner als neue groesste Datenplatte | Parity-Upgrade vor Datenplatten-Upgrade |
|
| Parity | Kleiner als neue groesste Datenplatte | Parity-Upgrade vor Datenplatten-Upgrade |
|
||||||
| Boot-USB | Lesefehler oder Alter TBD | Flash-Backup verifizieren, Ersatzstick vorbereiten |
|
| Boot-USB | Lesefehler oder Alter TBD | Flash-Backup verifizieren, Ersatzstick vorbereiten |
|
||||||
| RAM | Swap/OOM oder Immich/Nextcloud-Druck | Ausbau planen |
|
| RAM | Swap/OOM oder Immich/Nextcloud-Druck | Ausbau planen |
|
||||||
| USV | keine funktionierende USV-Abschaltung | Risiko am 2026-05-26 bewusst akzeptiert; bei Review erneut bewerten |
|
| USV | keine funktionierende USV-Abschaltung | Anschaffung 2026-06-05 bewusst auf Q3/2026 geparkt; Trigger: Hardware-Upgrade, realer Stromausfall mit Datenfolge, oder Q3-Review |
|
||||||
|
|
||||||
## Audit-Kommandos
|
## Audit-Kommandos
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
# Home Assistant -> InfluxDB 3 -> Grafana
|
# Home Assistant -> InfluxDB 3 -> Grafana
|
||||||
|
|
||||||
|
**Status 2026-06-06: archiviert / nicht aktiv.** Home Assistant existiert seit
|
||||||
|
dem Crash aktuell nicht mehr. Dieses Dokument ist nur noch ein historischer
|
||||||
|
Zielbild-Entwurf fuer einen spaeteren Neuaufbau. Das fruehere TODO
|
||||||
|
`influxdb3_homeassistant_token` wurde aus der aktiven Master-Liste gestrichen;
|
||||||
|
vor Token-, InfluxDB-Writer- oder Ecowitt-Arbeiten muss Home Assistant zuerst
|
||||||
|
neu aufgesetzt und neu inventarisiert werden.
|
||||||
|
|
||||||
Ziel: Home Assistant schreibt ausgewaehlte Ecowitt- und Energiesensoren nach InfluxDB 3 Core. Grafana bleibt das Langzeit-Dashboard, Home Assistant bleibt die Automationszentrale.
|
Ziel: Home Assistant schreibt ausgewaehlte Ecowitt- und Energiesensoren nach InfluxDB 3 Core. Grafana bleibt das Langzeit-Dashboard, Home Assistant bleibt die Automationszentrale.
|
||||||
|
|
||||||
## Live-Stand 2026-05-04
|
## Historischer Live-Stand 2026-05-04
|
||||||
|
|
||||||
- Home Assistant ist per SSH unter `192.168.178.50:22222` erreichbar.
|
- Home Assistant ist per SSH unter `192.168.178.50:22222` erreichbar.
|
||||||
- `ha core check` ist erfolgreich.
|
- `ha core check` ist erfolgreich.
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# Master To-do - KalliLab CORE
|
||||||
|
|
||||||
|
Stand: 2026-06-06 (Wochenend-Sprint, nach Status-Kategorien sortiert)
|
||||||
|
|
||||||
|
Diese Liste ist die zentrale Arbeitsliste fuer offene operative Punkte im
|
||||||
|
Homelab. Detailentscheidungen bleiben in den verlinkten Runbooks; diese Datei
|
||||||
|
haelt Status, naechsten konkreten Schritt und Quelle zusammen.
|
||||||
|
|
||||||
|
## Status-Kategorien
|
||||||
|
|
||||||
|
- **Aktiv dieses Wochenende** - soll jetzt vorankommen (Claude, Codex oder Operator); konkreter naechster Schritt steht.
|
||||||
|
- **Operator-Entscheidung** - wartet auf eine bewusste Entscheidung des Betreibers (ja/nein/welche Option).
|
||||||
|
- **Geparkt** - bewusst nicht jetzt, mit klarem Review-Trigger.
|
||||||
|
- **Extern blockiert** - wartet auf ein externes Ereignis oder eine Abhaengigkeit (Nachtlauf, zweite Hardware, Geraetebeschaffung).
|
||||||
|
|
||||||
|
Owner-Aufteilung fuer das Wochenende: `baerchen`/Veeam/Backup-Verifikation liegt
|
||||||
|
bei **Codex**; Doku-/Inventar-/Onboarding-Arbeit liegt bei **Claude**;
|
||||||
|
Host-/Entscheidungsaufgaben beim **Operator**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Aktiv dieses Wochenende
|
||||||
|
|
||||||
|
| Thema | Owner | Naechster konkreter Schritt | Quelle |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Family-Onboarding erster Termin | Operator | Checkliste ist fertig (`docs/FAMILY_ONBOARDING.md` Abschnitt "Erster Onboarding-Termin"). Operator legt fest, welche Personen/Geraete real verfuegbar sind, und arbeitet die Reihenfolge Vaultwarden -> Immich -> Mealie pro Person ab | `docs/FAMILY_ONBOARDING.md`, `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
|
| Restore-Test Unraid OS Flash (Stick-Boot) | Operator | Artefakt-Validierung am 2026-06-05 erledigt (`ops/maintenance/check-unraid-flash-backup.sh`, sha256 OK, 8 Kern-Configs). **Verbleibt:** physischer Ersatzstick-Boot-Test, wenn ein Wegwerf-Stick bereitliegt | `docs/RESTORE_MATRIX.md` Abschnitt "Unraid OS Flash" |
|
||||||
|
| Restore-Test Tailscale | Operator | Runbook-Stub abarbeiten: State-Validierung + Reconnect nur auf Wegwerf-Host/VM, danach Geraet in Tailscale-Admin entfernen | `docs/RESTORE_MATRIX.md` Abschnitt "Tailscale" |
|
||||||
|
| Authelia OIDC fuer Apps | Operator/Claude | **Aktive Phase abgeschlossen 2026-06-06.** Live: Grafana (admin, Login verifiziert) + Mealie (family, verifiziert) + Paperless (family, deployed; Login-Test offen). Muster + Gotchas in `docs/AUTHELIA_OIDC_PLAN.md`. **Immich + Nextcloud bewusst GEPARKT bis Onboarding** (Entscheidung 2026-06-06): nur `micha` hat Authelia-Account, Familien-SSO-Nutzen + UI/occ-Aufwand lohnen erst mit Familien-Accounts. Runbook bereit | `docs/AUTHELIA_OIDC_PLAN.md`, `security/authelia/configuration.yml` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Operator-Entscheidung
|
||||||
|
|
||||||
|
**Stand 2026-06-06: keine offenen Operator-Entscheidungen.** Alle am 2026-06-06
|
||||||
|
entschieden — Ergebnisse in "Aktiv", "Geparkt" bzw. "Entschieden 2026-06-06".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Geparkt
|
||||||
|
|
||||||
|
Bewusst nicht jetzt - mit Review-Trigger.
|
||||||
|
|
||||||
|
| Thema | Entscheidung / Trigger | Quelle |
|
||||||
|
|---|---|---|
|
||||||
|
| USV-Anschaffung | **Auf Q3/2026 geparkt** (2026-06-05). Power-Loss bleibt akzeptiertes Risiko. Trigger: Hardware-Upgrade, realer Stromausfall mit Datenfolge, oder Q3-Review ab 2026-07-01 | `docs/HARDWARE_INVENTORY.md` |
|
||||||
|
| Cold-Backup-Rotation | **Bewusst Hetzner-only** (2026-06-05). Keine zweite rotierende Cold-Kopie. Trigger: stark wachsender Datenwert, wiederholte Hetzner-Probleme, geaenderte Praeferenz | `docs/HARDWARE_INVENTORY.md` |
|
||||||
|
| WAN-Ausfallschutz | **Spaeter evaluieren** (2026-06-05). Mobilfunk-Failover inaktiv; lokale Apps laufen bei WAN-Ausfall weiter. Trigger: haeufigere/laengere DSL-Ausfaelle oder kritischer Remote-Zugang | `docs/NETWORK_INVENTORY.md` |
|
||||||
|
| Docker Critical Events Watcher | **Aktiviert 2026-06-05:** Unraid User Script `docker-critical-events-at-start` nutzt den Supervisor und steht in `schedule.json` auf `frequency: start`; Watcher manuell gestartet, Status `running`. Optionaler ntfy-Smoke wurde nachts bewusst nicht gesendet und kann spaeter mit `docker-critical-events-supervisor.sh smoke` nachgeholt werden | `docs/SERVICE_CATALOG.md`, `services/posture-check/docker-critical-events.sh`, `services/posture-check/unraid-user-scripts.md` |
|
||||||
|
| Negativ-Test Backup-Frische | **Validiert 2026-06-06:** `ops/restore-tests/negative-freshness-alert-test.sh` simuliert fehlende Dumps nur in einem synthetischen Restore-Lab-Pfad und sendet einen Test-Alert nach `homelab-alerts`; Host-Lauf schrieb Report `/mnt/user/backups/restore-reports/freshness-negative-2026-06-06-130320.md` (10 Criticals, produktive Dumps unangetastet). Quartalsweise wiederholen: `ops/restore-tests/run-restore-checks.sh freshness-negative` | `ops/restore-tests/README.md`, `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
|
| End-to-end-DR-Drill | Komplett-Bootstrap Phase 1-5 auf Wegwerf-Host; realistisch erst mit zweiter Hardware (siehe auch Extern blockiert) | `docs/AUDIT_2026-05-25_TODO.md`, `docs/DISASTER_RECOVERY.md` |
|
||||||
|
| Wiederkehrende Restore-Drills | Vaultwarden, Gitea, Authelia, Komodo, Paperless, Immich, Traefik, PostgreSQL, Mongo, Nextcloud, Mealie, Mail-Archiver nach Matrix-Intervallen rotieren | `docs/RESTORE_MATRIX.md`, `docs/RESTORE_HANDBOOK.md` |
|
||||||
|
| Dedizierter SMB-User `veeam-baerchen` | Optional spaeter, nur wenn Unraid-User-/Share-Rechte bewusst angefasst werden | `ops/windows-reinstall/docs/windows-image-backup-baseline.md` |
|
||||||
|
| Nextcloud 2FA (Operator-TOTP) | **Geparkt (Entscheidung 2026-06-06):** Operator-TOTP fuer Nextcloud erst zusammen mit der app-weiten Familien-/OIDC-Policy entscheiden. Trigger: OIDC-/SSO-Block (jetzt aktiv) erreicht die App-Login-Ebene | `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
|
| Tailnet-Konsole aufraeumen (Rest) | Nach Docker-Stack-Abbau (2026-06-06) nur noch tote Node-Eintraege: `kallilab-core` (down) und alter Offline-`baerchen` in der Tailscale-Admin-Konsole entfernen. Optional State-Pfad `/mnt/user/appdata/tailscale` nach `_archive/`. Trivial, kein Risiko | `docs/NETWORK_INVENTORY.md` |
|
||||||
|
| CrowdSec vor Traefik | Bewusst nicht umgesetzt; einzige WAN-Tuer ist `443/tcp`, Authelia `regulation:` deckt Brute-Force ab. Neu bewerten bei breiterer Attack Surface | `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
|
| Hermes-Agent | NAS-Stack bleibt deaktiviert; Review-Deadline 2026-07-25 | `docs/AUDIT_2026-05-25_TODO.md`, `docs/SERVICE_CATALOG.md` |
|
||||||
|
| Filebrowser-Mounts | Bei zukuenftigem Hardening-Sprint Mount-Scope reduzieren | `docs/SERVICE_CATALOG.md` |
|
||||||
|
| Scrutiny Privileged-Ausnahme | Nur mit klarer Begruendung aendern; sonst dokumentierte Ausnahme beibehalten | `docs/SERVICE_CATALOG.md` |
|
||||||
|
| Immich Redis named volume | Anonymes Volume bei passender Wartung auf named volume umstellen oder Ausnahme dokumentieren | `docs/SERVICE_CATALOG.md` |
|
||||||
|
| Storage-Wachstum | Zweite NVMe, ZFS/BTRFS-Optionen, zweite Array-Disk nur bei Triggern aus Capacity-Doku | `docs/STORAGE_LAYOUT.md`, `docs/CAPACITY_AND_LIFECYCLE.md` |
|
||||||
|
| Zweites Off-site-Ziel | Bewusst nicht umgesetzt; neu bewerten bei Hetzner-Problemen oder wachsendem Datenwert | `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
|
| Borg `append-only` auf Hetzner | Operator-Entscheidung 2026-06-01: nicht umgesetzt (forced-command brach Key-Auth, Nutzen/Risiko unguenstig) | `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Extern blockiert
|
||||||
|
|
||||||
|
Wartet auf ein externes Ereignis oder eine Abhaengigkeit.
|
||||||
|
|
||||||
|
| Thema | Blockiert durch | Naechster Schritt sobald entblockt | Quelle |
|
||||||
|
|---|---|---|---|
|
||||||
|
| End-to-end-DR-Drill (Hardware-Teil) | Keine zweite Wegwerf-Hardware verfuegbar | Sobald zweite Hardware da ist: Komplett-Bootstrap Phase 1-5 fahren | `docs/DISASTER_RECOVERY.md` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Erledigt im Wochenend-Sprint (2026-06-05)
|
||||||
|
|
||||||
|
- Restore-Matrix "Naechste Restore-Test-Kandidaten" bereinigt: 5 am 2026-06-03 abgeschlossene Kandidaten entfernt, durch die 4 real offenen Pfade ersetzt; Stand-Datum aktualisiert.
|
||||||
|
- Restore-Test-Runbook-Stubs fuer Unraid Flash / AdGuard / Tailscale / Redis 8 in `docs/RESTORE_MATRIX.md` ergaenzt.
|
||||||
|
- Alte Windows-Doku bereinigt: WinRE-/Admin-Check-To-dos in `boot-cleanup-plan-2026-06-04.md` und `laufwerks-neustruktur-2026-06-04.md` als erledigt markiert.
|
||||||
|
- `docs/HARDWARE_INVENTORY.md`: USV (Q3-Park), Cold-Backup (Hetzner-only) und Stromverbrauch von diffusen TBDs auf bewusste Entscheidungen mit Review-Triggern gehoben.
|
||||||
|
- `docs/NETWORK_INVENTORY.md`: Tailscale-Inventar am 2026-06-05 **real per read-only SSH gemessen** und eingetragen: IPv6 `fd7a:115c:a1e0::2c01:62b2`, Exit Node `nein`, **Subnet-Router fuer `192.168.178.0/24` aktiv** (widerlegt fruehere Vermutung), Tailnet `taild9fcf2.ts.net`, Geraete-Snapshot + Dubletten-Hinweis. WAN-Failover und Gast-/IoT geschaerft. `zu messen`-Platzhalter entfernt. **`Tailscale-Inventar messen` damit geschlossen.**
|
||||||
|
- `ops/maintenance/check-unraid-flash-backup.sh` neu: read-only Validierung des Flash-Artefakts (sha256, Frische, Kern-Configs, keine Extraktion). Am 2026-06-05 gegen den Host getestet: Exit 0, sha256 OK, 390 Eintraege, 8/8 Kern-Configs. `docs/RESTORE_MATRIX.md` mit Testdatum/Ergebnis aktualisiert. **Artefakt-Validierung des Unraid-Flash-Backups damit erledigt; nur Stick-Boot-Test offen.**
|
||||||
|
- `docs/FAMILY_ONBOARDING.md`: Michi-Checkliste in eine echte Erste-Termin-Checkliste (Vorbereitung, Reihenfolge, Erfolgskriterium, bewusst spaeter) umgebaut.
|
||||||
|
- `docs/MASTER_TODO.md` in vier Status-Kategorien (Aktiv / Operator-Entscheidung / Geparkt / Extern blockiert) umstrukturiert.
|
||||||
|
- `baerchen` Veeam-Erstbackup: erster Full-Lauf 2026-06-05 erfolgreich geschrieben (Veeam-GUI 53,8 GB, Dauer 0:11:31, MetaCheck 0 Fehler/0 Warnungen, VSS `job: success`). Beleg in `ops/windows-reinstall/docs/windows-image-backup-baseline.md`; Veeam Storage Encryption war im ersten Lauf nicht aktiv und ist als Operator-Entscheidung nachgezogen.
|
||||||
|
- Docker Critical Events Watcher auf Unraid aktiviert: Host-Clone auf Commit `2f3d184` aktualisiert, User Script `/boot/config/plugins/user.scripts/scripts/docker-critical-events-at-start/script` auf den Supervisor umgestellt, altes Script als `script.bak-20260605-232621` gesichert, `schedule.json` zeigt `frequency: start`, Watcher laeuft mit PID `1681168`. ntfy-Smoke am 2026-06-06 erfolgreich beim Operator angekommen.
|
||||||
|
- Restore-Test AdGuard Home: automatisierter Test `ops/restore-tests/adguard-restore-test.sh` erstellt und am 2026-06-06 auf Unraid erfolgreich ausgefuehrt. Ergebnis: Borg-Config-Restore aus Archiv `Taegliche-Sicherung-2026-06-06T04:30:05.910`, isolierter Container `restoretest-adguard`, HTTP `/control/status` = `401`, DNS-Smoke `git.kaleschke.info -> 192.168.178.58`, 7 Filterlisten-Eintraege, Report `/mnt/user/backups/restore-reports/adguard-2026-06-06.md`.
|
||||||
|
- Restore-Test Redis 8: automatisierter Test `ops/restore-tests/redis-restore-test.sh` erstellt und am 2026-06-06 auf Unraid erfolgreich ausgefuehrt. Ergebnis: Restore aus `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-20260531-185011`, isolierter Container `restoretest-redis`, `PING` = `PONG`, Redis `8.8.0`, AOF `1`, `DBSIZE` = `1`, Report `/mnt/user/backups/restore-reports/redis-2026-06-06.md`.
|
||||||
|
- **Tailscale ACL-Policy restriktiv ausgerollt (2026-06-06):** Von Default-Allow auf Tag-basierte `grants`-Policy umgestellt, gemeinsam mit dem Operator in lockout-sicherer Reihenfolge (additiv -> taggen -> Allow-all entfernen), jeder Schritt read-only per SSH verifiziert. Live: `kallilabcore`=`tag:server`, `baerchen-1`+`iphone-14`=`tag:operator`, `tag:family` vorbereitet/schlafend. Subnet-Route `192.168.178.0/24` bleibt via `autoApprovers` approved. Smoke-Tests gruen (Operator-SSH, AdGuard-Admin `HTTP 302` ueber Tailnet, Ping 0%); untagged Nodes (`kallilab-core` Docker-Sidecar, alter `baerchen`) isoliert. Beleg: `docs/NETWORK_INVENTORY.md` Abschnitt "ACL-Policy — ANGEWENDET 2026-06-06". Familien-Dienste konkretisieren bei erstem realem Familiengeraet.
|
||||||
|
- **Redundanten Docker-Tailscale-Stack entfernt (2026-06-06):** Befund: Host hatte zwei `tailscaled` — die funktionale native Plugin-Instanz `kallilabcore` (echtes TUN `tailscale1`, Subnet-Router, State im Flash-Backup) und den redundanten userspace-only Docker-Stack `kallilab-core` (`host-services/tailscale/`, routet nichts, nichts haengt dran). Sauber per GitOps abgebaut: Operator hat Komodo-Stack `tailscale` gestoppt+destroyed; danach `git rm host-services/tailscale/`, Glance-Widget entfernt, Architektur-/Service-Catalog-/DR-Bootstrap-/CLAUDE-/Restore-Matrix-/Netzwerk-Doku auf "natives Plugin" nachgezogen. Read-only verifiziert: Container weg, nur noch der native `tailscaled`, Subnet-Route + Operator-Zugriff intakt. Rest: tote Node-Eintraege in der Admin-Konsole entfernen (eigener Todo).
|
||||||
|
|
||||||
|
- DR-Workstation Bare-Metal-Kit abgeschlossen: WSL2 Ubuntu 24.04 auf `baerchen`, Borg 1.2.8, GitHub-Read-DR-Key und Hetzner-DR-Key in WSL, `~/dr-smoke.sh` vorhanden. Finaler Smoke 2026-06-06 erfolgreich: GitHub HEAD `3a263a4...`, Hetzner Storage Box Repos sichtbar, Borg-Repo `hetzner_borg_appdata_critical` gelesen, Repository ID `5dd9b949...`, encrypted `Yes (repokey)`, `DR-Smoke OK (2026-06-06 10:05:30)`. Passphrase wurde nur interaktiv eingegeben und nicht gespeichert.
|
||||||
|
- Restore-Frische-Negativtest validiert: `ops/restore-tests/negative-freshness-alert-test.sh` erstellt und am 2026-06-06 auf Unraid erfolgreich ausgefuehrt. Ergebnis: synthetischer leerer Dump-Pfad erzeugte erwartungsgemaess 10 Criticals, Test-Alert nach `homelab-alerts` gesendet, Report `/mnt/user/backups/restore-reports/freshness-negative-2026-06-06-130320.md`, produktive Dumps unangetastet.
|
||||||
|
- Gast-/IoT-Netz aktiviert und validiert: FRITZ!Box-Gastzugang `Fritzi Gastzugang` aktiv, Heimnetz-Zugriff aus dem Gastnetz blockiert. LAN- und Host-Preflight gruen; iPhone-Smoke aus dem Gast-WLAN bestaetigt, dass `192.168.178.58:8082`, `:8181`, `:222`, `https://192.168.178.58` und `192.168.178.1` nicht erreichbar sind. Runbook: `docs/GUEST_IOT_NETWORK.md`.
|
||||||
|
- `baerchen` Veeam-Recovery-Test ohne echten Restore abgeschlossen: Recovery-USB `VEEAMRE` bootet, SMB-Ziel `\\kallilabcore\backups\windows-images\baerchen` ist in der Recovery Environment erreichbar, Restore Point wird angezeigt, Test vor echtem Restore abgebrochen. Runbook: `ops/windows-reinstall/docs/windows-image-backup-baseline.md`.
|
||||||
|
- **Operator-Entscheidungen 2026-06-06 abgeschlossen** (Liste damit ohne offene Entscheidungen):
|
||||||
|
- **BitLocker `baerchen`: bewusst deaktiviert.** Recovery laeuft ueber Veeam-Image; kein BitLocker-Key-Management. Restrisiko physischer Diebstahl bewusst akzeptiert.
|
||||||
|
- **Veeam Storage Encryption: bewusst unverschluesselt.** Erster Full-Lauf bleibt; Image liegt auf dem lokalen SMB-Share `\\kallilabcore\backups`. Neu bewerten bei Off-host-Auslagerung des Images.
|
||||||
|
- **Stromverbrauch: bewusst ohne Messung.** Kein Messgeraet; Werte bleiben dauerhaft offen, kein Beschaffungs-Todo mehr.
|
||||||
|
- **Authelia Rest-2FA: KOMPLETT erledigt 2026-06-06.** Catch-all `*.kaleschke.info` -> `two_factor` in Repo **und** Host-Config (chirurgische Einzelzeilen-Aenderung mit Backup, OIDC-Beszel-Client + Secret unangetastet), `docker restart authelia` -> healthy + "Startup complete", Operator-2FA-Login auf einer vorher-1FA-Domain verifiziert. Nebenbei vorbestehenden Drift gefunden+bereinigt (Host-Config war vom 25. Mai, borg/code nie gemerged); Repo-Baseline an Host-Endzustand angeglichen, damit `authelia-diff.sh` clean wird sobald der Host-Mirror nachzieht. Rollback-`.bak` auf dem Host vorhanden.
|
||||||
|
- **Authelia OIDC: angehen** (neuer aktiver Block) — **Gast-/IoT-Netz: einrichten/planen** (neuer aktiver Block) — **Nextcloud 2FA: geparkt** bis OIDC die App-Login-Ebene erreicht.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pflege-Regel
|
||||||
|
|
||||||
|
- Neue operative To-dos zuerst hier eintragen oder aus Detaildokumenten hierher uebernehmen, immer mit Status-Kategorie.
|
||||||
|
- Wenn ein Punkt erledigt ist, in der Detaildoku den Beleg/Report eintragen und diese Liste aktualisieren.
|
||||||
|
- Keine vagen "pruefen"-Eintraege ohne Kommando oder Entscheidung.
|
||||||
|
- Historische Drill-Reports bleiben Belegmaterial, aber nicht die fuehrende Arbeitsliste.
|
||||||
+288
-23
@@ -1,7 +1,7 @@
|
|||||||
# Network Inventory - KalliLab CORE
|
# Network Inventory - KalliLab CORE
|
||||||
|
|
||||||
Status: Host-Audit erfasst; Router-Baseline und Portfreigaben-UI bereinigt; VLAN/IPv6-Details offen.
|
Status: Host-Audit erfasst; Router-Baseline und Portfreigaben-UI bereinigt; FRITZ!Box-Remote-Dienste aus; IPv6-Exposure technisch und per UI entschaerft; Tailscale-Inventar am 2026-06-05 real gemessen.
|
||||||
Letzte Pruefung: 2026-05-28
|
Letzte Pruefung: 2026-06-05 (Tailscale-Inventar), 2026-06-01 (Router/Ports)
|
||||||
|
|
||||||
## Zweck
|
## Zweck
|
||||||
|
|
||||||
@@ -14,13 +14,13 @@ Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennun
|
|||||||
| Anschluss / Provider | DSL, Telekom |
|
| Anschluss / Provider | DSL, Telekom |
|
||||||
| Bandbreite (FRITZ!Box-UI) | ca. 87,3 Mbit/s Download, ca. 36 Mbit/s Upload |
|
| Bandbreite (FRITZ!Box-UI) | ca. 87,3 Mbit/s Download, ca. 36 Mbit/s Upload |
|
||||||
| Router-Modell | FRITZ!Box 7590 |
|
| Router-Modell | FRITZ!Box 7590 |
|
||||||
| Firmware | FRITZ!OS 8.21 (Update gemeldet, nicht eingespielt) |
|
| Firmware | FRITZ!OS 8.25 (`154.08.25` per TR-064 am 2026-06-01) |
|
||||||
| Router-IP | 192.168.178.1 |
|
| Router-IP | 192.168.178.1 |
|
||||||
| DHCP-Server | FRITZ!Box (Standardannahme, Override durch Operator nicht dokumentiert) |
|
| DHCP-Server | FRITZ!Box (Standardannahme, Override durch Operator nicht dokumentiert) |
|
||||||
| Lokales Subnetz | 192.168.178.0/24 |
|
| Lokales Subnetz | 192.168.178.0/24 |
|
||||||
| IPv6 aktiv | TBD (FRITZ!Box-UI separat pruefen) |
|
| IPv6 aktiv | Windows-Client hat Provider-IPv6; Host hat keine globale Provider-IPv6, nur Tailscale-ULA |
|
||||||
| DynDNS / DDNS | Cloudflare via `ddns-updater` (kein FRITZ!Box-DynDNS in Nutzung) |
|
| DynDNS / DDNS | Cloudflare via `ddns-updater` (kein FRITZ!Box-DynDNS in Nutzung) |
|
||||||
| Heimnetz-Geraete (FRITZ!Box-UI) | 36 aktive Geraete |
|
| Heimnetz-Geraete (FRITZ!Box-UI) | 35 aktive Geraete |
|
||||||
| LAN-Ports belegt | LAN 1-4 verbunden |
|
| LAN-Ports belegt | LAN 1-4 verbunden |
|
||||||
| Telefonie / DECT | aktiv |
|
| Telefonie / DECT | aktiv |
|
||||||
| USB an FRITZ!Box | nicht verbunden |
|
| USB an FRITZ!Box | nicht verbunden |
|
||||||
@@ -30,7 +30,8 @@ Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennun
|
|||||||
|
|
||||||
- Telekom-DSL ist Single-WAN; ohne Ausfallschutz ist Internet-Ausfall = kein DDNS-Update, keine ACME-Erneuerung, keine externen Push-Quellen.
|
- 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.
|
- 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.
|
- FRITZ!OS ist am 2026-06-01 per TR-064 auf `154.08.25` beobachtet; FRITZ!Box-Konfig-Backup `Einstellungen_FRITZ.Box_7590_154.08.25_01.06.26_1318.export` wurde extern/off-system in Vaultwarden abgelegt.
|
||||||
|
- `Internet -> Freigaben -> FRITZ!Box-Dienste` ist am 2026-06-01 geprueft: Internetzugriff auf die FRITZ!Box per HTTPS ist aus, FTP/FTPS-Zugriff auf Speichermedien ist aus.
|
||||||
|
|
||||||
## DNS
|
## DNS
|
||||||
|
|
||||||
@@ -43,28 +44,168 @@ Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennun
|
|||||||
|
|
||||||
## Tailscale
|
## Tailscale
|
||||||
|
|
||||||
| Feld | Wert |
|
Gemessen am 2026-06-05 per read-only SSH auf den Host (`tailscale status`,
|
||||||
|
`tailscale status --json`, `tailscale ip -4/-6`).
|
||||||
|
|
||||||
|
| Feld | Wert / Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Node-Name | Kallilabcore |
|
| Node-Name | Kallilabcore |
|
||||||
| Tailscale IPv4 | 100.80.98.33 |
|
| Tailnet / MagicDNS | `taild9fcf2.ts.net`; DNSName `kallilabcore.taild9fcf2.ts.net` |
|
||||||
| Tailscale IPv6 | TBD |
|
| Tailscale IPv4 | `100.80.98.33` |
|
||||||
| Exit Node | TBD |
|
| Tailscale IPv6 | `fd7a:115c:a1e0::2c01:62b2` (gemessen 2026-06-05) |
|
||||||
| Subnet Router | TBD |
|
| Exit Node | **Nein.** `Self.ExitNodeOption: false` und `Self.ExitNode: false` — Host bietet keinen Exit Node an und nutzt keinen. Entspricht dem Ziel (Operator-Zugang ist eingehend, nicht als Internet-Ausgang). |
|
||||||
| ACL-Policy extern dokumentiert | TBD |
|
| Subnet Router | **Ja, aktiv.** Host advertised und ist Primary fuer `192.168.178.0/24` (`Self.PrimaryRoutes: ["192.168.178.0/24"]`, ebenfalls in `AllowedIPs`). Das LAN ist also fuer das gesamte Tailnet ueber diesen Subnet-Router erreichbar — bewusst gemessener Ist-Zustand, **kein** "keine Route" wie zuvor vermutet. |
|
||||||
|
| ACL-Policy extern dokumentiert | **Angewendet 2026-06-06** — restriktive Tag-basierte `grants`-Policy live (`tag:server`/`tag:operator`, `tag:family` schlafend). Default-Allow entfernt, verifiziert. Details im Block unten. |
|
||||||
|
|
||||||
Pruefkommando:
|
### Tailnet-Geraete (Snapshot 2026-06-05)
|
||||||
|
|
||||||
|
| Tailscale-IP | Node | OS | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `100.80.98.33` | kallilabcore | linux | aktiv (Host, Subnet-Router) |
|
||||||
|
| `100.78.133.37` | baerchen-1 | windows | aktiv (aktuelle Operator-Workstation, direct) |
|
||||||
|
| `100.105.203.21` | baerchen | windows | offline, zuletzt vor ~1 Tag gesehen (Alt-Node) |
|
||||||
|
| `100.73.83.55` | iphone-14 | iOS | bekannt |
|
||||||
|
| `100.112.0.90` | kallilab-core | linux | **am 2026-06-06 entfernt.** War der redundante userspace-only `Tailscale-Docker`-Stack (`host-services/tailscale/`). Komodo-Stack gestoppt+destroyed, Repo-Pfad per `git rm` entfernt, Container weg (read-only verifiziert). Node-Eintrag in der Admin-Konsole noch zu entfernen. |
|
||||||
|
|
||||||
|
> **Befund 2026-06-06 (read-only auf dem Host ermittelt):** Der Host hat **zwei**
|
||||||
|
> `tailscaled`-Prozesse:
|
||||||
|
>
|
||||||
|
> 1. **Native Unraid-Plugin** = `kallilabcore` (100.80.98.33). Prozess
|
||||||
|
> `/usr/local/sbin/tailscaled -statedir /boot/config/plugins/tailscale/state
|
||||||
|
> -tun tailscale1`. **Echtes TUN-Interface `tailscale1`, ist der Subnet-Router
|
||||||
|
> fuer `192.168.178.0/24`**, laeuft seit 24. Mai, installiert via
|
||||||
|
> `tailscale.plg` + `unraid-tailscale-utils`. State unter
|
||||||
|
> `/boot/config/plugins/tailscale/state` → ueber das **Flash-Backup** gesichert.
|
||||||
|
> Im ACL-Rollout `tag:server`. **Das ist die funktionale, kanonische Instanz.**
|
||||||
|
> 2. **Docker-Stack** = `kallilab-core` (100.112.0.90), `host-services/tailscale/`.
|
||||||
|
> Prozess `tailscaled --tun=userspace-networking` → **nur Userspace, kann
|
||||||
|
> technisch nicht routen / kein Subnet-Router/Exit-Node sein**, advertised
|
||||||
|
> nichts, kein Container teilt seinen Namespace, seit 31. Mai. State unter
|
||||||
|
> `/mnt/user/appdata/tailscale`. Im ACL-Rollout untagged → isoliert.
|
||||||
|
> **Hochwahrscheinlich redundant.**
|
||||||
|
>
|
||||||
|
> **Umgesetzt 2026-06-06:** Der redundante Docker-Stack `host-services/tailscale/`
|
||||||
|
> wurde sauber per GitOps abgebaut — Komodo-Stack `tailscale` gestoppt+destroyed
|
||||||
|
> (Operator), `git rm host-services/tailscale/`, Glance-Widget entfernt, und
|
||||||
|
> Architektur-/Service-Catalog-/DR-/CLAUDE-Doku auf "natives Plugin" nachgezogen.
|
||||||
|
> Read-only verifiziert: Container weg, nur noch der native `tailscaled` mit
|
||||||
|
> `tailscale1`, Subnet-Route + Operator-Zugriff intakt. Offen: Node-Eintraege
|
||||||
|
> `kallilab-core` und alter `baerchen` in der Admin-Konsole entfernen; State-Pfad
|
||||||
|
> `/mnt/user/appdata/tailscale` bei Gelegenheit nach `_archive/` (kein Sofort-Loeschen).
|
||||||
|
>
|
||||||
|
> **Doku-Korrektur erledigt:** `docs/RESTORE_MATRIX.md` zeigt jetzt auf den
|
||||||
|
> funktionalen State `/boot/config/plugins/tailscale/state` (im Flash-Backup)
|
||||||
|
> statt auf den entfernten userspace-Docker-Pfad.
|
||||||
|
|
||||||
|
### Subnet-Router-Konsequenz
|
||||||
|
|
||||||
|
Weil `Kallilabcore` das LAN `192.168.178.0/24` als Subnet-Route anbietet, kann
|
||||||
|
**jedes** Tailnet-Geraet mit Zugriff auf diese Route potenziell LAN-Dienste auf
|
||||||
|
`192.168.178.0/24` erreichen — auch die Admin-Ports, die im LAN bewusst nur auf
|
||||||
|
die Tailscale-IP gebunden sind, sind ueber die Subnet-Route adressierbar. Genau
|
||||||
|
deshalb ist die ACL-Policy (unten) der eigentliche Schutzmechanismus und nicht
|
||||||
|
nur der LAN-Bind.
|
||||||
|
|
||||||
|
Pruefkommando (auf dem Unraid-Host, read-only):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tailscale status
|
tailscale status
|
||||||
|
tailscale status --json | jq '{exitNode: .Self.ExitNodeOption, primaryRoutes: .Self.PrimaryRoutes, allowedIPs: .Self.AllowedIPs}'
|
||||||
tailscale ip -4
|
tailscale ip -4
|
||||||
tailscale ip -6
|
tailscale ip -6
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### ACL-Policy — ANGEWENDET 2026-06-06 (restriktive Tag-basierte grants)
|
||||||
|
|
||||||
|
**Status: live und verifiziert.** Die restriktive Policy wurde am 2026-06-06
|
||||||
|
gemeinsam mit dem Operator in der lockout-sicheren Reihenfolge ausgerollt und
|
||||||
|
read-only verifiziert (siehe "Rollout-Protokoll" unten). Ausgangspunkt war die
|
||||||
|
**unveraenderte Default-Policy** im **`grants`-Schema** (eine Allow-all-Regel,
|
||||||
|
keine Groups/Tags/`autoApprovers`); es gab also keinen eigenen Bestand zu
|
||||||
|
erhalten.
|
||||||
|
|
||||||
|
> **Schema-Hinweis:** Dieses Tailnet nutzt das `grants`-Modell
|
||||||
|
> (`{"src","dst","ip"}`), nicht das aeltere `acls`/`action:accept`-Modell.
|
||||||
|
> Normaler SSH-Zugriff (`ssh kallilabcore` ueber OpenSSH Port 22) wird ueber
|
||||||
|
> `grants` geregelt, nicht ueber den `ssh`-Block; letzterer betrifft nur die
|
||||||
|
> Tailscale-SSH-Funktion.
|
||||||
|
|
||||||
|
**Angewendete Policy (live, kein Secret):**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tagOwners": {
|
||||||
|
"tag:server": ["autogroup:admin"],
|
||||||
|
"tag:operator": ["autogroup:admin"],
|
||||||
|
"tag:family": ["autogroup:admin"]
|
||||||
|
},
|
||||||
|
"autoApprovers": {
|
||||||
|
"routes": { "192.168.178.0/24": ["tag:server"] }
|
||||||
|
},
|
||||||
|
"grants": [
|
||||||
|
{"src": ["tag:operator"], "dst": ["*"], "ip": ["*"]},
|
||||||
|
{"src": ["tag:server"], "dst": ["tag:operator"], "ip": ["*"]},
|
||||||
|
{"src": ["tag:family"], "dst": ["tag:server"], "ip": ["tcp:443"]}
|
||||||
|
],
|
||||||
|
"ssh": [
|
||||||
|
{"action": "check", "src": ["autogroup:member"], "dst": ["autogroup:self"],
|
||||||
|
"users": ["autogroup:nonroot", "root"]}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Geraete-Tags (live):** `kallilabcore` = `tag:server`; `baerchen-1` + `iphone-14`
|
||||||
|
= `tag:operator`; `kallilab-core` (Docker) + alter `baerchen` bewusst untagged ->
|
||||||
|
isoliert.
|
||||||
|
|
||||||
|
**Rollout-Protokoll 2026-06-06 (lockout-sicher, je Schritt read-only verifiziert):**
|
||||||
|
|
||||||
|
1. Policy additiv erweitert (Tags/grants definiert, Allow-all noch drin) -> alle Peers unveraendert verbunden, Route approved.
|
||||||
|
2. `baerchen-1` getaggt `tag:operator` -> online, verifiziert.
|
||||||
|
3. `iphone-14` getaggt `tag:operator` -> verifiziert.
|
||||||
|
4. `kallilab-core` faktisch geprueft (Docker-Sidecar, keine Abhaengigen) -> bewusst untagged gelassen.
|
||||||
|
5. Host `kallilabcore` getaggt `tag:server` -> Route blieb via `autoApprovers` automatisch approved, SSH ok.
|
||||||
|
6. Allow-all entfernt -> restriktiv. Smoke-Tests gruen: Operator-SSH ok, AdGuard-Admin ueber Tailnet `HTTP 302`, Ping 0% Verlust, Route weiter approved; Host sieht nur noch die zwei Operator-Peers (untagged Nodes isoliert). LAN-Rueckweg durchgehend verfuegbar.
|
||||||
|
|
||||||
|
**Schema-/Erhaltungs-Hinweis fuer spaeter:** Die LAN-Subnet-Route
|
||||||
|
`192.168.178.0/24` wird jetzt ueber `autoApprovers`/`tag:server` approved
|
||||||
|
(vorher manuell). Es gibt keinen eigenen Bestand zu erhalten; die Policy oben
|
||||||
|
ist die vollstaendige Wahrheit.
|
||||||
|
|
||||||
|
**Hintergrund / Designentscheidungen (2026-06-05/06):**
|
||||||
|
|
||||||
|
- Single-User-Realitaet: alle Nodes gehoeren demselben User `michaelkaleschke@`.
|
||||||
|
Eine Differenzierung Operator/Familie ist nur ueber **Tags** moeglich, deshalb
|
||||||
|
der Tag-Ansatz statt user-/gruppenbasiert.
|
||||||
|
- Erster Rollout bewusst klein: nur `tag:server` + `tag:operator`.
|
||||||
|
- **`tag:family` ist vorbereitet, aber schlafend:** Tag und eine konservative
|
||||||
|
Minimal-Regel (`dst: tag:server`, `ip: tcp:443`) sind definiert, aber **kein
|
||||||
|
Geraet traegt den Tag**, daher null Wirkung. Sobald ein echtes Familiengeraet
|
||||||
|
dazukommt, wird es einmal mit `tag:family` getaggt und die Regel greift sofort
|
||||||
|
— ohne Policy-Umbau. Vor dem ersten realen Familiengeraet die Regel auf die
|
||||||
|
dann benoetigten Dienste/Ports pruefen.
|
||||||
|
- Der `ssh`-Block bleibt der Default (Tailscale-SSH Check-Modus); normaler
|
||||||
|
OpenSSH-Zugriff laeuft ueber die `grants` (Port 22, fuer `tag:operator` ueber
|
||||||
|
`ip: ["*"]` abgedeckt).
|
||||||
|
|
||||||
|
**Offene Folgepunkte (kein Risiko, Hygiene/spaeter):**
|
||||||
|
|
||||||
|
- Familien-Dienste/Ports konkretisieren — erst wenn ein reales Familiengeraet dazukommt.
|
||||||
|
- **Zwei-Tailscale-Konsolidierung: ERLEDIGT 2026-06-06** — redundanter Docker-Stack
|
||||||
|
abgebaut, nur noch die native Plugin-Instanz `kallilabcore` (Subnet-Router) aktiv.
|
||||||
|
- **Tailnet-Konsole aufraeumen: ERLEDIGT 2026-06-06** — Node-Eintraege `kallilab-core`
|
||||||
|
und alter Offline-`baerchen` aus der Admin-Konsole entfernt.
|
||||||
|
- State-Pfad `/mnt/user/appdata/tailscale` (vom entfernten Docker-Stack) bei
|
||||||
|
Gelegenheit nach `_archive/tailscale-removed-2026-06-06/` (kein Sofort-Loeschen).
|
||||||
|
- Optionaler Off-LAN-Routentest: von einem Operator-Geraet im Mobilfunk
|
||||||
|
(nicht im Heim-LAN) ein LAN-Ziel ueber `192.168.178.0/24` erreichen, um die
|
||||||
|
Subnet-Route end-to-end zu bestaetigen (im Heim-LAN nicht sauber isolierbar).
|
||||||
|
|
||||||
## Portfreigaben und Exposure
|
## Portfreigaben und Exposure
|
||||||
|
|
||||||
### FRITZ!Box (WAN -> Host)
|
### FRITZ!Box (WAN -> Host)
|
||||||
|
|
||||||
Aktiver Soll-Stand nach Operator-Bereinigung 2026-05-28:
|
Aktiver Soll-Stand nach Operator-Bereinigung und UI-Gegencheck 2026-06-01:
|
||||||
|
|
||||||
| Aktive Freigabe | Ziel | Zweck | Bemerkung |
|
| Aktive Freigabe | Ziel | Zweck | Bemerkung |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -76,13 +217,14 @@ Bewusst **nicht** freigegeben:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `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. |
|
| `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. |
|
| `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. |
|
||||||
|
| `32400/tcp` (Plex) | Plex wird extern ausschliesslich ueber `https://plex.kaleschke.info` via Traefik/443 erreicht. Kein direkter WAN-Port fuer Plex, Plex Remote Access bleibt aus. |
|
||||||
|
|
||||||
### UPnP / Selbstständige Portfreigaben
|
### UPnP / Selbstständige Portfreigaben
|
||||||
|
|
||||||
| Geraet | UPnP-Selbstfreigabe-Recht | Begruendung |
|
| Geraet | UPnP-Selbstfreigabe-Recht | Begruendung |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `Kallilabcore` (192.168.178.58) | nicht erlaubt | Repo-managed; alle benoetigten Public-Ports sind explizite Freigaben |
|
| `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 |
|
| `PC-192-168-178-71` / VONETS-Adapter (192.168.178.71, MAC 00:17:13:2F:61:96) | **2026-06-01 erneut geprueft und 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 hier als bewusste Ausnahme dokumentiert oder pro Geraet wieder deaktiviert.
|
Sollten neue Geraete UPnP-Selbstfreigaben anfordern, wird das hier als bewusste Ausnahme dokumentiert oder pro Geraet wieder deaktiviert.
|
||||||
|
|
||||||
@@ -94,7 +236,7 @@ Historischer UI-Befund vor Bereinigung vom 2026-05-27 (`Internet -> Freigaben ->
|
|||||||
| `HTTPS-Server`, TCP, extern `443/tcp` auf `192.168.178.58` | entspricht Repo-Soll |
|
| `HTTPS-Server`, TCP, extern `443/tcp` auf `192.168.178.58` | entspricht Repo-Soll |
|
||||||
| Keine `222/tcp`-Freigabe sichtbar | entspricht seit 2026-05-28 dem Soll: Gitea-SSH bleibt Tailscale-only |
|
| Keine `222/tcp`-Freigabe sichtbar | entspricht seit 2026-05-28 dem Soll: Gitea-SSH bleibt Tailscale-only |
|
||||||
| Kallilabcore: keine selbststaendige Portfreigabe, kein IPv4-/IPv6-Exposed-Host sichtbar | entspricht Sicherheitsziel |
|
| Kallilabcore: keine selbststaendige Portfreigabe, kein IPv4-/IPv6-Exposed-Host sichtbar | entspricht Sicherheitsziel |
|
||||||
| `PC-192-168-178-71`: selbststaendige Portfreigabe erlaubt, `0 aktiv` | **2026-05-28 deaktiviert** |
|
| `PC-192-168-178-71`: selbststaendige Portfreigabe erlaubt, `0 aktiv` | **2026-06-01 deaktiviert**; danach nur noch `Kallilabcore` in der Portfreigabenliste sichtbar |
|
||||||
|
|
||||||
### Host (lokal beobachtbar)
|
### Host (lokal beobachtbar)
|
||||||
|
|
||||||
@@ -104,6 +246,7 @@ Historischer UI-Befund vor Bereinigung vom 2026-05-27 (`Internet -> Freigaben ->
|
|||||||
| 443/tcp | Traefik | HTTPS | WAN-Freigabe in FRITZ!Box erwartet |
|
| 443/tcp | Traefik | HTTPS | WAN-Freigabe in FRITZ!Box erwartet |
|
||||||
| 222/tcp | Gitea SSH | Git SSH | nur LAN/Tailscale; keine WAN-Freigabe |
|
| 222/tcp | Gitea SSH | Git SSH | nur LAN/Tailscale; keine WAN-Freigabe |
|
||||||
| 53/tcp+udp | AdGuard | DNS | LAN-only, dokumentierte Ausnahme |
|
| 53/tcp+udp | AdGuard | DNS | LAN-only, dokumentierte Ausnahme |
|
||||||
|
| 32400/tcp | Plex | Medienserver / Plex Web lokal | LAN/Tailscale direkt; extern nur via Traefik `https://plex.kaleschke.info`, keine WAN-Freigabe fuer 32400 |
|
||||||
| 8082/tcp | AdGuard Admin | Admin UI | Bind nur `100.80.98.33:8082` (Tailscale), nicht im LAN exponiert |
|
| 8082/tcp | AdGuard Admin | Admin UI | Bind nur `100.80.98.33:8082` (Tailscale), nicht im LAN exponiert |
|
||||||
| 8181/tcp | InfluxDB 3 Core | Home Assistant / Ecowitt Writer | 2026-05-31 effektiv nur `127.0.0.1:8181`, nicht LAN-exponiert |
|
| 8181/tcp | InfluxDB 3 Core | Home Assistant / Ecowitt Writer | 2026-05-31 effektiv nur `127.0.0.1:8181`, nicht LAN-exponiert |
|
||||||
|
|
||||||
@@ -118,9 +261,9 @@ docker ps --format "{{.Names}}: {{.Ports}}" | sort
|
|||||||
|
|
||||||
| Netz | Status | Bemerkung |
|
| Netz | Status | Bemerkung |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| LAN | 192.168.178.0/24 | Hauptnetz, Host `192.168.178.58`, FRITZ!Box meldet 36 aktive Geraete |
|
| LAN | 192.168.178.0/24 | Hauptnetz, Host `192.168.178.58`, FRITZ!Box meldet 35 aktive Geraete |
|
||||||
| WLAN 2,4 / 5 GHz | aktiv, SSID `Fritzi` | Standard-WLAN, im LAN-Adressbereich, kein eigener Adressraum |
|
| 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 |
|
| Gast-WLAN | aktiv, SSID `Fritzi Gastzugang` | FRITZ!Box-Gastnetz ist vom Heimnetz getrennt; Smoke 2026-06-06 vom iPhone bestaetigt keine Erreichbarkeit der getesteten LAN-/Admin-Ziele |
|
||||||
| IoT-Netz | nicht existent | Keine VLAN-Trennung dokumentiert |
|
| IoT-Netz | nicht existent | Keine VLAN-Trennung dokumentiert |
|
||||||
| Tailscale | aktiv | Operator-Zugang, Host-IP `100.80.98.33` |
|
| Tailscale | aktiv | Operator-Zugang, Host-IP `100.80.98.33` |
|
||||||
| VLANs | nicht in Nutzung | FRITZ!Box 7590 kann VLAN-Tagging an einzelnen LAN-Ports; aktuell nicht konfiguriert |
|
| VLANs | nicht in Nutzung | FRITZ!Box 7590 kann VLAN-Tagging an einzelnen LAN-Ports; aktuell nicht konfiguriert |
|
||||||
@@ -145,14 +288,136 @@ docker network inspect frontend_net | jq '.[0].Containers | keys'
|
|||||||
docker network inspect backend_net | jq '.[0].Internal'
|
docker network inspect backend_net | jq '.[0].Internal'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## SSH-Konfiguration Host
|
||||||
|
|
||||||
|
Geprueft 2026-06-06 (read-only), **gehaertet 2026-06-07** via `ssh root@192.168.178.58`.
|
||||||
|
|
||||||
|
| Parameter | Ist-Wert (effektiv via `sshd -T`, Stand 2026-06-07) | Soll | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `Port` | `22` | 22 | ok |
|
||||||
|
| `PermitRootLogin` | `prohibit-password` | `prohibit-password` | **gehaertet 2026-06-07** |
|
||||||
|
| `PasswordAuthentication` | `no` | `no` | **gehaertet 2026-06-07** |
|
||||||
|
| `KbdInteractiveAuthentication` | `no` | `no` | **gehaertet 2026-06-07** (noetig wegen `UsePAM yes`) |
|
||||||
|
| `PubkeyAuthentication` | `yes` | `yes` | ok |
|
||||||
|
| `PermitEmptyPasswords` | `no` | `no` | ok |
|
||||||
|
| `AuthorizedKeysFile` | `.ssh/authorized_keys` | `.ssh/authorized_keys` | ok |
|
||||||
|
|
||||||
|
**Hinterlegte SSH-Keys (root):** 3 Keys vorhanden (persistiert unter `/boot/config/ssh/root/authorized_keys`):
|
||||||
|
- `root@Kallilabcore` (Host-eigener Key)
|
||||||
|
- `michi@Baerchen` (Operator-Workstation)
|
||||||
|
- `hetzner-storagebox-maintenance-2026-06-01` (Hetzner-Maintenance-Key)
|
||||||
|
|
||||||
|
**Durchgefuehrte Haertung (2026-06-07):** Root-Login ist jetzt key-only,
|
||||||
|
Passwort- und Keyboard-Interactive-Auth sind serverseitig abgeschaltet.
|
||||||
|
Verifiziert: frischer Key-Login `OK`; `ssh -o PreferredAuthentications=none`
|
||||||
|
meldet `Authentications that can continue: publickey`; reiner Passwort-Versuch
|
||||||
|
`Permission denied (publickey)`.
|
||||||
|
|
||||||
|
**Wichtig — Unraid-Persistenz:** `/etc/ssh/sshd_config` wird beim Boot aus dem
|
||||||
|
OS-Image regeneriert (`rc.sshd`: `cp -f /boot/config/ssh/* /etc/ssh/`, danach
|
||||||
|
`sshd_build`, das nur `Port`/`ListenAddress`/`AddressFamily` setzt). Die
|
||||||
|
Unraid-GUI (**Settings → Management Access → SSH**) bietet nur `Use SSH`/`SSH port`
|
||||||
|
an — **`PermitRootLogin`/`PasswordAuthentication` sind dort nicht einstellbar.**
|
||||||
|
Persistiert wird daher **upgrade-sicher** ueber einen idempotenten Hook:
|
||||||
|
|
||||||
|
- `/boot/config/ssh-harden.sh` — setzt die drei Direktiven idempotent (bestehende
|
||||||
|
aktive Zeile entfernen, genau einmal global vor dem ersten `Match`-Block einfuegen),
|
||||||
|
`sshd -t`-Validierung, Reload nur per `kill -HUP` des Host-`sshd` bei valider Config.
|
||||||
|
Idempotenz belegt: nach mehreren Laeufen je Direktive exakt 1 aktive Zeile, alte
|
||||||
|
`PermitRootLogin yes` entfernt.
|
||||||
|
- `/boot/config/go` — ruft `/bin/bash /boot/config/ssh-harden.sh` bei jedem Boot auf.
|
||||||
|
|
||||||
|
**Selbst-Verifikation (Syslog, rein informativ, keine Reparatur):** Das Skript
|
||||||
|
schreibt nach jedem Lauf die effektiven Auth-Werte (`sshd -T`) nach syslog, z. B.
|
||||||
|
`ssh-harden: VERIFY permitrootlogin prohibit-password pubkeyauthentication yes
|
||||||
|
passwordauthentication no kbdinteractiveauthentication no`. Damit ist nach jedem
|
||||||
|
Boot/Upgrade nachweisbar, ob die Haertung gegriffen hat.
|
||||||
|
|
||||||
|
**Post-Upgrade-/Reboot-Check** (manuell, einmal nach jedem Unraid-Upgrade):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# A) Effektive Werte direkt abfragen (Soll: prohibit-password / no / no / yes)
|
||||||
|
ssh root@192.168.178.58 "sshd -T | grep -Ei 'permitroot|passwordauth|kbdinteractive|pubkey'"
|
||||||
|
# B) Oder die automatische VERIFY-Zeile im Syslog lesen (Unraid nutzt rsyslog -> /var/log/syslog, nicht logread)
|
||||||
|
ssh root@192.168.178.58 "grep 'ssh-harden' /var/log/syslog | tail -3"
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieser Weg editiert die **jeweils aktuelle** von Unraid generierte Config nach und
|
||||||
|
ueberlebt damit Unraid-Upgrades; findet er die Stock-Zeile nicht (z. B. weil eine
|
||||||
|
neue Version schon `prohibit-password` ausliefert), macht der `sed` nichts und
|
||||||
|
bricht den Boot nicht (fail-safe Richtung offen, nicht ausgesperrt). Bewusst
|
||||||
|
**nicht** der oft empfohlene Weg einer kompletten `/boot/config/ssh/sshd_config`
|
||||||
|
auf Flash — der wuerde die Stock-Config einfrieren und beim Upgrade neue Defaults
|
||||||
|
verschlucken.
|
||||||
|
|
||||||
|
**Rollback:** `go`-Block + `/boot/config/ssh-harden.sh` entfernen, dann
|
||||||
|
`cp /boot/config/ssh-harden.sshd_config.bak-20260607 /etc/ssh/sshd_config` und
|
||||||
|
`kill -HUP $(cat /var/run/sshd.pid)`. Notzugang ueber Unraid-Konsole/GUI bleibt.
|
||||||
|
|
||||||
|
**Abgrenzung:** Ein zweiter `sshd` (`-D -e`) laeuft in einem Docker-Container
|
||||||
|
(s6-overlay, moby-Namespace) und bindet **nicht** den Host-`:22`; eigene Config
|
||||||
|
im Container, von dieser Haertung unberuehrt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Upgrade Posture-Recheck — Unraid 7.3.1 (2026-06-07)
|
||||||
|
|
||||||
|
Nach dem Major-Upgrade **7.2.4 → 7.3.1** read-only die Host-Listener-Landschaft
|
||||||
|
(`ss -tlnp`) gegen die dokumentierten Annahmen geprueft.
|
||||||
|
|
||||||
|
**Dokumentierte Ausnahmen verifiziert (alle weiterhin gueltig):**
|
||||||
|
|
||||||
|
| Dienst | Soll | Ist nach 7.3.1 | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| InfluxDB 3 | nur `127.0.0.1:8181` | `127.0.0.1:8181` | ✅ |
|
||||||
|
| AdGuard-Admin | nur Tailscale `100.80.98.33:8082` | `100.80.98.33:8082` | ✅ |
|
||||||
|
| Gitea-SSH `222` | LAN/Tailscale, keine WAN-Freigabe | `0.0.0.0:222` (LAN/TS), WAN am Router zu | ✅ |
|
||||||
|
| Traefik `80/443` | einziger Owner | docker-proxy (Traefik) allein | ✅ |
|
||||||
|
| libvirt `:53` | darf nicht existieren | **weg** (Fix vom 2026-06-07 haelt) | ✅ |
|
||||||
|
|
||||||
|
**Docker-Socket (`/var/run/docker.sock`) — C-3-Kontext:**
|
||||||
|
|
||||||
|
| Container | Mount | Bewertung |
|
||||||
|
|---|---|---|
|
||||||
|
| komodo-periphery | **RW** | dokumentierte Ausnahme (Periphery startet/stoppt Container) |
|
||||||
|
| traefik | ro | C-3: Direkt-Mount (ro), nicht ueber Socket-Proxy — offener Audit-Punkt, kein Regress |
|
||||||
|
| glances / monitoring-promtail / glance-docker-socket-proxy | ro | unkritisch |
|
||||||
|
|
||||||
|
Keine neue RW-Socket-Exposure durch das Upgrade.
|
||||||
|
|
||||||
|
**Vorfall-Notiz AdGuard/DNS (Boot-Race, behoben 2026-06-07):** Das Upgrade hatte das
|
||||||
|
ungenutzte **libvirt-Default-Netz** auf Autostart gebracht; dessen `dnsmasq` belegte
|
||||||
|
beim Boot Port `53` **vor** AdGuard → AdGuards erster Start scheiterte am Bind und
|
||||||
|
liess den Container ohne Netz-Anbindung (`Networks={}`, keine Ports) zurueck. Fix:
|
||||||
|
`virsh net-autostart default --disable` + `virsh net-destroy default` (kein VM
|
||||||
|
betroffen, Liste leer) + AdGuard-Container aus der Compose `--force-recreate`
|
||||||
|
(re-attach `dns_net`, `:53` neu veroeffentlicht). DNS danach verifiziert aufloesend.
|
||||||
|
`libvirtd` laeuft weiter nur auf `127.0.0.1:16509`.
|
||||||
|
|
||||||
|
**Empfehlung (Dauerfix):** Da keine VMs genutzt werden, **Unraid VM Manager → Enable
|
||||||
|
VMs = No** — dann startet `libvirtd` gar nicht und der `:53`-Konflikt kann prinzipiell
|
||||||
|
nicht wiederkehren. Bis dahin verhindert der abgeschaltete Autostart die Wiederkehr.
|
||||||
|
|
||||||
|
**Beobachtungen (kein Regress, Inventar):** SMB (`:445/:139`) und Plex (`*:32400`)
|
||||||
|
lauschen auch auf der Tailscale-IP; durch die seit 2026-06-06 tag-restriktive
|
||||||
|
Tailnet-ACL akzeptabel.
|
||||||
|
|
||||||
|
**SSH-Haertung nach Upgrade:** key-only root unveraendert aktiv und verifiziert
|
||||||
|
(`prohibit-password`/`password no`/`kbd no`), go-Hook genau 1× gefeuert — siehe
|
||||||
|
Abschnitt „SSH-Konfiguration Host".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Offene Entscheidungen
|
## Offene Entscheidungen
|
||||||
|
|
||||||
| Thema | Status | Naechster Schritt |
|
| 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 |
|
| 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 |
|
||||||
| 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!Box-Portfreigaben mit Repo-Soll abgleichen | **erledigt 2026-06-01** | 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-Selbstfreigaben sind aus. 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 |
|
| FRITZ!Box-Dienste aus dem Internet | **erledigt 2026-06-01** | `Internet -> Freigaben -> FRITZ!Box-Dienste`: HTTPS-Zugriff auf die FRITZ!Box aus dem Internet aus; FTP/FTPS auf Speichermedien aus. |
|
||||||
| 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 |
|
| FRITZ!OS Update und Konfig-Backup | **erledigt 2026-06-01** | TR-064 meldet `154.08.25`; Konfig-Export liegt extern/off-system in Vaultwarden, Kennwort und Datei bleiben ausserhalb des Repos. |
|
||||||
| IPv6 Exposure | offen | Router und Traefik/Cloudflare pruefen; Telekom-DSL liefert in der Regel IPv6, FRITZ!Box-Standard-Verhalten klaeren |
|
| Gast-/IoT-Zugriff auf Admin-Ports | **validiert 2026-06-06** | Runbook `docs/GUEST_IOT_NETWORK.md` und Checks `ops/maintenance/check-guest-iot-isolation.ps1` sowie `ops/maintenance/check-guest-iot-preflight.sh` vorhanden. LAN-Preflight von `baerchen` gruen: `192.168.178.58:8082` und `:8181` blockiert. Host-Preflight auf Unraid gruen, Report `/mnt/user/backups/restore-reports/guest-iot-preflight-2026-06-06-131316.md`. Gast-WLAN-Smoke per iPhone: `192.168.178.58:8082`, `:8181`, `:222`, `https://192.168.178.58` und `192.168.178.1` nicht erreichbar. |
|
||||||
| WAN-Ausfallschutz | bewusst nicht eingerichtet | Mobilfunk-Stick-Failover an FRITZ!Box ist nicht aktiv; Internet-Ausfall = ACME/DDNS pausieren, lokale Apps laufen weiter |
|
| IPv6 Exposure | technisch und per UI entschaerft | Public DNS liefert keine AAAA-Records fuer `*.kaleschke.info`; Host hat keine globale Provider-IPv6. TR-064 meldet IPv6-Firewall aktiv und Pinholes grundsaetzlich erlaubt; FRITZ!Box-UI zeigt keine aktiven IPv6-Freigaben, keine Admin-/SSH-Freigaben. |
|
||||||
|
| WAN-Ausfallschutz | **geparkt: spaeter evaluieren** (Operator-Entscheidung 2026-06-05) | Mobilfunk-Stick-Failover an FRITZ!Box bleibt vorerst inaktiv. Folgen sind bewusst akzeptiert: Internet-Ausfall = ACME/DDNS pausieren, lokale Apps laufen weiter. Review-Trigger: haeufigere oder laengere DSL-Ausfaelle, oder wenn externer Remote-Zugang (statt nur lokalem Betrieb) geschaeftskritisch wird. Erst dann Mobilfunk-Failover technisch bewerten. |
|
||||||
| Home Assistant InfluxDB Bind | validiert 2026-05-31 | `docker-proxy` bindet `127.0.0.1:8181`; keine LAN-Exposure. Wenn Home Assistant nicht lokal auf dem Host schreibt, braucht das eine bewusste Bind-Aenderung. |
|
| Home Assistant InfluxDB Bind | validiert 2026-05-31 | `docker-proxy` bindet `127.0.0.1:8181`; keine LAN-Exposure. Wenn Home Assistant nicht lokal auf dem Host schreibt, braucht das eine bewusste Bind-Aenderung. |
|
||||||
|
| SSH-Haertung Host | **erledigt 2026-06-07** | Root-Login key-only: `PermitRootLogin prohibit-password`, `PasswordAuthentication no`, `KbdInteractiveAuthentication no`. Live gesetzt + verifiziert (Key-Login ok, Passwort-Auth abgelehnt). Persistenz upgrade-sicher ueber `/boot/config/ssh-harden.sh` (idempotent, `sshd -t` vor Reload) aufgerufen aus `/boot/config/go`. GUI bietet diese Optionen nicht. Details im Abschnitt „SSH-Konfiguration Host". |
|
||||||
|
|||||||
+8
-2
@@ -1,6 +1,6 @@
|
|||||||
# Documentation Index
|
# Documentation Index
|
||||||
|
|
||||||
Stand: 2026-05-31
|
Stand: 2026-06-05
|
||||||
|
|
||||||
Diese Datei trennt aktive Betriebsdokumentation von historischer Arbeitsdoku. Neue operative Dokumente duerfen nur in `docs/` liegen, wenn sie heute als Einstieg, Runbook, Inventar oder offene Arbeitsliste gebraucht werden. Erledigte Audits, Chat-Handoffs, Prompt-Dateien und abgeschlossene Plaene bleiben in der Git-Historie, aber nicht als dauerhafte Arbeitskopie.
|
Diese Datei trennt aktive Betriebsdokumentation von historischer Arbeitsdoku. Neue operative Dokumente duerfen nur in `docs/` liegen, wenn sie heute als Einstieg, Runbook, Inventar oder offene Arbeitsliste gebraucht werden. Erledigte Audits, Chat-Handoffs, Prompt-Dateien und abgeschlossene Plaene bleiben in der Git-Historie, aber nicht als dauerhafte Arbeitskopie.
|
||||||
|
|
||||||
@@ -31,9 +31,12 @@ Diese Datei trennt aktive Betriebsdokumentation von historischer Arbeitsdoku. Ne
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `STORAGE_LAYOUT.md` | verbindliche Storage-/Share-/Pfad-Regeln |
|
| `STORAGE_LAYOUT.md` | verbindliche Storage-/Share-/Pfad-Regeln |
|
||||||
| `SECRETS_MAP.md` | Secret-Namen, Speicherorte und Einbindungsarten ohne Werte |
|
| `SECRETS_MAP.md` | Secret-Namen, Speicherorte und Einbindungsarten ohne Werte |
|
||||||
|
| `AUTHELIA_OIDC_PLAN.md` | Plan & Runbook fuer app-uebergreifendes SSO via Authelia OIDC |
|
||||||
| `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART-, USV- und Power-Baseline |
|
| `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART-, USV- und Power-Baseline |
|
||||||
| `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen |
|
| `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen |
|
||||||
|
| `GUEST_IOT_NETWORK.md` | Sicherer Ablauf fuer FRITZ!Box-Gastnetz / IoT-Isolation |
|
||||||
| `EXTERNAL_DEPENDENCIES.md` | Provider, Konten und externe Abhaengigkeiten |
|
| `EXTERNAL_DEPENDENCIES.md` | Provider, Konten und externe Abhaengigkeiten |
|
||||||
|
| `EXTERNAL_OPERATOR_RUNBOOK.md` | Hetzner-/Borg-/FRITZ!Box-Betreibercheck |
|
||||||
| `CAPACITY_AND_LIFECYCLE.md` | Kapazitaet, Wachstum und Upgrade-Trigger |
|
| `CAPACITY_AND_LIFECYCLE.md` | Kapazitaet, Wachstum und Upgrade-Trigger |
|
||||||
|
|
||||||
## Monitoring und Automatisierung
|
## Monitoring und Automatisierung
|
||||||
@@ -42,7 +45,7 @@ Diese Datei trennt aktive Betriebsdokumentation von historischer Arbeitsdoku. Ne
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `ALERT_RULES.md` | Prometheus-/ntfy-Regeln und Handlungslogik |
|
| `ALERT_RULES.md` | Prometheus-/ntfy-Regeln und Handlungslogik |
|
||||||
| `RENOVATE.md` | Self-hosted Renovate gegen Gitea |
|
| `RENOVATE.md` | Self-hosted Renovate gegen Gitea |
|
||||||
| `HOME_ASSISTANT_INFLUXDB_ECOWITT.md` | Home Assistant -> InfluxDB 3 -> Grafana |
|
| `HOME_ASSISTANT_INFLUXDB_ECOWITT.md` | Archivierter Entwurf: Home Assistant -> InfluxDB 3 -> Grafana; nicht aktiv seit Crash |
|
||||||
| `H_DRIVE_NEARLINE_PULL.md` | Windows-H:/ Nearline-Pull fuer kritische Restore-Artefakte |
|
| `H_DRIVE_NEARLINE_PULL.md` | Windows-H:/ Nearline-Pull fuer kritische Restore-Artefakte |
|
||||||
|
|
||||||
## Nutzer- und Planungsdoku
|
## Nutzer- und Planungsdoku
|
||||||
@@ -51,6 +54,9 @@ Diese Datei trennt aktive Betriebsdokumentation von historischer Arbeitsdoku. Ne
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `FAMILY_ONBOARDING.md` | familienverstaendliche Nutzungsdoku |
|
| `FAMILY_ONBOARDING.md` | familienverstaendliche Nutzungsdoku |
|
||||||
| `AUDIT_2026-05-25_TODO.md` | kompakte Restliste aus dem Audit-Zyklus |
|
| `AUDIT_2026-05-25_TODO.md` | kompakte Restliste aus dem Audit-Zyklus |
|
||||||
|
| `MASTER_TODO.md` | zentrale operative Master-To-do-Liste ueber alle Bereiche |
|
||||||
|
| `WEEKEND_EXECUTION_PLAN_2026-06-05.md` | Owner-Aufteilung und Wochenendplan fuer Todo-Abschluss |
|
||||||
|
| `WEEKEND_STATUS_2026-06-05.md` | kurzlebiges Arbeitsboard fuer den laufenden Wochenend-Sprint |
|
||||||
| `AI_CONTEXT.md` | kompakter Kontext fuer KI-Agenten |
|
| `AI_CONTEXT.md` | kompakter Kontext fuer KI-Agenten |
|
||||||
|
|
||||||
Windows-Neuaufsetzen-Dokumente liegen nicht mehr in `docs/`, sondern im fachlich passenden Ordner `../ops/windows-reinstall/docs/`.
|
Windows-Neuaufsetzen-Dokumente liegen nicht mehr in `docs/`, sondern im fachlich passenden Ordner `../ops/windows-reinstall/docs/`.
|
||||||
|
|||||||
+9
-1
@@ -93,7 +93,15 @@ Script: bash /mnt/user/services/homelab-infra/ops/renovate/run-renovate.sh
|
|||||||
| Schedule | `extends ["schedule:weekly"]` | Renovate-Engine prueft, aber PRs/Updates folgen Wochen-Profilen wo sinnvoll |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Ignore-Pfade | `_archive`, `ops/grafana-influxdb`, `ops/loki`, `ops/komodo` | Renovate scant alte/abgeloeste Stacks nicht; `ops/komodo` ist bewusst raus (siehe unten) |
|
||||||
|
|
||||||
|
## Ausnahme: komodo-Stack ist inline-verwaltet, nicht git-deployed
|
||||||
|
|
||||||
|
Der `komodo`-Stack (Komodo-Core/Mongo/Periphery, Datei `ops/komodo/docker-compose.yml`) wird **nicht aus diesem Repo deployed**. In Komodo ist der Stack als **inline `file_contents`** (UI-defined) gespeichert (`repo` leer, `files_on_host=false`, `has_inline_file_contents=true`) und hat bewusst `webhook_enabled=false`, damit Komodo sich nicht selbst per Webhook recreated (Bootstrap-/Henne-Ei-Fall).
|
||||||
|
|
||||||
|
Konsequenz: Ein Renovate-PR auf `ops/komodo/docker-compose.yml` wirkt zur Laufzeit **nicht** (Komodo deployt aus seiner Inline-Definition) und erzeugt nur Git↔Komodo-Scheinsicherheit. Deshalb steht `ops/komodo/**` in `ignorePaths`. Die Repo-Datei bleibt als Doku/Spiegel und traegt den aktuell real laufenden Digest.
|
||||||
|
|
||||||
|
Befund-Datum 2026-06-10: Renovate-PR #13 (mongo-8.0.23 Digest-Refresh) wurde gemergt, wirkte aber nicht; der Digest wurde im Repo auf den laufenden Stand zurueckgesetzt und der Pfad ausgenommen. Echte Updates des komodo-Stacks laufen bis auf Weiteres manuell ueber Komodo (Inline-Compose anpassen) bzw. spaeter via Migration auf git-backed (eigener Aenderungsblock).
|
||||||
|
|
||||||
## Aktueller Betriebsstand
|
## Aktueller Betriebsstand
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ Details gilt immer die betroffene Compose-Datei oder das jeweilige Runbook.
|
|||||||
| `docs/SECRETS_MAP.md` | Secret-Namen und Pfade ohne Werte |
|
| `docs/SECRETS_MAP.md` | Secret-Namen und Pfade ohne Werte |
|
||||||
| `docs/GITOPS_DRIFT_RUNBOOK.md` | Git/Gitea/Komodo/Docker/Host-Drift |
|
| `docs/GITOPS_DRIFT_RUNBOOK.md` | Git/Gitea/Komodo/Docker/Host-Drift |
|
||||||
| `docs/AUDIT_2026-05-25_TODO.md` | aktuelle Restliste |
|
| `docs/AUDIT_2026-05-25_TODO.md` | aktuelle Restliste |
|
||||||
|
| `docs/DR_WORKSTATION_SETUP.md` | Schritt-fuer-Schritt-Runbook fuer den DR-Gaming-PC (WSL2 + Borg-Client + SSH-Keys) |
|
||||||
|
| `docs/runbooks/komodo-bulk-deploy-dns.md` | Bulk-Deploy-Pulls scheitern an DNS, wenn AdGuard im selben Batch recreated wird |
|
||||||
|
|
||||||
## Wichtige Skripte
|
## Wichtige Skripte
|
||||||
|
|
||||||
|
|||||||
+90
-46
@@ -1,6 +1,6 @@
|
|||||||
# Restore Handbook - KalliLab CORE
|
# Restore Handbook - KalliLab CORE
|
||||||
|
|
||||||
Stand: 2026-05-07
|
Stand: 2026-06-03
|
||||||
|
|
||||||
Dieses Handbuch ist die praktische Betriebsanleitung fuer Restore-Checks und Restore-Lab in KalliLab CORE.
|
Dieses Handbuch ist die praktische Betriebsanleitung fuer Restore-Checks und Restore-Lab in KalliLab CORE.
|
||||||
|
|
||||||
@@ -41,28 +41,36 @@ Alle validierten Restore-Tests folgen demselben Muster:
|
|||||||
|
|
||||||
### Vaultwarden
|
### Vaultwarden
|
||||||
|
|
||||||
- Report: `/mnt/user/backups/restore-reports/vaultwarden-2026-05-07.md`
|
- Erstlauf: 2026-05-07
|
||||||
- Nachweis:
|
- Nachweis: Borg-Restore, Testcontainer, Login-Seite erreichbar
|
||||||
- Borg-Restore erfolgreich
|
|
||||||
- Testcontainer startete
|
|
||||||
- Login-Seite war erreichbar
|
|
||||||
|
|
||||||
### Gitea
|
### Gitea
|
||||||
|
|
||||||
- Report: `/mnt/user/backups/restore-reports/gitea-2026-05-07.md`
|
- Erstlauf: 2026-05-07
|
||||||
- Nachweis:
|
- Nachweis: Borg-Restore, Web-UI, SSH-TCP-Port
|
||||||
- Borg-Restore erfolgreich
|
|
||||||
- Web-UI antwortete
|
|
||||||
- SSH-Port reagierte
|
|
||||||
|
|
||||||
### Paperless
|
### Paperless
|
||||||
|
|
||||||
- Report: `/mnt/user/backups/restore-reports/paperless-2026-05-07.md`
|
- Erstlauf: 2026-05-07, Folgelauf: 2026-05-31
|
||||||
- Nachweis:
|
- Nachweis: Borg-Datei-Restore, Dump-Import in Test-Postgres, Login-Seite, Doc-Count
|
||||||
- Borg-Datei-Restore erfolgreich
|
|
||||||
- Paperless-Dump aus Borg importiert
|
### Immich
|
||||||
- Login-Seite war erreichbar
|
|
||||||
- Test-DB enthielt `25` Dokumente
|
- Erstlauf: 2026-05-27
|
||||||
|
- Nachweis: DB-Dump-Restore in VectorChord-Test-Postgres, HTTP-Smoke, Asset-Count
|
||||||
|
- Hinweis: Foto-Dateien-Restore ist bewusst nicht Teil des Smokes
|
||||||
|
|
||||||
|
### Authelia
|
||||||
|
|
||||||
|
- Erstlauf: 2026-06-03
|
||||||
|
- Nachweis: Config-Borg-Restore, `authelia config validate`, HTTP-Health `/api/health`
|
||||||
|
- Hinweis: Daten-Restore des produktiven Dumps ist bewusst nicht Teil des Smokes (Storage-Encryption-Key-Kopplung)
|
||||||
|
|
||||||
|
### Komodo Bootstrap
|
||||||
|
|
||||||
|
- Erstlauf: 2026-05-30
|
||||||
|
- Nachweis: Compose-Validierung, Mongo healthy, Core HTTP, Periphery running
|
||||||
|
- Hinweis: Daten-Restore aus `komodo-mongo.archive.gz` ist noch nicht getestet
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -80,6 +88,10 @@ Alle validierten Restore-Tests folgen demselben Muster:
|
|||||||
- `/mnt/user/backups/restore-lab/vaultwarden`
|
- `/mnt/user/backups/restore-lab/vaultwarden`
|
||||||
- `/mnt/user/backups/restore-lab/gitea`
|
- `/mnt/user/backups/restore-lab/gitea`
|
||||||
- `/mnt/user/backups/restore-lab/paperless`
|
- `/mnt/user/backups/restore-lab/paperless`
|
||||||
|
- `/mnt/user/backups/restore-lab/immich`
|
||||||
|
- `/mnt/user/backups/restore-lab/authelia`
|
||||||
|
- `/mnt/user/backups/restore-lab/komodo`
|
||||||
|
- `/mnt/user/backups/restore-lab/_failed` (Diagnose-Material bei Fehllaeufen)
|
||||||
|
|
||||||
### Reports
|
### Reports
|
||||||
|
|
||||||
@@ -89,31 +101,33 @@ Alle validierten Restore-Tests folgen demselben Muster:
|
|||||||
|
|
||||||
## 5. Restore-Frequenz
|
## 5. Restore-Frequenz
|
||||||
|
|
||||||
- jeden Montag, 06:30:
|
- jeden Montag, 06:30: Frische-Check fuer Dumps und Reports
|
||||||
- Frische-Check fuer Dumps und Reports
|
- 1. Samstag im Monat, 07:00: Vaultwarden
|
||||||
- 1. Samstag im Monat, 07:00:
|
- 3. Samstag im Monat, 07:15: Gitea
|
||||||
- Vaultwarden
|
- 2. Samstag in ungeraden Monaten, 08:00: Paperless
|
||||||
- 3. Samstag im Monat, 07:00:
|
- 2. Sonntag in Feb/Mai/Aug/Nov, 08:30: Immich
|
||||||
- Gitea
|
- 2. Samstag in geraden Monaten, 07:30: Authelia
|
||||||
- jeder 2. Monat, 2. Samstag, 08:00:
|
- 1. Kalendertag im Monat, 09:00: Zufaelliger Restore aus Pool
|
||||||
- Paperless
|
|
||||||
|
Vollstaendiger Kalender mit Cron-Ausdruecken und Shell-Guards steht in `ops/restore-tests/schedule.md`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Betriebsmodi
|
## 6. Betriebsmodus
|
||||||
|
|
||||||
### V1
|
Stand 2026-06-03 ist der Betrieb auf V1+ (V1 mit ntfy):
|
||||||
|
|
||||||
- validierte Bash-Host-Jobs
|
- validierte Bash-Host-Jobs fuer Vaultwarden, Gitea, Paperless, Immich, Authelia, Komodo-Bootstrap
|
||||||
- Host-Job-Definitionen liegen im Repo
|
- Host-Job-Definitionen und Cron-Vorlagen liegen im Repo (`ops/restore-tests/unraid-user-scripts.md`)
|
||||||
- Scheduler kann bereits echte Frische- und Restore-Checks fahren
|
- `ntfy`-Wrapper sendet Erfolg an `homelab-info`, Fehler an `homelab-alerts`
|
||||||
- `ntfy` und Hermes-Auswertung folgen danach
|
- Frische-Check prueft zusaetzlich pg-Custom-Format-Dumps per `pg_restore --list` Header-Validierung
|
||||||
|
- bei Fehlschlag wird das Restore-Lab nach `_failed/` verschoben statt geloescht
|
||||||
|
|
||||||
### V2
|
Noch geplant fuer V2:
|
||||||
|
|
||||||
- `ntfy` bei Erfolg/Fehler
|
- Hermes-Zusammenfassung ueber vorhandene Reports
|
||||||
- Hermes liest Reports und baut Uebersichten
|
- Sammelreports und Report-Rotation
|
||||||
- zusaetzliche Rotation, Sammelreports und weitere Dienste
|
- weitere Dienste (Nextcloud, Mailarchiver, Mealie)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -126,15 +140,18 @@ Die Vorlagen stehen in:
|
|||||||
Host-Repo-Pfad:
|
Host-Repo-Pfad:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/mnt/user/services/homelab
|
/mnt/user/services/homelab-infra
|
||||||
```
|
```
|
||||||
|
|
||||||
V1-Jobs:
|
Jobs:
|
||||||
|
|
||||||
1. `restore-freshness-weekly`
|
1. `restore-freshness-weekly`
|
||||||
2. `restore-vaultwarden-monthly`
|
2. `restore-vaultwarden-monthly`
|
||||||
3. `restore-gitea-monthly`
|
3. `restore-gitea-monthly`
|
||||||
4. `restore-paperless-bimonthly`
|
4. `restore-paperless-bimonthly`
|
||||||
|
5. `restore-immich-quarterly`
|
||||||
|
6. `restore-authelia-bimonthly`
|
||||||
|
7. `monthly-random-restore`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -169,38 +186,65 @@ Nur `Container laeuft` reicht nicht.
|
|||||||
Auf dem Unraid-Host:
|
Auf dem Unraid-Host:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh freshness
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
```
|
```
|
||||||
|
|
||||||
### Vaultwarden Restore-Check
|
### Vaultwarden Restore-Check
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh vaultwarden
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh vaultwarden
|
||||||
```
|
```
|
||||||
|
|
||||||
### Gitea Restore-Check
|
### Gitea Restore-Check
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh gitea
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh gitea
|
||||||
```
|
```
|
||||||
|
|
||||||
### Paperless Restore-Check
|
### Paperless Restore-Check
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-checks.sh paperless
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh paperless
|
||||||
|
```
|
||||||
|
|
||||||
|
### Immich Restore-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh immich
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authelia Restore-Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh authelia
|
||||||
|
```
|
||||||
|
|
||||||
|
### Komodo Bootstrap Trockenlauf
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh komodo-bootstrap
|
||||||
```
|
```
|
||||||
|
|
||||||
### Optional mit `ntfy`
|
### Optional mit `ntfy`
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash /mnt/user/services/homelab/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-job-with-ntfy.sh freshness homelab-info
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 11. Naechste Ausbaustufen
|
## 11. Naechste Ausbaustufen
|
||||||
|
|
||||||
1. Vollautomatik fuer Vaultwarden, Gitea und Paperless
|
1. Nextcloud-Restore-Test (mit `occ maintenance:mode`-Choreographie)
|
||||||
2. `ntfy`-Meldungen fuer Erfolg/Fehler
|
2. Mailarchiver-Restore-Test
|
||||||
3. Hermes-Zusammenfassung ueber vorhandene Reports
|
3. Mealie-Restore-Test
|
||||||
4. naechster Referenz-Restore fuer `mail-archiver` oder `mealie`
|
4. Komodo-Mongo-Daten-Restore (echtes `mongorestore` statt reinem Bootstrap)
|
||||||
|
5. Shared-PostgreSQL-18-Cluster-Restore-Drill (globals + per-DB-Dumps)
|
||||||
|
6. Traefik-Restore-Test (mit `dynamic/` und LE-State)
|
||||||
|
7. Hermes-Zusammenfassung ueber vorhandene Reports
|
||||||
|
8. Report-Rotation (archivieren nach 12 Monaten)
|
||||||
|
9. Negativ-Test: bewusst kaputten Dump in den Frische-Check einfuettern
|
||||||
|
|
||||||
|
## 12. Report-Aufbewahrung
|
||||||
|
|
||||||
|
Reports unter `/mnt/user/backups/restore-reports` werden dauerhaft aufbewahrt. Bei wachsender Anzahl (ca. 50-60 pro Jahr) empfiehlt sich eine jaehrliche Archivierung alter Reports in einen Unterordner `_archive/YYYY/`. Der Frische-Check warnt bei `MAX_REPORT_AGE_DAYS=45`, loescht aber bewusst nicht automatisch.
|
||||||
|
|||||||
+224
-19
@@ -28,15 +28,23 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| Unraid OS Flash | Borg-Artefakt + optional Unraid Connect | `/boot/config` aus `unraid-flash-config.tar.gz` | `unraid-flash-config.tar.gz`, `.sha256`, Manifest | enthaelt sensible Host-Konfiguration, wie Secret-Material behandeln | Unraid USB Flash Creator / neuer Boot-Stick | Unraid bootet, Array-Zuordnung und Shares sind sichtbar |
|
| Unraid OS Flash | Borg-Artefakt + optional Unraid Connect | `/boot/config` aus `unraid-flash-config.tar.gz` | `unraid-flash-config.tar.gz`, `.sha256`, Manifest | enthaelt sensible Host-Konfiguration, wie Secret-Material behandeln | Unraid USB Flash Creator / neuer Boot-Stick | Unraid bootet, Array-Zuordnung und Shares sind sichtbar |
|
||||||
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
|
| Traefik | Share / Borg | `/mnt/user/appdata/traefik`, besonders `dynamic/`, `letsencrypt`, `secrets` | keine eigene DB | `cloudflare_dns_api_token` | `frontend_net`, `backend_net` | `https://traefik.kaleschke.info` erreichbar, Dashboard ueber Authelia |
|
||||||
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert |
|
| AdGuard Home | Share / Borg | `/mnt/user/appdata/adguard/conf` | keine | keine zusaetzlichen Repo-Secrets dokumentiert | `dns_net`, `frontend_net` | DNS-Aufloesung funktioniert; Restore-Smoke am 2026-06-06 erfolgreich |
|
||||||
| Tailscale | Share / Borg | `/mnt/user/appdata/tailscale` | keine | Tailscale-State im Pfad | Host-Netz | Tailscale verbunden |
|
| Tailscale | Flash-Backup (funktional) / Share | **Funktional: `/boot/config/plugins/tailscale/state`** (native Unraid-Plugin-Instanz `kallilabcore`, Subnet-Router, im Flash-Backup gesichert). Der frueher hier genannte Pfad `/mnt/user/appdata/tailscale` gehoert zum **userspace-only Docker-Stack** `kallilab-core` (redundant, Abbau geplant — siehe `docs/NETWORK_INVENTORY.md`) | keine | Tailscale-State im jeweiligen State-Pfad | Host-Netz | Tailscale verbunden, Subnet-Route `192.168.178.0/24` aktiv |
|
||||||
| PostgreSQL 18 | Share + Dumps | `/mnt/user/appdata/postgresql18` (Rollback-Altstand: `/mnt/user/appdata/postgresql17`) | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt`, App-Rollen-Passwoerter aus den jeweiligen Stack-ENV/Secret-Dateien | `backend_net` | DB startet, Ziel-Datenbanken vorhanden; `SHOW data_checksums` ist `on` |
|
| PostgreSQL 18 | Share + Dumps | `/mnt/user/appdata/postgresql18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17`) | `postgresql17-globals.sql`, `postgresql17-mailarchiver.dump`, `postgresql17-paperless.dump`, optional `postgresql17-authelia.dump` | `postgres_password.txt`, App-Rollen-Passwoerter aus den jeweiligen Stack-ENV/Secret-Dateien | `backend_net` | DB startet, Ziel-Datenbanken vorhanden; `SHOW data_checksums` ist `on` |
|
||||||
| Redis 8 | Share / Host | `/mnt/user/appdata/redis`; Rollback-Backup unter `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>` | RDB/AOF-Dateien im Datenpfad | `redis_password.txt` | `backend_net` | Redis startet, `redis_version` ist 8.x, Apps verbinden sich |
|
| Redis 8 | Share / Host | `/mnt/user/appdata/redis`; Rollback-Backup unter `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>` | RDB/AOF-Dateien im Datenpfad | `redis_password.txt` | `backend_net` | Redis startet, `redis_version` ist 8.x, Apps verbinden sich; Restore-Smoke am 2026-06-06 erfolgreich |
|
||||||
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL 18, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 18, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut |
|
| Authelia | Borg | `/mnt/user/appdata/authelia/config`, `/mnt/user/appdata/secrets/*authelia*` | Shared PostgreSQL 18, optional Dump `postgresql17-authelia.dump` | JWT/Session/Storage/Postgres-/SMTP-Secret-Dateien | PostgreSQL 18, Traefik, GMX SMTP | Login-Seite und ForwardAuth funktionieren; SMTP-Notifier startet; aktive Sessions werden nach Restart neu aufgebaut; Restore-Smoke am 2026-06-03 erfolgreich: Config aus Borg, minimale Test-Config, frisches Test-Postgres, HTTP `/api/health` 200, Report `/mnt/user/backups/restore-reports/authelia-2026-06-03.md` |
|
||||||
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| Vaultwarden | Borg + Dump | `/mnt/user/appdata/vaultwarden` | `vaultwarden.sqlite.dump` | `vaultwarden_admin_token.txt` fuer Produktion; Restore-Test nutzt Wegwerf-Admin-Token und `borg_repo_passphrase.txt` | Traefik | Login-Seite erreichbar, Tresor-Daten sichtbar; Mini-Restore nach `/mnt/user/backups/restore-lab/vaultwarden` am 2026-05-07 erfolgreich validiert |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workstations
|
||||||
|
|
||||||
|
| System | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| `baerchen` Windows 11 | Veeam Agent Image auf Unraid-SMB | `/mnt/user/backups/windows-images/baerchen/` bzw. `\\kallilabcore\backups\windows-images\baerchen` | Veeam Restore Points im Zielordner; erster Full-Lauf 2026-06-05, GUI-Groesse 53,8 GB, Dauer 0:11:31, MetaCheck 0 Fehler/0 Warnungen | SMB-User `micha`; Veeam Job Encryption Password nur noetig, falls Storage Encryption spaeter aktiviert wird; BitLocker-Recovery-Key erst noetig, wenn BitLocker spaeter aktiviert wird | Veeam Recovery USB `VEEAMRE`, SMB auf `kallilabcore`, AdGuard/DNS oder direkte IP | Recovery-Test am 2026-06-06 erfolgreich: USB-Boot, SMB-Ziel erreichbar, Restore Point sichtbar, vor echtem Restore abgebrochen; Runbook `ops/windows-reinstall/docs/windows-image-backup-baseline.md` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -45,13 +53,13 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
| Dienst | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| 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 18, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Restore-Test am 2026-05-31 erfolgreich: Borg-Archiv `Tägliche-Sicherung-2026-05-31T04:30:13.181`, isolierter PostgreSQL-18-/Redis-8-Testpfad, HTTP `200`, `32` Dokumente im Test-DB-Check, Report `/mnt/user/backups/restore-reports/paperless-2026-05-31.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 18, Redis, Traefik | Web-UI startet, Dokumente vorhanden; Restore-Test am 2026-05-31 erfolgreich: Borg-Archiv `Tägliche-Sicherung-2026-05-31T04:30:13.181`, isolierter PostgreSQL-18-/Redis-8-Testpfad, HTTP `200`, `32` Dokumente im Test-DB-Check, Report `/mnt/user/backups/restore-reports/paperless-2026-05-31.md` |
|
||||||
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres18` (Rollback-Altstand: `/mnt/user/appdata/mealie/postgres`) | `mealie.dump` | `mealie_postgres_password.txt` | `mealie-postgres`, Traefik | UI startet, Rezepte vorhanden |
|
| Mealie | Borg + Dump | `/mnt/user/appdata/mealie/data`, `/mnt/user/appdata/mealie/postgres18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`) | `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`, `/mnt/user/appdata/immich_postgres_vectorchord`; Rollback-Altstand: `/mnt/user/appdata/immich_postgres` | `immich.dump`; nach VectorChord braucht ein Restore ein Postgres-Image mit VectorChord | `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; VectorChord-Migration am 2026-05-31: `11977` Assets, `11107` Smart-Search-Zeilen, `7092` Face-Search-Zeilen, `vchord 0.4.3`, `vector 0.8.1`, HTTP/API-Smoke 200. Voll-Restore der Foto-Dateien bleibt separater DR-Drill |
|
| Immich | Borg + Dump | `/mnt/user/photos/immich`, `/mnt/user/photos/family_archive`, `/mnt/user/appdata/immich_postgres_vectorchord`; archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs` | `immich.dump`; nach VectorChord braucht ein Restore ein Postgres-Image mit VectorChord | `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; VectorChord-Migration am 2026-05-31: `11977` Assets, `11107` Smart-Search-Zeilen, `7092` Face-Search-Zeilen, `vchord 0.4.3`, `vector 0.8.1`, HTTP/API-Smoke 200. 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 18, Traefik, Authelia | Authelia-Weiterleitung greift; nach Login startet die Web-UI und das Archiv laesst sich oeffnen |
|
| Mail-Archiver | Borg + Shared Dump | `/mnt/user/appdata/mailarchiver/data-protection-keys` | `postgresql17-mailarchiver.dump` | `MAILARCHIVER_DB_CONNECTION`, `MAILARCHIVER_AUTH_PASSWORD` | PostgreSQL 18, 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`, `/mnt/user/appdata/nextcloud/postgres18` (Rollback-Altstand: `/mnt/user/appdata/nextcloud/postgres`), `/mnt/user/appdata/nextcloud/redis` | `nextcloud.dump`; Redis-Backup vor Redis-8-Cutover unter `/mnt/user/backups/borg/dumps/latest/nextcloud-redis-pre-redis8-<ts>` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt`; produktive DB-Rolle laut `config.php` ist `oc_admin` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar; `occ status` zeigt `maintenance: false` |
|
| Nextcloud | Borg + Dump | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data`, `/mnt/user/appdata/nextcloud/postgres18` (archivierter Rollback-Altstand: `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`), `/mnt/user/appdata/nextcloud/redis` | `nextcloud.dump`; Redis-Backup vor Redis-8-Cutover unter `/mnt/user/backups/borg/dumps/latest/nextcloud-redis-pre-redis8-<ts>` | `nextcloud_admin_user.txt`, `nextcloud_admin_password.txt`, `nextcloud_postgres_password.txt`; produktive DB-Rolle laut `config.php` ist `oc_admin` | `nextcloud-postgres`, `nextcloud-redis`, Traefik | Web-UI startet, Login funktioniert, Dateien sichtbar; `occ status` zeigt `maintenance: false` |
|
||||||
| 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` |
|
| 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` |
|
||||||
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
|
| ntfy | Borg / Share | `/mnt/user/appdata/ntfy` | keine | keine besonderen Secret-Dateien dokumentiert | Traefik | UI und Push-Endpunkt erreichbar |
|
||||||
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN` | Traefik, Paperless | UI startet, Konfiguration vorhanden |
|
| Paperless-GPT | Borg / Share | `/mnt/user/appdata/paperless-gpt` | keine eigene DB | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Traefik, Paperless, OpenAI API | UI startet, Konfiguration vorhanden; LLM-Provider zeigt `openai` / `gpt-5.4-mini` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -100,10 +108,10 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
|
|||||||
### PostgreSQL 18 Restore- und Rollback-Regeln
|
### PostgreSQL 18 Restore- und Rollback-Regeln
|
||||||
|
|
||||||
- PostgreSQL-18-Container verwenden das Docker-Image-Layout mit Mount auf `/var/lib/postgresql` und `PGDATA=/var/lib/postgresql/18/docker`.
|
- PostgreSQL-18-Container verwenden das Docker-Image-Layout mit Mount auf `/var/lib/postgresql` und `PGDATA=/var/lib/postgresql/18/docker`.
|
||||||
- Die alten PostgreSQL-17-Datenpfade bleiben nach dem Major-Upgrade als Rollback-Altstand erhalten und duerfen erst nach separater Freigabe geloescht werden.
|
- Die alten PostgreSQL-17-Datenpfade wurden nach Burn-in am 2026-06-02 aus den aktiven Appdata-Pfaden entfernt und unter `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602` archiviert.
|
||||||
- Shared-Cluster-Restore: zuerst `pg_dumpall --globals-only` einspielen, dann die einzelnen Custom-Format-Dumps per `pg_restore`. Der Bootstrap-Rollenkonflikt fuer `mailarchiver` ist benign, solange `CREATE ROLE mailarchiver;` gezielt ausgelassen und das folgende `ALTER ROLE mailarchiver ...` eingespielt wird.
|
- Shared-Cluster-Restore: zuerst `pg_dumpall --globals-only` einspielen, dann die einzelnen Custom-Format-Dumps per `pg_restore`. Der Bootstrap-Rollenkonflikt fuer `mailarchiver` ist benign, solange `CREATE ROLE mailarchiver;` gezielt ausgelassen und das folgende `ALTER ROLE mailarchiver ...` eingespielt wird.
|
||||||
- Nextcloud-Restore: vor dem Dump `occ maintenance:mode --on`, nach erfolgreichem Restore und `occ status` wieder `occ maintenance:mode --off`. Die Rolle `oc_admin` muss mit dem in `config.php` hinterlegten DB-Passwort existieren.
|
- Nextcloud-Restore: vor dem Dump `occ maintenance:mode --on`, nach erfolgreichem Restore und `occ status` wieder `occ maintenance:mode --off`. Die Rolle `oc_admin` muss mit dem in `config.php` hinterlegten DB-Passwort existieren.
|
||||||
- Rollback: betroffene App(s) und DB stoppen, Compose auf das vorherige PostgreSQL-17-Image und den alten Datenpfad zuruecksetzen, dann DB und App wieder starten.
|
- Rollback: betroffene App(s) und DB stoppen, archivierten Altstand zurueck an den frueheren Datenpfad verschieben, Compose auf das vorherige PostgreSQL-17-Image und den alten Datenpfad zuruecksetzen, dann DB und App wieder starten.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -125,13 +133,210 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Erste sinnvolle Referenz-Restores
|
## Restore-Test-Reifegrad
|
||||||
|
|
||||||
Wenn weitere Restore-Uebungen dokumentiert werden sollen, sind diese Dienste besonders geeignet:
|
Stand 2026-06-06. Pro Dienst auf einen Blick: Wurde der Restore schon einmal real getestet?
|
||||||
|
|
||||||
1. `mail-archiver`
|
| Dienst | Tier | Letzter Restore-Test | Typ | Naechster Lauf |
|
||||||
2. `paperless-ngx`
|
|---|---|---|---|---|
|
||||||
3. `gitea`
|
| Vaultwarden | 1 | 2026-05-07 | File + Container + HTTP | monatlich (1. Sa) |
|
||||||
4. `vaultwarden`
|
| Gitea | 1 | 2026-05-07 | File + Container + HTTP + TCP | monatlich (3. Sa) |
|
||||||
|
| Authelia | 1 | 2026-06-03 | Config + Validate + HTTP Health | zweimonatlich (2. Sa gerade Mon.) |
|
||||||
|
| Komodo Bootstrap | 1 | 2026-05-30 | Compose + Mongo + HTTP | quartalsweise |
|
||||||
|
| Paperless | 2 | 2026-05-31 | File + Dump + Container + HTTP + Doc-Count | zweimonatlich (2. Sa ungerade Mon.) |
|
||||||
|
| Immich | 2 | 2026-05-27 | Dump + Container + HTTP + Asset-Count | quartalsweise (2. So Feb/Mai/Aug/Nov) |
|
||||||
|
| Unraid OS Flash | 1 | 2026-06-05 (Artefakt-Validierung) | sha256 OK + 390 Eintraege + 8 Kern-Configs vorhanden (`ops/maintenance/check-unraid-flash-backup.sh`); **physischer Ersatzstick-Boot-Test weiter offen** | Stick-Boot-Test nach Bedarf |
|
||||||
|
| Traefik | 1 | 2026-06-03 | Config + LE-State + File-Provider + Ping 200 | quartalsweise |
|
||||||
|
| AdGuard Home | 1 | 2026-06-06 | Config + Container + HTTP 401 + DNS + Filter-Count | quartalsweise oder nach DNS-Aenderungen |
|
||||||
|
| Tailscale | 1 | - | noch kein Test | - |
|
||||||
|
| PostgreSQL 18 Cluster | 1 | 2026-06-03 | globals + 5 per-DB dumps, 290 Tabellen gesamt | quartalsweise |
|
||||||
|
| Redis 8 | 1 | 2026-06-06 | Pre-Cutover-Artefakt + Container + PING + INFO + DBSIZE | quartalsweise oder vor/nach Redis-Major-Aenderungen |
|
||||||
|
| Komodo Mongo Daten | 1 | 2026-06-03 | mongorestore --archive --gzip, 86904 docs | quartalsweise |
|
||||||
|
| Nextcloud | 2 | 2026-06-03 | File + Dump + Container + HTTP 200 + occ status + Table-Count (126) | quartalsweise |
|
||||||
|
| Mealie | 2 | 2026-06-03 | File + Dump + Container + HTTP + Recipe-Count (3) | quartalsweise |
|
||||||
|
| Mail-Archiver | 2 | 2026-06-03 | Keys + 645M Dump + Container + HTTP 200 | quartalsweise |
|
||||||
|
| Glance | 2 | - | rebuildbar, kein Test noetig | - |
|
||||||
|
| ntfy | 2 | - | rebuildbar, kein Test noetig | - |
|
||||||
|
| Borg UI | 3 | - | rebuildbar | - |
|
||||||
|
| Filebrowser | 3 | - | rebuildbar | - |
|
||||||
|
| baerchen Windows Image | Workstation | 2026-06-06 | Full-Backup geschrieben; Recovery-USB-Boot, SMB-Mount und Restore-Point-Sichtpruefung erfolgreich; vor echtem Restore abgebrochen | nach Image-Aenderungen oder quartalsweise |
|
||||||
|
|
||||||
Sie liefern hohen Erkenntnisgewinn ohne den kompletten Homelab-Neuaufbau zu brauchen.
|
---
|
||||||
|
|
||||||
|
## Naechste Restore-Test-Kandidaten (priorisiert)
|
||||||
|
|
||||||
|
Stand 2026-06-06. Die frueheren Kandidaten (Shared PG18, Komodo Mongo, Mailarchiver, Mealie, Traefik)
|
||||||
|
wurden alle am 2026-06-03 abgeschlossen und sind in der Reifegrad-Tabelle belegt.
|
||||||
|
|
||||||
|
Verbleibende offene Restore-Pfade ohne vollstaendigen Test:
|
||||||
|
|
||||||
|
1. **Unraid OS Flash** - Artefakt-Validierung am 2026-06-05 erfolgreich (siehe Reifegrad-Tabelle und Runbook unten); offen bleibt nur der **physische Ersatzstick-Boot-Test**.
|
||||||
|
2. **Tailscale** - State-/Reconnect-Pfad dokumentiert testen
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Restore-Test-Runbooks (Entwurf)
|
||||||
|
|
||||||
|
Diese Abschnitte sind vorbereitete Checklisten fuer die noch untesteten Restore-Pfade.
|
||||||
|
Sie sind **nicht** als produktive Anleitungen zu verwenden, bevor ein erster Testlauf
|
||||||
|
die konkreten Artefaktnamen und Pfade bestaetigt hat.
|
||||||
|
|
||||||
|
### Unraid OS Flash
|
||||||
|
|
||||||
|
**Voraussetzungen:**
|
||||||
|
- Borg-Artefakt `unraid-flash-config.tar.gz` und `unraid-flash-config.tar.gz.sha256` unter `/mnt/user/backups/borg/dumps/latest` oder im Hetzner-Borg-Repo verfuegbar
|
||||||
|
- Neuer leerer USB-Stick (Empfehlung: 16 GB, USB 2.0 kompatibel)
|
||||||
|
- Unraid USB Flash Creator oder manueller Restore-Pfad
|
||||||
|
- Offline-gesicherte Borg-Passphrase verfuegbar
|
||||||
|
|
||||||
|
**Checkliste Artefakt-Validierung (ohne produktiven Stick):**
|
||||||
|
|
||||||
|
Automatisiert via Repo-Skript `ops/maintenance/check-unraid-flash-backup.sh`
|
||||||
|
(read-only, keine Extraktion). Manuelle Einzelschritte:
|
||||||
|
|
||||||
|
1. SHA256-Pruefung: `sha256sum -c unraid-flash-config.tar.gz.sha256`
|
||||||
|
2. Artefakt-Inhalt pruefen: `tar -tzf unraid-flash-config.tar.gz | head -40` — erwartet `config/` als Prefix
|
||||||
|
3. Kern-Configs vorhanden: `super.dat`, `disk.cfg`, `ident.cfg`, `share.cfg`, `network.cfg`, `docker.cfg`, `go`, `domain.cfg`
|
||||||
|
4. Keine produktiven Konfigurationspfade (z. B. `config/ssh/`) ausserhalb des Test-Environments extrahieren
|
||||||
|
5. Manifest-Datei auf Vollstaendigkeit pruefen
|
||||||
|
|
||||||
|
**Validierungsergebnis 2026-06-05 (read-only per SSH):** Artefakt frisch
|
||||||
|
(2026-06-05 04:00, ~16 h alt beim Test), `sha256sum -c` = OK, 390 Eintraege,
|
||||||
|
alle 8 Kern-Configs vorhanden. Das Archiv enthaelt erwartungsgemaess
|
||||||
|
Secret-Material (SSH-Host-Keys, Tailscale-State, `passwd`/`shadow`/`smbpasswd`,
|
||||||
|
`Trial.key`) und ist wie Secret-Backup zu behandeln. Es wurde nichts extrahiert,
|
||||||
|
nur Eintragsnamen gelistet. Offen bleibt der physische Ersatzstick-Boot-Test.
|
||||||
|
|
||||||
|
**Checkliste vollstaendiger Restore-Test (auf Wegwerf-Stick):**
|
||||||
|
|
||||||
|
1. Neuen USB-Stick mit Unraid USB Flash Creator formatieren und Basis-Unraid draufspielen
|
||||||
|
2. `config/`-Verzeichnis aus `unraid-flash-config.tar.gz` in den `/boot/config`-Pfad des neuen Sticks extrahieren
|
||||||
|
3. Im Testrahmen booten (kein Array starten, keine Shares mounten)
|
||||||
|
4. Pruefen: Unraid-Grundkonfiguration (Shares, Hostname, Netzwerk) ist sichtbar
|
||||||
|
5. Array-Zuordnung lesbar, ohne Drive-Assigns zu bestaetigen
|
||||||
|
|
||||||
|
**Smoke-Test-Kriterium:** Unraid bootet, Hostname ist `Kallilabcore`, Share-Konfiguration ist sichtbar, kein Array gestartet.
|
||||||
|
|
||||||
|
**Sonderregel:** Das Artefakt enthaelt Host-Konfiguration und SSH-Keys und ist wie Secret-Material zu behandeln. Nicht auf oeffentlichen oder unverschluesselten Testzielen extrahieren.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### AdGuard Home
|
||||||
|
|
||||||
|
**Validierungsergebnis 2026-06-06:** Automatisierter Test
|
||||||
|
`ops/restore-tests/adguard-restore-test.sh` auf Unraid erfolgreich ausgefuehrt.
|
||||||
|
Report: `/mnt/user/backups/restore-reports/adguard-2026-06-06.md`.
|
||||||
|
Getestet wurden Borg-Extract der Config, `AdGuardHome.yaml`-Struktur,
|
||||||
|
isolierter Testcontainer `restoretest-adguard` auf localhost-Ports,
|
||||||
|
HTTP `/control/status` = `401`, DNS-Smoke `git.kaleschke.info -> 192.168.178.58`,
|
||||||
|
7 Filterlisten-Eintraege. Testdaten wurden nach Erfolg bereinigt.
|
||||||
|
|
||||||
|
**Voraussetzungen:**
|
||||||
|
- Borg-Archiv mit `/mnt/user/appdata/adguard/conf` zugaenglich (produktives Repo oder Teststand)
|
||||||
|
- Testpfad unter `/mnt/user/backups/restore-lab/adguard` vorbereitet
|
||||||
|
- Docker-Faehigkeit auf dem Testhost oder in der Restore-Lab-Umgebung
|
||||||
|
|
||||||
|
**Automatisierter Test:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh adguard
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manuelle Checkliste:**
|
||||||
|
|
||||||
|
1. Borg-Extract des letzten Archivs nach `/mnt/user/backups/restore-lab/adguard/conf`:
|
||||||
|
```
|
||||||
|
borg extract ::ARCHIV /mnt/user/appdata/adguard/conf
|
||||||
|
```
|
||||||
|
2. Konfigurationsdatei `AdGuardHome.yaml` auf Vollstaendigkeit pruefen (YAML-Syntax valide)
|
||||||
|
3. Testcontainer starten (kein produktiver DNS-Port 53, stattdessen z. B. `15353`):
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:15353:53/udp"
|
||||||
|
- "127.0.0.1:13001:80/tcp"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/adguard/conf:/opt/adguardhome/conf
|
||||||
|
```
|
||||||
|
4. `http://127.0.0.1:13001/control/status` erreichbar (`200`, `401` oder `403` sind fuer den Smoke ausreichend)
|
||||||
|
5. DNS-Aufloesung: `dig @127.0.0.1 -p 15353 git.kaleschke.info` gibt plausible Antwort
|
||||||
|
6. Testcontainer stoppen und Testpfad aufraeumen
|
||||||
|
|
||||||
|
**Smoke-Test-Kriterium:** AdGuard-Web-UI laeuft, DNS-Aufloesung antwortet, Filterlisten sind geladen.
|
||||||
|
|
||||||
|
**Keine Secrets:** AdGuard Home verwendet keine dokumentierten Repo-Secrets; Login-Credentials liegen in der `AdGuardHome.yaml` im Borg-Archiv.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Tailscale
|
||||||
|
|
||||||
|
**Voraussetzungen:**
|
||||||
|
- Borg-Archiv mit `/mnt/user/appdata/tailscale` zugaenglich
|
||||||
|
- Testpfad unter `/mnt/user/backups/restore-lab/tailscale` vorbereitet
|
||||||
|
- Achtung: Der Tailscale-State ist maschinenspezifisch. Ein Restore auf denselben produktiven Host wuerde die laufende Verbindung verdraengen. Nur auf einem Wegwerf- oder Offline-Host testen.
|
||||||
|
|
||||||
|
**Checkliste Artefakt-Validierung (ohne produktiven Host):**
|
||||||
|
|
||||||
|
1. Borg-Extract nach `/mnt/user/backups/restore-lab/tailscale`
|
||||||
|
2. State-Verzeichnis auf erwartete Dateien pruefen: `tailscaled.state` vorhanden
|
||||||
|
3. Dateisystem-Rechte pruefen: `tailscaled.state` muss fuer `root` zugaenglich sein
|
||||||
|
|
||||||
|
**Checkliste Reconnect-Test (auf Wegwerf-Host oder VM):**
|
||||||
|
|
||||||
|
1. Tailscale-Container mit dem gemounteten State-Pfad starten
|
||||||
|
2. `tailscale status` zeigt `Connected` oder den erwarteten Hostnamen
|
||||||
|
3. Tailscale-Admin-Konsole (`login.tailscale.com`) zeigt Geraet als `Online`
|
||||||
|
4. SSH ueber Tailscale-IP auf den Testhost moeglich
|
||||||
|
5. Testcontainer stoppen; Wegwerf-Geraet in der Tailscale-Admin-Konsole entfernen
|
||||||
|
|
||||||
|
**Smoke-Test-Kriterium:** Container verbindet sich mit bestehendem Tailscale-Account (kein neues Re-Auth noetig), Tailscale-IP ist erreichbar.
|
||||||
|
|
||||||
|
**Hinweis:** Falls der State veraltet ist (Key expired), wird Tailscale einen Re-Auth anfordern. Das ist ein valides Testergebnis und belegt, wie lang der Reconnect-Pfad bei abgelaufenem Key ist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Redis 8 (Shared)
|
||||||
|
|
||||||
|
**Validierungsergebnis 2026-06-06:** Automatisierter Test
|
||||||
|
`ops/restore-tests/redis-restore-test.sh` auf Unraid erfolgreich ausgefuehrt.
|
||||||
|
Report: `/mnt/user/backups/restore-reports/redis-2026-06-06.md`.
|
||||||
|
Getestet wurde das Pre-Cutover-Artefakt
|
||||||
|
`/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-20260531-185011`
|
||||||
|
in einer isolierten Redis-8.8-Testinstanz auf `127.0.0.1:16379`.
|
||||||
|
Ergebnis: `PING` = `PONG`, `redis_version` = `8.8.0`, AOF aktiv (`1`),
|
||||||
|
`DBSIZE` = `1`. Produktiver Port und produktiver Datenpfad wurden nicht genutzt.
|
||||||
|
|
||||||
|
**Voraussetzungen:**
|
||||||
|
- Pre-Cutover-Backup unter `/mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>` vorhanden, oder Borg-Archiv mit `/mnt/user/appdata/redis`
|
||||||
|
- Secret-Datei `redis_password.txt` fuer Testinstanz verfuegbar (aus Borg, nicht als Wert dokumentieren)
|
||||||
|
- Testpfad unter `/mnt/user/backups/restore-lab/redis` vorbereitet
|
||||||
|
|
||||||
|
**Automatisierter Test:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh redis
|
||||||
|
```
|
||||||
|
|
||||||
|
**Manuelle Checkliste:**
|
||||||
|
|
||||||
|
1. RDB/AOF-Datei aus dem Backup in den Testpfad kopieren:
|
||||||
|
```
|
||||||
|
cp /mnt/user/backups/borg/dumps/latest/shared-redis-pre-redis8-<ts>/dump.rdb \
|
||||||
|
/mnt/user/backups/restore-lab/redis/
|
||||||
|
```
|
||||||
|
(oder Borg-Extract aus dem Appdata-Archiv)
|
||||||
|
2. Testcontainer starten (kein produktiver Port 6379, stattdessen z. B. `16379`):
|
||||||
|
```yaml
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:16379:6379"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/redis:/data
|
||||||
|
command: redis-server --requirepass <aus Secret> --appendonly yes
|
||||||
|
```
|
||||||
|
3. Verbindungstest: `redis-cli -p 16379 -a <pass> PING` antwortet `PONG`
|
||||||
|
4. Redis-Version pruefen: `redis-cli -p 16379 -a <pass> INFO server | grep redis_version` zeigt `8.x`
|
||||||
|
5. Stichprobe Key-Bestand: `redis-cli -p 16379 -a <pass> DBSIZE` zeigt plausible Zahl (nicht 0)
|
||||||
|
6. Testcontainer stoppen und Testpfad aufraeumen
|
||||||
|
|
||||||
|
**Smoke-Test-Kriterium:** Redis 8 startet mit dem Restore-Datenpfad, `PING` antwortet, `DBSIZE` ist nicht 0.
|
||||||
|
|
||||||
|
**Shared Redis Besonderheit:** Shared Redis wird produktiv nur von Paperless genutzt (AOF aktiv). Bei einem echten Restore nach App-Absturz: Erst Redis aus Backup hochziehen, dann Paperless. Nextcloud hat eigene Redis-Instanz ohne Passwort.
|
||||||
|
|||||||
+21
-3
@@ -17,6 +17,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| Service | Secret | Datei / Methode | Status |
|
| Service | Secret | Datei / Methode | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Vaultwarden | `ADMIN_TOKEN` | `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt` -> `ADMIN_TOKEN_FILE` | aktiv |
|
| Vaultwarden | `ADMIN_TOKEN` | `/mnt/user/appdata/secrets/vaultwarden_admin_token.txt` -> `ADMIN_TOKEN_FILE` | aktiv |
|
||||||
|
| Vaultwarden | SMTP Password | `/mnt/user/appdata/secrets/homelab_smtp_password.txt` -> `SMTP_PASSWORD_FILE` fuer Einladungen/Benachrichtigungen | aktiv |
|
||||||
| Traefik | Cloudflare DNS API Token | `/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token` -> Docker Secret `cloudflare_dns_api_token` | aktiv |
|
| Traefik | Cloudflare DNS API Token | `/mnt/user/appdata/traefik/secrets/cloudflare_dns_api_token` -> Docker Secret `cloudflare_dns_api_token` | aktiv |
|
||||||
| PostgreSQL 18 | DB Password | `/mnt/user/appdata/secrets/postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
| PostgreSQL 18 | DB Password | `/mnt/user/appdata/secrets/postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
||||||
| Redis | Passwort | `/mnt/user/appdata/secrets/redis_password.txt` -> Datei-Mount + Startkommando in `infra/redis/docker-compose.yml` | aktiv |
|
| Redis | Passwort | `/mnt/user/appdata/secrets/redis_password.txt` -> Datei-Mount + Startkommando in `infra/redis/docker-compose.yml` | aktiv |
|
||||||
@@ -24,6 +25,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| mealie-postgres | DB Password | `/mnt/user/appdata/secrets/mealie_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
| mealie-postgres | DB Password | `/mnt/user/appdata/secrets/mealie_postgres_password.txt` -> `POSTGRES_PASSWORD_FILE` | aktiv |
|
||||||
| Paperless-ngx | DB Password | Stack ENV `${PAPERLESS_DBPASS}` | aktiv |
|
| Paperless-ngx | DB Password | Stack ENV `${PAPERLESS_DBPASS}` | aktiv |
|
||||||
| Paperless-ngx | Redis URL | Stack ENV `${PAPERLESS_REDIS}` | aktiv |
|
| Paperless-ngx | Redis URL | Stack ENV `${PAPERLESS_REDIS}` | aktiv |
|
||||||
|
| Paperless-GPT | OpenAI API Key | Stack ENV `${OPENAI_API_KEY}`; nicht im Repo, nicht in Logs | aktiv |
|
||||||
| code-server | Passwort | `/mnt/user/appdata/code-server/secrets/password` -> `FILE__PASSWORD` | aktiv |
|
| code-server | Passwort | `/mnt/user/appdata/code-server/secrets/password` -> `FILE__PASSWORD` | aktiv |
|
||||||
| Filebrowser | Admin Password | `/mnt/user/appdata/secrets/filebrowser_admin_password.txt` -> initialisierte SQLite-DB | aktiv |
|
| Filebrowser | Admin Password | `/mnt/user/appdata/secrets/filebrowser_admin_password.txt` -> initialisierte SQLite-DB | aktiv |
|
||||||
| Immich (server) | DB Password | Stack ENV `${IMMICH_DB_PASSWORD}` | aktiv |
|
| Immich (server) | DB Password | Stack ENV `${IMMICH_DB_PASSWORD}` | aktiv |
|
||||||
@@ -51,8 +53,16 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | aktiv |
|
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | aktiv |
|
||||||
| 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 | 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 |
|
| 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 |
|
| Grafana OIDC (Authelia) | Client Secret | `/mnt/user/appdata/secrets/grafana_oidc_client_secret` (Klartext, chmod 600) -> Docker Secret `/run/secrets/grafana_oidc_client_secret` -> `GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET__FILE`. Zugehoeriger pbkdf2-Hash liegt im Authelia-Host-Config-Client `grafana` (kein Wert im Repo) | aktiv (2026-06-06) |
|
||||||
|
| Mealie OIDC (Authelia) | Client Secret | Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}` in `/mnt/user/services/stacks/mealie/apps/mealie/.env` (Komodo-Stack-ENV); pbkdf2-Hash im Authelia-Host-Config-Client `mealie` (kein Wert im Repo) | aktiv (2026-06-06) |
|
||||||
| 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`) |
|
| 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`) |
|
||||||
|
| n8n | Encryption Key fuer interne Credential-Verschluesselung | `/mnt/user/appdata/secrets/n8n_encryption_key.txt` (chmod 600) -> Komodo Stack ENV `${N8N_ENCRYPTION_KEY}`; kein `_FILE`-Support im Upstream-Image | aktiv |
|
||||||
|
| n8n | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv |
|
||||||
|
| n8n | OpenAI API Key (LLM-Extraktion Workflow) | n8n Credentials Store (Typ `httpHeaderAuth`, Header `Authorization: Bearer ...`) | aktiv |
|
||||||
|
| n8n | Gitea PAT fuer `n8n-bot` (Issue-Erstellung Workflow) | n8n Credentials Store (Typ `httpHeaderAuth`, Header `Authorization: token ...`); separater Bot-User mit Scope `write:issue` auf `Micha/mails` | aktiv |
|
||||||
|
| baerchen Veeam | Veeam Job Encryption Password | Vaultwarden Secure Note `Veeam baerchen backup encryption password`; kein Datei-Secret im Repo | geplant, nur noetig falls Veeam Storage Encryption aktiviert wird |
|
||||||
|
| baerchen SMB Backup Target | SMB Credential fuer User `micha` | bestehender Unraid-/Vaultwarden-Zugang fuer Share `backups`; wird im Veeam-Job gespeichert, Wert nie dokumentieren | aktiv |
|
||||||
|
| baerchen BitLocker | BitLocker Recovery Key C: | **bewusst deaktiviert (Entscheidung 2026-06-06):** kein BitLocker, kein Recovery-Key noetig. Falls spaeter aktiviert: Key nach `D:\30_Finanzen\BitLocker-RecoveryKey-baerchen-<DATUM>.txt` + Vaultwarden Secure Note + physischer Ausdruck | nicht aktiv (bewusst) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -89,6 +99,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
|-- borg_repo_passphrase.txt
|
|-- borg_repo_passphrase.txt
|
||||||
|-- influxdb3_admin_token.json
|
|-- influxdb3_admin_token.json
|
||||||
|-- filebrowser_admin_password.txt
|
|-- filebrowser_admin_password.txt
|
||||||
|
|-- homelab_smtp_password.txt
|
||||||
`-- vaultwarden_admin_token.txt
|
`-- vaultwarden_admin_token.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -101,6 +112,12 @@ Weitere dokumentierte Secret-Pfade:
|
|||||||
- 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.
|
- 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.
|
- 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.
|
- `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.
|
||||||
|
- `baerchen` nutzt fuer das Veeam-Backup aktuell den bestehenden SMB-User
|
||||||
|
`micha`. Ein dedizierter SMB-User `veeam-baerchen` ist nur ein spaeteres
|
||||||
|
Hardening-Ziel, solange keine Unraid-User-/Share-Aenderungen gewuenscht sind.
|
||||||
|
- Das Veeam-Job-Encryption-Passwort ist restore-kritisch. Ohne diesen Wert ist
|
||||||
|
das Image unter `\\kallilabcore\backups\windows-images\baerchen` nicht
|
||||||
|
brauchbar.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -109,8 +126,7 @@ Weitere dokumentierte Secret-Pfade:
|
|||||||
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:
|
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/`).
|
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).
|
2. **Externe Operator-Notiz** (analoge Sicherung, vergleichbar mit der Borg-Passphrase).
|
||||||
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.
|
**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.
|
||||||
|
|
||||||
@@ -119,12 +135,14 @@ Einige Secrets liegen bewusst nur als Komodo Stack Environment Variables vor, we
|
|||||||
| Stack | Stack-ENV-Variablen | Restore-Quelle (Reihenfolge) | Folgen bei Verlust aller Quellen |
|
| 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 |
|
| `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 |
|
||||||
|
| `paperless-gpt` | `PAPERLESS_API_TOKEN`, `OPENAI_API_KEY` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Paperless-Token kann in Paperless neu erzeugt werden; OpenAI-Key muss im OpenAI-Projekt rotiert/neu erstellt werden |
|
||||||
| `immich-server` | `IMMICH_DB_PASSWORD` | Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | analog Paperless: Postgres-User-Passwort in `immich_postgres` und Stack-ENV gemeinsam zuruecksetzen |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
| `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 |
|
||||||
|
| `n8n` | `N8N_ENCRYPTION_KEY` | Host-Secret-Datei `/mnt/user/appdata/secrets/n8n_encryption_key.txt` -> Komodo-Mongo-Dump -> Vaultwarden -> externe Notiz | Bei Verlust aller Quellen: n8n startet, aber **alle gespeicherten Credentials sind unbrauchbar** (Re-Eingabe noetig: GMX IMAP, OpenAI, Gitea PAT). Workflows bleiben strukturell erhalten. |
|
||||||
|
|
||||||
### Komodo-Sonderfall
|
### Komodo-Sonderfall
|
||||||
|
|
||||||
|
|||||||
+12
-10
@@ -1,6 +1,6 @@
|
|||||||
# Service Catalog
|
# Service Catalog
|
||||||
|
|
||||||
Stand: 2026-06-01
|
Stand: 2026-06-02
|
||||||
|
|
||||||
Dieser Katalog beschreibt produktive und repo-vorbereitete Dienste aus Sicht von Betrieb, Restore und KI-Kontext. Er basiert auf dem Repo-Sollzustand. Vor produktiven Eingriffen immer den Live-Zustand in Komodo/Docker pruefen.
|
Dieser Katalog beschreibt produktive und repo-vorbereitete Dienste aus Sicht von Betrieb, Restore und KI-Kontext. Er basiert auf dem Repo-Sollzustand. Vor produktiven Eingriffen immer den Live-Zustand in Komodo/Docker pruefen.
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| `traefik` | zentraler Reverse Proxy, TLS, Docker-Label-Routing | `traefik/docker-compose.yml`, `traefik/dynamic/*` | `https://traefik.kaleschke.info` | Docker socket, Cloudflare DNS API, `frontend_net`, `backend_net` | `/mnt/user/appdata/traefik/dynamic`, `/mnt/user/appdata/traefik/letsencrypt` | Tier 1, Share/Borg | ja, eigene Dashboard-Route mit Authelia | Host-Ports 80/443 sind zentrale Ausnahme; dynamic configs werden nicht automatisch von Komodo deployed |
|
| `traefik` | zentraler Reverse Proxy, TLS, Docker-Label-Routing | `traefik/docker-compose.yml`, `traefik/dynamic/*` | `https://traefik.kaleschke.info` | Docker socket, Cloudflare DNS API, `frontend_net`, `backend_net` | `/mnt/user/appdata/traefik/dynamic`, `/mnt/user/appdata/traefik/letsencrypt` | Tier 1, Share/Borg | ja, eigene Dashboard-Route mit Authelia | Host-Ports 80/443 sind zentrale Ausnahme; dynamic configs werden nicht automatisch von Komodo deployed |
|
||||||
| `adguard` | DNS-Server / LAN DNS | `host-services/Adguard/docker-compose.yml` | LAN-Port `53`, Admin `100.80.98.33:8082` | `dns_net`, `frontend_net`, Unbound | `/mnt/user/appdata/adguard/conf`, `/mnt/user/appdata/adguard/work` | Tier 1, config relevant | nein | Direkter DNS-Port 53 bleibt; Admin-Port ist bewusst ohne Traefik/2FA, aber auf Tailscale-IP begrenzt (Operator-Entscheidung 2026-05-26) |
|
| `adguard` | DNS-Server / LAN DNS | `host-services/Adguard/docker-compose.yml` | LAN-Port `53`, Admin `100.80.98.33:8082` | `dns_net`, `frontend_net`, Unbound | `/mnt/user/appdata/adguard/conf`, `/mnt/user/appdata/adguard/work` | Tier 1, config relevant | nein | Direkter DNS-Port 53 bleibt; Admin-Port ist bewusst ohne Traefik/2FA, aber auf Tailscale-IP begrenzt (Operator-Entscheidung 2026-05-26) |
|
||||||
| `unbound` | Upstream DNS Resolver fuer AdGuard | `apps/unbound/docker-compose.yml` | intern | `dns_net` | `/mnt/user/appdata/unbound/config` | rebuildbar / config relevant | nein | intern isoliert |
|
| `unbound` | Upstream DNS Resolver fuer AdGuard | `apps/unbound/docker-compose.yml` | intern | `dns_net` | `/mnt/user/appdata/unbound/config` | rebuildbar / config relevant | nein | intern isoliert |
|
||||||
| `tailscale` | VPN/Remote-Zugang | `host-services/tailscale/docker-compose.yml` | Tailscale | Host-Netz | `/mnt/user/appdata/tailscale` | Tier 1, State relevant | nein | `network_mode: host`, `NET_ADMIN`, `NET_RAW` und `/dev/net/tun` sind dokumentierte VPN-Ausnahmen |
|
| `tailscale` | VPN/Remote-Zugang, Subnet-Router | **Natives Unraid-Plugin** `tailscale.plg` (nicht repo-/Komodo-verwaltet) | Tailscale | Host-Netz (`tailscale1`) | `/boot/config/plugins/tailscale/state` (im Flash-Backup) | Tier 1, State relevant | nein | Subnet-Router `192.168.178.0/24`; redundanter Docker-Stack `host-services/tailscale/` am 2026-06-06 entfernt |
|
||||||
| `gitea` | Git-Server / origin fuer GitOps | `core/gitea/docker-compose.yml` | `https://git.kaleschke.info`, SSH `222` | Traefik, `frontend_net`, externe DNS-Resolver fuer GitHub-Push-Mirror | `/mnt/user/services/gitea/data` | Tier 1, `gitea.sqlite.dump` + Share; privater GitHub-Push-Mirror fuer Repo-Bootstrap | ja | SSH-Port 222 direkte Host-Port-Ausnahme; Push-Mirror nach `michaelkaleschke-spec/homelab-infra` reduziert das DR-Bootstrap-Risiko |
|
| `gitea` | Git-Server / origin fuer GitOps | `core/gitea/docker-compose.yml` | `https://git.kaleschke.info`, SSH `222` | Traefik, `frontend_net`, externe DNS-Resolver fuer GitHub-Push-Mirror | `/mnt/user/services/gitea/data` | Tier 1, `gitea.sqlite.dump` + Share; privater GitHub-Push-Mirror fuer Repo-Bootstrap | ja | SSH-Port 222 direkte Host-Port-Ausnahme; Push-Mirror nach `michaelkaleschke-spec/homelab-infra` reduziert das DR-Bootstrap-Risiko |
|
||||||
|
|
||||||
## Security / Identity
|
## Security / Identity
|
||||||
@@ -21,13 +21,13 @@ 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 |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `authelia` | ForwardAuth / zentrale Auth fuer Admin-UIs | `security/authelia/docker-compose.yml`, `security/authelia/configuration.yml` | `https://auth.kaleschke.info` | PostgreSQL 18, Traefik, GMX SMTP | `/mnt/user/appdata/authelia/config`, Authelia Secret-Dateien | Tier 1, config + DB + secrets | ja | Bewusst ohne Redis-Session-Backend; SMTP-Notifier via GMX und `authelia_smtp_password.txt`; explizite DNS-Server fuer SMTP/NTP; Repo-Baseline muss manuell in die Host-Config gemerged werden, OIDC/Secrets bleiben hostseitig; Access-Control und Compose-Middleware bei Aenderungen abgleichen |
|
| `authelia` | ForwardAuth / zentrale Auth fuer Admin-UIs | `security/authelia/docker-compose.yml`, `security/authelia/configuration.yml` | `https://auth.kaleschke.info` | PostgreSQL 18, Traefik, GMX SMTP | `/mnt/user/appdata/authelia/config`, Authelia Secret-Dateien | Tier 1, config + DB + secrets | ja | Bewusst ohne Redis-Session-Backend; SMTP-Notifier via GMX und `authelia_smtp_password.txt`; explizite DNS-Server fuer SMTP/NTP; Repo-Baseline muss manuell in die Host-Config gemerged werden, OIDC/Secrets bleiben hostseitig; Access-Control und Compose-Middleware bei Aenderungen abgleichen |
|
||||||
| `vaultwarden` | Passwort-Tresor | `security/vaultwarden/docker-compose.yml` | `https://vault.kaleschke.info` | Traefik, `frontend_net` | `/mnt/user/appdata/vaultwarden` | Tier 1, `vaultwarden.sqlite.dump` + Share | ja | `ADMIN_TOKEN_FILE`; keine direkten Ports |
|
| `vaultwarden` | Passwort-Tresor | `security/vaultwarden/docker-compose.yml` | `https://vault.kaleschke.info` | Traefik, `frontend_net`, GMX SMTP | `/mnt/user/appdata/vaultwarden` | Tier 1, `vaultwarden.sqlite.dump` + Share | ja | `ADMIN_TOKEN_FILE`; SMTP ueber `homelab_smtp_password.txt` fuer Einladungen/Benachrichtigungen; keine direkten Ports |
|
||||||
|
|
||||||
## Shared Infrastructure
|
## Shared Infrastructure
|
||||||
|
|
||||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `postgresql17` | shared PostgreSQL 18 Cluster (historischer Service-Name bleibt fuer DNS/Clients stabil) | `infra/postgresql17/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/postgresql18`, Rollback-Altstand `/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 |
|
| `postgresql17` | shared PostgreSQL 18 Cluster (historischer Service-Name bleibt fuer DNS/Clients stabil) | `infra/postgresql17/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/postgresql18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/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` | 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 | Redis 8.8; 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. |
|
| `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 | Redis 8.8; 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 |
|
| `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 |
|
||||||
|
|
||||||
@@ -36,20 +36,21 @@ 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 |
|
| 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 18, Redis 8, 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; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
| `paperless-ngx` | Dokumentenmanagement | `apps/paperless/docker-compose.yml` | `https://paperless.kaleschke.info` | PostgreSQL 18, Redis 8, 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; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
||||||
| `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. |
|
| `paperless-gpt` | KI-Ergaenzung fuer Paperless | `apps/paperless-gpt/docker-compose.yml` | `https://paperless-gpt.kaleschke.info` | Paperless API, OpenAI API, Traefik | `/mnt/user/appdata/paperless-gpt/data`, `/mnt/user/appdata/paperless-gpt/prompts` | Tier 2 | ja + Authelia | `PAPERLESS_API_TOKEN` und `OPENAI_API_KEY` als Stack ENV; LLM und Vision-OCR laufen ueber `gpt-5.4-mini`, kein Zugriff mehr auf lokale Ollama-VM. **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_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_vectorchord`, Rollback-Altstand `/mnt/user/appdata/immich_postgres`, `immich_postgres_password.txt` | Dump `immich.dump`; Restore braucht ein Image mit VectorChord/pgvector | nein | PG14 bleibt bewusst; Immich-DB-Image `ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0`; nie ins `frontend_net` |
|
| `immich_postgres` | Immich-Datenbank | `apps/immich/docker-compose.yml` | intern | `immich_default` | `/mnt/user/appdata/immich_postgres_vectorchord`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs`, `immich_postgres_password.txt` | Dump `immich.dump`; Restore braucht ein Image mit VectorChord/pgvector | nein | PG14 bleibt bewusst; Immich-DB-Image `ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0`; nie ins `frontend_net` |
|
||||||
| `immich_redis` | Immich Cache | `apps/immich/docker-compose.yml` | intern | `immich_default` | kein kritischer Pfad dokumentiert | rebuildbar | nein | Redis 8.8; Architektur nennt anonymes Volume -> named volume als offenes Thema |
|
| `immich_redis` | Immich Cache | `apps/immich/docker-compose.yml` | intern | `immich_default` | kein kritischer Pfad dokumentiert | rebuildbar | nein | Redis 8.8; Architektur nennt anonymes Volume -> named volume als offenes Thema |
|
||||||
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default` | `model-cache` | rebuildbar | nein | intern-only |
|
| `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default` | `model-cache` | rebuildbar | nein | intern-only |
|
||||||
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
| `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt |
|
||||||
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, Rollback-Altstand `/mnt/user/appdata/mealie/postgres`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
| `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 |
|
||||||
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
| `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen |
|
||||||
| `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` | 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/postgres18`, Rollback-Altstand `/mnt/user/appdata/nextcloud/postgres`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
||||||
| `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 8.8 |
|
| `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 8.8 |
|
||||||
| `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. |
|
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml`, `traefik/dynamic/plex.yml` | `https://plex.kaleschke.info`, LAN `http://192.168.178.58:32400/web`, Remote Access deaktiviert | Host-Netz, Traefik File provider | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | ja, native Plex-Auth | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme. Traefik routet via File-Provider-Ausnahme auf `http://192.168.178.58:32400`, weil Docker-Labels Host-Netz-Container aus Traefik heraus auf `127.0.0.1` routen wuerden. Keine FRITZ!Box-Freigabe fuer `32400`. Keine Authelia-ForwardAuth, weil Plex Web/App-Clients native Plex-Auth und eigene Flows nutzen. Server geclaimt von `Xeridos`; Smart-TVs greifen weiter ueber WLAN-LAN per mDNS/Plex-GDM direkt zu. `PublishServerOnPlexOnlineKey=0` (Plex 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` |
|
| `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. **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). |
|
| `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). |
|
||||||
|
| `super-productivity` | Persoenliche Produktivitaets-/Task-PWA (Operator), konsumiert Gitea-Issues aus `Micha/mails` | `apps/super-productivity/docker-compose.yml` | `https://sp.kaleschke.info` | Traefik + Authelia, Gitea `Micha/mails` (Polling vom Client) | statisches Frontend, kein Server-State; Browser-IndexedDB plus optionaler WebDAV-Sync gegen Nextcloud | Tier 3, rebuildbar | ja + Authelia | Reine Static-PWA; SP synchronisiert client-seitig ueber Gitea-API (Scope `assigned`, Repo `Micha/mails`, User `Micha`). |
|
||||||
|
|
||||||
## Operations / Monitoring / Admin
|
## Operations / Monitoring / Admin
|
||||||
|
|
||||||
@@ -77,13 +78,14 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| `monitoring-influxdb3-core` | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten | `monitoring/docker-compose.yml` | Host-Port `8181` je `INFLUXDB_BIND_IP`, keine Public URL | Monitoring-Grafana, Home Assistant Writer | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | Tier 3 | nein | 2026-05-31 effektiv auf `127.0.0.1:8181` gebunden, also nicht LAN-exponiert; `user: "0"` ist fuer den lokalen Object-Store-Pfad dokumentiert; uebernimmt den bisherigen InfluxDB-Daten-/Token-Katalog; `401 Unauthorized` beim Curl ohne Token ist erwarteter Reachability-Test |
|
| `monitoring-influxdb3-core` | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten | `monitoring/docker-compose.yml` | Host-Port `8181` je `INFLUXDB_BIND_IP`, keine Public URL | Monitoring-Grafana, Home Assistant Writer | `/mnt/user/appdata/influxdb3/data`, `/mnt/user/appdata/influxdb3/plugins` | Tier 3 | nein | 2026-05-31 effektiv auf `127.0.0.1:8181` gebunden, also nicht LAN-exponiert; `user: "0"` ist fuer den lokalen Object-Store-Pfad dokumentiert; uebernimmt den bisherigen InfluxDB-Daten-/Token-Katalog; `401 Unauthorized` beim Curl ohne Token ist erwarteter Reachability-Test |
|
||||||
| `hermes-gateway` | Hermes Agent Gateway/API intern | `ops/hermes-agent/docker-compose.yml` | intern `8642` auf `hermes_net` | SSH Runner (VM 192.168.178.143), LLM Provider, optional Home Assistant | `/mnt/user/appdata/hermes-agent/data`, SSH key path | Tier 3, Borg/Share | nein | NAS-Stack bleibt deaktiviert, solange die separate Hermes-VM/Runner-Seite nicht wiederhergestellt ist; kein Docker-Socket |
|
| `hermes-gateway` | Hermes Agent Gateway/API intern | `ops/hermes-agent/docker-compose.yml` | intern `8642` auf `hermes_net` | SSH Runner (VM 192.168.178.143), LLM Provider, optional Home Assistant | `/mnt/user/appdata/hermes-agent/data`, SSH key path | Tier 3, Borg/Share | nein | NAS-Stack bleibt deaktiviert, solange die separate Hermes-VM/Runner-Seite nicht wiederhergestellt ist; kein Docker-Socket |
|
||||||
| `hermes-dashboard` | Hermes Dashboard | `ops/hermes-agent/docker-compose.yml` | `https://hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes-gateway`, Traefik + Authelia | shared read-only data mount | Tier 3, Borg/Share | ja + Authelia | Compose-Profil `dashboard`; aktuell VM-seitig offen, nicht Teil des NAS-Finalstarts |
|
| `hermes-dashboard` | Hermes Dashboard | `ops/hermes-agent/docker-compose.yml` | `https://hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes-gateway`, Traefik + Authelia | shared read-only data mount | Tier 3, Borg/Share | ja + Authelia | Compose-Profil `dashboard`; aktuell VM-seitig offen, nicht Teil des NAS-Finalstarts |
|
||||||
|
| `n8n` | Workflow-Automation; aktuell genutzt fuer Mail->LLM->Gitea-Issue (Inbox `Micha/mails`) | `apps/n8n/docker-compose.yml`, `apps/n8n/workflows/*.json` | `https://n8n.kaleschke.info` | Traefik (ohne pauschale Authelia, analog Komodo/Nextcloud), GMX IMAP, OpenAI API, Gitea API | `/mnt/user/appdata/n8n/data` (SQLite, Credentials, Workflows) | Tier 2, Borg + `n8n-data` (Credentials sind nur mit `N8N_ENCRYPTION_KEY` entschluesselbar) | ja, native Auth | Wegen Webhook-Endpunkten (`/webhook/*`) bewusst ohne `authelia@file`; eigene Login-/Owner-Auth bleibt Pflicht; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret, Verlust macht Credentials unbrauchbar. |
|
||||||
|
|
||||||
## Host Operations
|
## Host Operations
|
||||||
|
|
||||||
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
| Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs |
|
||||||
|---|---|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|---|---|
|
||||||
| `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") |
|
| `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` |
|
| `docker-critical-events` | Live-Alarmierung fuer Docker `die`/`oom`/`kill` Events | `services/posture-check/docker-critical-events.sh`, Supervisor `services/posture-check/docker-critical-events-supervisor.sh` | Unraid User-Script / Hintergrundprozess | Docker CLI, ntfy | `/mnt/user/services/posture-check/docker-critical-events-last.log`, PID/Outfile unter `/mnt/user/services/posture-check/` | Repo-Skript + letzter Event-Log | nein | Optional als Unraid User-Script `at array start` starten; Supervisor kann `start`, `stop`, `status`, `smoke`; sendet nach `homelab-alerts` |
|
||||||
|
|
||||||
## Backup- und Restore-Hinweise
|
## Backup- und Restore-Hinweise
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Weekend Execution Plan - 2026-06-05 bis 2026-06-07
|
||||||
|
|
||||||
|
Ziel: Bis Ende des Wochenendes alle offenen To-dos aus `docs/MASTER_TODO.md`
|
||||||
|
entweder erledigen, verifiziert schliessen, oder bewusst als geparkt/extern
|
||||||
|
blockiert markieren. Nicht jeder Punkt ist realistisch "fertig" im Sinne von
|
||||||
|
technisch umgesetzt: Family-Onboarding, zweite Hardware, USV und WAN-Failover
|
||||||
|
brauchen Operator- oder Hardware-Entscheidungen.
|
||||||
|
|
||||||
|
## Arbeitsregeln
|
||||||
|
|
||||||
|
- Secrets niemals in Chat, Logs oder Repo schreiben.
|
||||||
|
- Homelab-Aenderungen nur via GitOps, keine direkten Komodo-/Docker-Hotfixes.
|
||||||
|
- Destruktive Windows- oder Host-Schritte nur nach expliziter Freigabe.
|
||||||
|
- Ergebnis jedes abgeschlossenen Punkts in der Detaildoku und in
|
||||||
|
`docs/MASTER_TODO.md` nachziehen.
|
||||||
|
- Am Ende: ein sauberer Commit-Block; Push erst nach Freigabe.
|
||||||
|
|
||||||
|
## Owner-Aufteilung
|
||||||
|
|
||||||
|
| Owner | Fokus | Ergebnis |
|
||||||
|
|---|---|---|
|
||||||
|
| Codex | `baerchen` Veeam, Doku-Konsolidierung, lokale Checks, Commit-Vorbereitung | Veeam-Erstbackup geprueft, Recovery-Test dokumentiert, Masterliste aktualisiert |
|
||||||
|
| Claude | Family-Onboarding-Paket, Network-/Tailscale-Entscheidungen, Hardware-/Todo-Konsolidierung, nicht-destruktive Runbooks | Konkrete Doku-Patches, ausfuehrbare Checklisten, klare Operator-Fragen statt diffuser TBDs |
|
||||||
|
| Operator | Physische/GUI-Schritte, Secrets, Familie, Hardwareentscheidungen | Recovery-USB booten, Passwoerter/Keys bereitstellen, Family-Onboarding starten/entscheiden |
|
||||||
|
|
||||||
|
## Codex-Aufgaben
|
||||||
|
|
||||||
|
| Prioritaet | Aufgabe | Abschlusskriterium |
|
||||||
|
|---|---|---|
|
||||||
|
| P1 | Veeam-Erstbackup `baerchen-c-image` pruefen | **erledigt 2026-06-05:** Full-Lauf geschrieben, Veeam-GUI 53,8 GB, Dauer 0:11:31, MetaCheck 0 Fehler/0 Warnungen; Storage Encryption war nicht aktiv und ist als Operator-Entscheidung dokumentiert |
|
||||||
|
| P1 | Recovery-USB-Test begleiten | `VEEAMRE` bootet, SMB-Ziel sichtbar, Restore Point sichtbar, vor Restore abgebrochen |
|
||||||
|
| P1 | `windows-image-backup-baseline.md` finalisieren | Erster Lauf und Teststatus mit Datum eingetragen |
|
||||||
|
| P1 | `docs/MASTER_TODO.md` nach jedem Abschluss aktualisieren | erledigte Punkte entfernt oder in "geschlossen" vermerkt |
|
||||||
|
| P2 | Alte Windows-Reinstall-Doku bereinigen | ueberholte WinRE-/Admin-To-dos als erledigt/ueberholt markiert |
|
||||||
|
| P2 | Git-Status sortieren | Eigene Aenderungen klar von vorhandenen User-Aenderungen getrennt |
|
||||||
|
| P2 | Commit vorbereiten | Commit-Message-Vorschlag und Datei-Liste bereit; kein Push ohne Freigabe |
|
||||||
|
|
||||||
|
## Claude-Aufgaben
|
||||||
|
|
||||||
|
Claude soll parallel nur repo-seitig arbeiten und keine produktiven Host-Aenderungen
|
||||||
|
ausfuehren. Die Aufgaben sind bewusst als echte Doku-/Planungsarbeit formuliert,
|
||||||
|
nicht nur als Pruefaufgaben:
|
||||||
|
|
||||||
|
| Prioritaet | Aufgabe | Abschlusskriterium |
|
||||||
|
|---|---|---|
|
||||||
|
| P1 | `docs/MASTER_TODO.md` gegen Detaildokus gegenpruefen | **erledigt 2026-06-05:** Sync-Notiz in `docs/AUDIT_2026-05-25_TODO.md`, Masterliste aktualisiert |
|
||||||
|
| P1 | Restore-Backlog aktualisieren | **erledigt 2026-06-05:** erledigte Kandidaten aus `docs/RESTORE_MATRIX.md` bereinigt |
|
||||||
|
| P1 | Family-Onboarding in ein ausfuehrbares Session-Paket umwandeln | **erledigt 2026-06-05:** `docs/FAMILY_ONBOARDING.md` enthaelt Vorbereitungs-, Termin- und Erfolgskriterien ohne Secret-Werte |
|
||||||
|
| P1 | `docs/NETWORK_INVENTORY.md` TBDs in Entscheidungen oder konkrete Operator-Fragen verwandeln | **erledigt 2026-06-05:** Tailscale IPv6/Exit Node/Subnet Router/ACL-Policy sind als Messaufgabe/Operator-Entscheidung formuliert; Gast-/WAN-Pfade sind geparkt oder mit Vorbedingungen versehen |
|
||||||
|
| P2 | Nicht-destruktive Runbooks fuer offene Restore-Tests vorbereiten | **erledigt 2026-06-05:** Runbook-Stubs fuer Unraid Flash, AdGuard, Tailscale, Redis 8 in `docs/RESTORE_MATRIX.md` |
|
||||||
|
| P2 | `docs/AUDIT_2026-05-25_TODO.md` und `MASTER_TODO.md` synchronisieren | **erledigt 2026-06-05:** keine doppelten oder widerspruechlichen P1/P2-Punkte |
|
||||||
|
| P2 | Windows-Reinstall-Altdoku auf ueberholte To-dos pruefen | **erledigt 2026-06-05:** WinRE/Admin-Check-Altlasten als erledigt/ueberholt markiert |
|
||||||
|
| P2 | Hardware-/Betriebsentscheidungen konsolidieren | **teilweise erledigt 2026-06-05:** USV und Cold-Backup-Rotation sind entschieden/geparkt; Masterliste fuehrt sie nicht mehr als aktive Umsetzungsaufgaben |
|
||||||
|
| P3 | Geparkte Punkte klassifizieren | Family/USV/WAN/CrowdSec/OIDC klar als Entscheidung statt Umsetzungsarbeit markiert |
|
||||||
|
|
||||||
|
## Operator-Aufgaben
|
||||||
|
|
||||||
|
| Prioritaet | Aufgabe | Abschlusskriterium |
|
||||||
|
|---|---|---|
|
||||||
|
| P1 | Veeam-Encryption-Entscheidung treffen | Fuer den ersten Full-Lauf ist kein Veeam-Encryption-Passwort noetig; falls Storage Encryption aktiviert wird, Passwort in Vaultwarden anlegen und neues Full erzeugen |
|
||||||
|
| P1 | Recovery-USB physisch booten | Boot ins Veeam-Recovery-System gelingt |
|
||||||
|
| P1 | Keine echten Restore-Ziele bestaetigen | Restore-Test wird vor destruktiver Datentraegerauswahl abgebrochen |
|
||||||
|
| P2 | BitLocker-Entscheidung treffen | `aktivieren`, `spaeter`, oder `bewusst aus` dokumentiert |
|
||||||
|
| P2 | Family-Onboarding real starten oder terminieren | konkreter Termin/Personenkreis statt offenem Wunsch |
|
||||||
|
| P3 | Hardware-Entscheidungen | USV/Cold-Rotation/WAN-Failover als kaufen, spaeter, oder bewusst nein markieren |
|
||||||
|
|
||||||
|
## Realistische Wochenend-Ziele
|
||||||
|
|
||||||
|
Bis Sonntagabend realistisch fertig:
|
||||||
|
|
||||||
|
- `baerchen` Veeam-Erstbackup verifiziert.
|
||||||
|
- `baerchen` Recovery-USB-Test ohne Restore verifiziert.
|
||||||
|
- Veeam-/BitLocker-Doku bereinigt.
|
||||||
|
- Master-To-do-Liste bereinigt.
|
||||||
|
- Restore-Backlog sortiert.
|
||||||
|
- Alte/ueberholte To-dos als erledigt/ueberholt markiert.
|
||||||
|
- Blockierte Punkte explizit als Betreiber-/Hardware-/Familienentscheidung markiert.
|
||||||
|
|
||||||
|
Nicht realistisch ohne externe Voraussetzungen:
|
||||||
|
|
||||||
|
- End-to-end-DR-Drill ohne zweite Hardware.
|
||||||
|
- Family-Onboarding ohne Familie/Geraete.
|
||||||
|
- USV erledigen ohne Kauf.
|
||||||
|
- WAN-Failover erledigen ohne Mobilfunk-/Router-Entscheidung.
|
||||||
|
- Dedizierter SMB-User ohne bewusste Unraid-User-/Share-Aenderung.
|
||||||
|
|
||||||
|
## Prompt fuer Claude
|
||||||
|
|
||||||
|
```text
|
||||||
|
Du bist Claude im KalliLab CORE Homelab-Repo.
|
||||||
|
|
||||||
|
Arbeitsziel fuer dieses Wochenende:
|
||||||
|
Hilf, alle offenen To-dos aus `docs/MASTER_TODO.md` bis Sonntagabend entweder
|
||||||
|
zu erledigen, sauber zu dokumentieren, oder bewusst als geparkt/blockiert zu
|
||||||
|
klassifizieren. Arbeite repo-seitig, keine produktiven Host-Aenderungen.
|
||||||
|
|
||||||
|
Pflichtregeln:
|
||||||
|
- Lies zuerst `CLAUDE.md`.
|
||||||
|
- Lies danach `HOMELAB_ARCHITECTURE_MASTER_V2.md`, `docs/WORKFLOW.md`,
|
||||||
|
`docs/README.md`, `docs/REPO_MAP.md`, `docs/MASTER_TODO.md`,
|
||||||
|
`docs/RESTORE_MATRIX.md`, `docs/DISASTER_RECOVERY.md`,
|
||||||
|
`docs/SECRETS_MAP.md` und `ops/windows-reinstall/docs/windows-image-backup-baseline.md`.
|
||||||
|
- Keine Secrets ins Repo. Nur Secret-Namen, Pfade und Ablageorte dokumentieren.
|
||||||
|
- Keine Komodo-/Docker-/Host-Hotfixes. Keine produktiven Schreibbefehle auf dem Homelab.
|
||||||
|
- Keine destruktiven Aktionen.
|
||||||
|
- Beachte vorhandene uncommitted Aenderungen; nichts revertieren, was du nicht selbst gemacht hast.
|
||||||
|
|
||||||
|
Konkrete Aufgaben:
|
||||||
|
1. Wandle `docs/FAMILY_ONBOARDING.md` von einer guten Erklaerseite in ein
|
||||||
|
ausfuehrbares Session-Paket um:
|
||||||
|
- 30-Minuten-Ablauf fuer das erste echte Onboarding
|
||||||
|
- Checkliste pro Geraet/Person ohne Namen oder Secret-Werte
|
||||||
|
- klare Abschlusskriterien fuer Vaultwarden, Immich und Mealie
|
||||||
|
- Liste der Operator-Fragen, falls Konten/Startpasswoerter fehlen
|
||||||
|
2. Bereinige `docs/NETWORK_INVENTORY.md`:
|
||||||
|
- Tailscale IPv6, Exit Node, Subnet Router und ACL-Policy nicht als
|
||||||
|
unerklaerte `TBD` stehen lassen
|
||||||
|
- wenn nicht verifizierbar: als konkrete Operator-Frage oder bewusst offene
|
||||||
|
Entscheidung formulieren
|
||||||
|
- Gast-/IoT-Zugriff als Entscheidungspfad dokumentieren, nicht als vage
|
||||||
|
Altlast
|
||||||
|
3. Ziehe `docs/MASTER_TODO.md` nach deinen Edits nach:
|
||||||
|
- echte naechste Schritte in P1/P2
|
||||||
|
- geparkte Entscheidungen nur im geparkten/geschlossenen Bereich
|
||||||
|
- keine Duplikate zu `docs/AUDIT_2026-05-25_TODO.md`
|
||||||
|
4. Falls du weitere diffuse TBDs in Hardware/Network/Family findest: nicht nur
|
||||||
|
melden, sondern in konkrete Entscheidung, geparkten Punkt oder naechsten
|
||||||
|
Operator-Schritt umformulieren.
|
||||||
|
5. Schon erledigte Restore-/Windows-Doku-Aufgaben nicht erneut bearbeiten,
|
||||||
|
ausser du findest einen klaren Widerspruch.
|
||||||
|
6. Am Ende liefere:
|
||||||
|
- geaenderte Dateien
|
||||||
|
- welche Punkte geschlossen wurden
|
||||||
|
- welche Punkte blockiert/geparkt bleiben und warum
|
||||||
|
- welche Operator-Schritte noch noetig sind
|
||||||
|
|
||||||
|
Nicht tun:
|
||||||
|
- Keine Secrets anzeigen oder erfinden.
|
||||||
|
- Kein Push.
|
||||||
|
- Kein `docker`, `ssh` oder Host-Schreibzugriff.
|
||||||
|
- Kein BitLocker, keine Veeam-Aenderung, keine Unraid-User-/Share-Aenderung.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Abschlusskriterien fuer Sonntag
|
||||||
|
|
||||||
|
- `docs/MASTER_TODO.md` ist die fuehrende Liste.
|
||||||
|
- Alle erledigten Punkte haben Beleg in der Detaildoku.
|
||||||
|
- Alle nicht erledigbaren Punkte sind als blockiert/geparkt mit Grund markiert.
|
||||||
|
- `git status` ist verstanden: eigene Doku-Aenderungen vs. bestehende
|
||||||
|
User-Aenderungen sind getrennt.
|
||||||
|
- Commit ist vorbereitet, Push erfolgt nur nach Operator-Freigabe.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Weekend Status - 2026-06-05
|
||||||
|
|
||||||
|
Kurzlebiges Arbeitsboard fuer den Wochenend-Sprint. Fuehrende Liste bleibt
|
||||||
|
`docs/MASTER_TODO.md`; dieses Board haelt nur den aktuellen Arbeitsstand fest.
|
||||||
|
|
||||||
|
## Jetzt laufend
|
||||||
|
|
||||||
|
| Owner | Aufgabe | Status | Naechster Schritt |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Codex | Veeam-Erstbackup `baerchen-c-image` | erledigt | Erster Full-Lauf 2026-06-05 geschrieben; Recovery-Test bleibt offen |
|
||||||
|
| Codex | Veeam-Verifikationshilfe | erledigt | Hilfsskript bleibt fuer spaetere Checks verfuegbar |
|
||||||
|
| Claude | Restore-/Altdoku-Bereinigung | erledigt | Keine weitere Arbeit an Veeam/Windows/Restore-Matrix ohne neuen Widerspruch |
|
||||||
|
| Claude | Family-/Network-Ausfuehrungspaket | erledigt | Masterliste und Weekend-Plan sind nachgezogen |
|
||||||
|
|
||||||
|
## Naechste Operator-Schritte
|
||||||
|
|
||||||
|
| Zeitpunkt | Aufgabe | Ergebnis, das dokumentiert wird |
|
||||||
|
|---|---|---|
|
||||||
|
| Erledigt | Veeam-Erstbackup `baerchen-c-image` pruefen | 2026-06-05 19:46, Full-Lauf erfolgreich, Veeam-GUI 53,8 GB, Dauer 0:11:31 |
|
||||||
|
| Als naechstes | Recovery-USB `VEEAMRE` booten | Boot OK, Netzwerk OK, SMB-Ziel sichtbar |
|
||||||
|
| Im Recovery-Test | Restore Point anzeigen; falls spaeter verschluesselt: Passwort testen | Restore Point sichtbar; vor echtem Restore abgebrochen |
|
||||||
|
| Spaeter | BitLocker-Entscheidung treffen | `aktivieren`, `spaeter`, oder `bewusst aus` in `docs/MASTER_TODO.md`/Baseline nachziehen |
|
||||||
|
|
||||||
|
## Heute bereits geschlossen
|
||||||
|
|
||||||
|
| Thema | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| WinRE/Admin-Altlasten | In Windows-Reinstall-Doku als erledigt/ueberholt markiert |
|
||||||
|
| Restore-Test-Kandidaten | Erledigte Kandidaten aus der aktiven Liste entfernt; Stubs fuer offene Kandidaten ergaenzt |
|
||||||
|
| Family-Onboarding | Aus der Familien-Doku wurde ein konkreter 30-45-Minuten-Terminablauf mit Vorbereitung und Erfolgskriterien |
|
||||||
|
| Network-TBDs | Tailscale-/Gastnetz-/WAN-Failover-Punkte wurden in Messaufgaben, Vorbedingungen oder geparkte Entscheidungen umgewandelt |
|
||||||
|
| Veeam-Erstbackup | Full-Lauf 2026-06-05 erfolgreich geschrieben: Veeam-GUI 53,8 GB, Dauer 0:11:31, MetaCheck 0 Fehler/0 Warnungen, VSS success; Veeam Storage Encryption war nicht aktiv |
|
||||||
|
| Cold-Backup-Rotation | Bewusst Hetzner-only; kein aktives Todo mehr |
|
||||||
|
| USV | Bewusst auf Q3/2026 geparkt; Power-Loss bleibt akzeptiertes Risiko |
|
||||||
|
|
||||||
|
## Nicht ohne neue Freigabe anfassen
|
||||||
|
|
||||||
|
- Keine BitLocker-Aktivierung.
|
||||||
|
- Keine Aenderung am Veeam-Job oder Encryption-Status.
|
||||||
|
- Keine Unraid-User-/Share-Aenderung.
|
||||||
|
- Keine produktiven Host- oder Docker-Schreibbefehle.
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# DR-Workstation Readiness - 2026-06-06
|
||||||
|
|
||||||
|
Automatisch erzeugter lokaler Readiness-Check fuer den Operator-PC. Es wurden keine Secret-Werte, Passphrases oder Private-Key-Inhalte ausgegeben.
|
||||||
|
|
||||||
|
## Zusammenfassung
|
||||||
|
|
||||||
|
| Check | Ergebnis |
|
||||||
|
|---|---|
|
||||||
|
| WSL2 Ubuntu | vorhanden (`Ubuntu 24.04`, WSL Version 2) |
|
||||||
|
| SSH/Git in WSL | vorhanden |
|
||||||
|
| GitHub-Read-Smoke mit DR-Key | ok |
|
||||||
|
| Borg Client | installiert |
|
||||||
|
| Hetzner Storage Box mit DR-Key | ok |
|
||||||
|
| `~/dr-smoke.sh` | vorhanden |
|
||||||
|
| Finaler Borg-Smoke | ok, Operator-Bestaetigung 2026-06-06 10:05:30 |
|
||||||
|
| WSL sudo ohne Passwortprompt | nein, Operator muss Passwort eingeben |
|
||||||
|
|
||||||
|
## Bewertung
|
||||||
|
|
||||||
|
- Der lokale WSL2-/Ubuntu-Unterbau ist vorhanden.
|
||||||
|
- Die DR-Key-Arbeitskopien liegen in WSL unter `~/.ssh/dr-readonly` und `~/.ssh/dr-hetzner`.
|
||||||
|
- GitHub-Read-Smoke und Hetzner-SSH-Smoke sind erfolgreich.
|
||||||
|
- `borgbackup` ist installiert.
|
||||||
|
- Der vollstaendige Bare-Metal-DR-Smoke ist erfolgreich abgeschlossen.
|
||||||
|
|
||||||
|
## Finaler Borg-Smoke
|
||||||
|
|
||||||
|
Operator-Bestaetigung vom 2026-06-06:
|
||||||
|
|
||||||
|
- Befehl: `bash ~/dr-smoke.sh`
|
||||||
|
- GitHub Deploy-Key: HEAD `3a263a4...`
|
||||||
|
- Hetzner SSH-Login: Repos `backup`, `backup2`, `hetzner_borg_appdata`, `hetzner_borg_appdata_critical` sichtbar
|
||||||
|
- Borg-Repo: `ssh://u565255@u565255.your-storagebox.de/./hetzner_borg_appdata_critical`
|
||||||
|
- Repository ID: `5dd9b949...`
|
||||||
|
- Encryption: `Yes (repokey)`
|
||||||
|
- Borg-Statistik: `Original size 1.16 TB`, `Compressed size 1.13 TB`, `Deduplicated size 35.92 GB`
|
||||||
|
- Ergebnis: `DR-Smoke OK (2026-06-06 10:05:30)`
|
||||||
|
|
||||||
|
Die Borg-Passphrase wurde nur interaktiv eingegeben und nicht dauerhaft auf `baerchen` gespeichert.
|
||||||
|
|
||||||
|
## Rohchecks
|
||||||
|
|
||||||
|
### wsl_status
|
||||||
|
|
||||||
|
- ExitCode: `0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
Standarddistribution: Ubuntu
|
||||||
|
|
||||||
|
Standardversion: 2
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### wsl_list
|
||||||
|
|
||||||
|
- ExitCode: `0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
NAME STATE VERSION
|
||||||
|
|
||||||
|
* Ubuntu Stopped 2
|
||||||
|
|
||||||
|
docker-desktop Stopped 2
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### ubuntu_os
|
||||||
|
|
||||||
|
- ExitCode: `0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
Distributor ID: Ubuntu
|
||||||
|
Description: Ubuntu 24.04.4 LTS
|
||||||
|
Release: 24.04
|
||||||
|
Codename: noble
|
||||||
|
6.6.114.1-microsoft-standard-WSL2
|
||||||
|
```
|
||||||
|
|
||||||
|
### tools
|
||||||
|
|
||||||
|
- ExitCode: `0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
/usr/bin/borg
|
||||||
|
borg 1.2.8
|
||||||
|
/usr/bin/ssh
|
||||||
|
OpenSSH_9.6p1 Ubuntu-3ubuntu13.16, OpenSSL 3.0.13 30 Jan 2024
|
||||||
|
/usr/bin/git
|
||||||
|
git version 2.43.0
|
||||||
|
```
|
||||||
|
|
||||||
|
### sudo
|
||||||
|
|
||||||
|
- ExitCode: `0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
sudo-password-needed
|
||||||
|
```
|
||||||
|
|
||||||
|
### wsl_ssh_files
|
||||||
|
|
||||||
|
- ExitCode: `0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
total 40
|
||||||
|
drwx------ 2 michi michi 4096 Jun 6 09:14 .
|
||||||
|
drwxr-x--- 5 michi michi 4096 Jun 6 08:37 ..
|
||||||
|
-rw------- 1 michi michi 411 Jun 6 09:14 dr-hetzner
|
||||||
|
-rw------- 1 michi michi 419 Jun 6 09:14 dr-readonly
|
||||||
|
-rw------- 1 michi michi 411 Apr 4 19:29 id_ed25519
|
||||||
|
-rw-r--r-- 1 michi michi 97 Apr 4 19:29 id_ed25519.pub
|
||||||
|
-rw------- 1 michi michi 6358 Jun 6 09:14 known_hosts
|
||||||
|
-rw------- 1 michi michi 3013 Apr 20 20:13 known_hosts.old
|
||||||
|
-rw------- 1 michi michi 3858 Apr 24 08:27 known_hosts.pre-port222-20260604-122031.bak
|
||||||
|
-rwxr-xr-x 1 michi michi 1311 Jun 6 08:37 /home/michi/dr-smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### github_dr_key_smoke
|
||||||
|
|
||||||
|
- ExitCode: `0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
68d3ace598ee4d1cdad3ed94b63ae5046ac187fb HEAD
|
||||||
|
```
|
||||||
|
|
||||||
|
### hetzner_dr_key_smoke
|
||||||
|
|
||||||
|
- ExitCode: `0`
|
||||||
|
|
||||||
|
```text
|
||||||
|
backup
|
||||||
|
backup2
|
||||||
|
hetzner_borg_appdata
|
||||||
|
hetzner_borg_appdata_critical
|
||||||
|
```
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
# System-Audit 2026-06-05
|
||||||
|
|
||||||
|
**Scope:** Windows-Host `baerchen` (frisch aufgesetzt), Read-only
|
||||||
|
**Referenz-Doku:** `ops/windows-reinstall/docs/laufwerks-neustruktur-2026-06-04.md`, `boot-cleanup-plan-2026-06-04.md`
|
||||||
|
**Durchgeführt:** 2026-06-05, ohne Admin-Rechte
|
||||||
|
**Rohdaten:** `audit/raw/01_volumes_partitions.txt` bis `06_events_hardware.txt`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Ordner- und Laufwerksstruktur (Priorität)
|
||||||
|
|
||||||
|
### 1.1 Soll-Ist-Vergleich: Ordner-Existenz
|
||||||
|
|
||||||
|
| Pfad | Soll | Ist | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `D:\00_Inbox` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\10_Dokumente` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\11_Bilder` | ✓ | vorhanden | OK, aber ReadOnly-Attribut gesetzt |
|
||||||
|
| `D:\12_Videos` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\13_Musik` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\14_Downloads` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\20_Projekte\aktiv` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\20_Projekte\archiv` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\30_Finanzen\Banking4` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\30_Finanzen\WISO_Steuer` | ✓ | vorhanden | OK |
|
||||||
|
| `D:\90_Archiv` | ✓ | vorhanden | OK |
|
||||||
|
| `E:\Steam\steamapps` | ✓ | vorhanden | OK |
|
||||||
|
| `E:\BattleNet` | ✓ | vorhanden | OK |
|
||||||
|
| `E:\EpicGames` | ✓ | vorhanden | OK |
|
||||||
|
| `E:\EA` | ✓ | vorhanden | OK |
|
||||||
|
| `E:\Riot` | ✓ | vorhanden | OK |
|
||||||
|
| `E:\Ubisoft` | ✓ | vorhanden | OK |
|
||||||
|
| **`E:\_Standalone`** | **✓** | **FEHLT** | **LÜCKE** |
|
||||||
|
| `G:\repos` | ✓ | vorhanden | OK |
|
||||||
|
| `G:\tools` | ✓ | vorhanden als `Tools` (Großbuchstabe) | OK (NTFS case-insensitive) |
|
||||||
|
|
||||||
|
**Nicht in Soll-Doku, aber vorhanden:**
|
||||||
|
|
||||||
|
| Pfad | Beurteilung |
|
||||||
|
|---|---|
|
||||||
|
| `D:\Micha\Videos` | Altquelle, fast leer (1 Datei), Rest wurde bereinigt |
|
||||||
|
| `D:\WSL` | WSL-Datenpfad, nicht in Doku erwähnt, aber logisch |
|
||||||
|
| `G:\Apps` | Zweck unklar, nicht dokumentiert |
|
||||||
|
| `G:\Gitea_Clone` | Bewusst so (homelab-infra bleibt laut Doku unangetastet) |
|
||||||
|
| `G:\Workspace` | Nicht dokumentiert, wahrscheinlich Dev-Workspace |
|
||||||
|
|
||||||
|
### 1.2 Known-Folder-Redirects
|
||||||
|
|
||||||
|
| Ordner | Soll (Doku) | Ist (gemessen) | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Desktop | `D:\Micha\Desktop` | `D:\00_Inbox\Desktop` | **ABWEICHUNG** |
|
||||||
|
| Dokumente | `D:\10_Dokumente` | `D:\10_Dokumente` | OK |
|
||||||
|
| Downloads | `D:\14_Downloads` | `D:\14_Downloads` | OK |
|
||||||
|
| Bilder | `D:\11_Bilder` | `D:\11_Bilder` | OK |
|
||||||
|
| Musik | `D:\13_Musik` | `D:\13_Musik` | OK |
|
||||||
|
| Videos | `D:\12_Videos` | `D:\12_Videos` | OK |
|
||||||
|
|
||||||
|
**Desktop-Befund (Detail):**
|
||||||
|
- Soll-Doku schreibt: `D:\Micha\Desktop` (als bewusster Sonderfall ohne nummerierten Ordner).
|
||||||
|
- Ist: Desktop zeigt auf `D:\00_Inbox\Desktop` — dieser Ordner existiert, enthält 4 Dateien.
|
||||||
|
- `D:\Micha\Desktop` existiert **nicht**.
|
||||||
|
- `D:\Micha` enthält nur noch `Videos` (1 Datei, leer).
|
||||||
|
- Fazit: Das Known-Folder-Ziel wurde nach der Doku-Erstellung nochmals geändert. Die Doku ist in diesem Punkt veraltet. Der Desktop liegt funktional korrekt auf D:, aber das Ziel weicht vom dokumentierten Soll ab. **Doku-Update empfohlen.**
|
||||||
|
|
||||||
|
### 1.3 Doppelbestand D:\Micha\* vs. neue Nummernstruktur
|
||||||
|
|
||||||
|
| Alt | Dateien | Neu | Dateien | Bewertung |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `D:\Micha\Dokumente` | NICHT MEHR VORHANDEN | `D:\10_Dokumente` | 4011 / 595 MB | Bereinigt ✓ |
|
||||||
|
| `D:\Micha\Bilder` | NICHT MEHR VORHANDEN | `D:\11_Bilder` | 7789 / 12,4 GB | Bereinigt ✓ |
|
||||||
|
| `D:\Micha\Videos` | 1 Datei, ~0 MB | `D:\12_Videos` | 1 Datei, ~0 MB | Quasi-leer, kein Doppelbestand |
|
||||||
|
| `D:\Micha\Musik` | NICHT MEHR VORHANDEN | `D:\13_Musik` | 0 Dateien | Bereinigt ✓ |
|
||||||
|
| `D:\Micha\Downloads` | NICHT MEHR VORHANDEN | `D:\14_Downloads` | 2186 / 2,2 GB | Bereinigt ✓ |
|
||||||
|
| `D:\Micha\Finanzen` | NICHT MEHR VORHANDEN | `D:\30_Finanzen` | 126 / 123 MB | Bereinigt ✓ |
|
||||||
|
|
||||||
|
**Fazit:** Der befürchtete Doppelbestand ist weitgehend aufgelöst. Nur `D:\Micha\Videos` ist noch vorhanden, ist aber inhaltlich leer. `D:\Micha` kann nach manueller Prüfung von Videos entfernt werden.
|
||||||
|
|
||||||
|
### 1.4 Labels
|
||||||
|
|
||||||
|
| Laufwerk | Soll | Ist | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| D: | `Daten-Projekte` | `Daten-Projekte` | OK ✓ |
|
||||||
|
| E: | `Games` | `Games` | OK ✓ |
|
||||||
|
| H: | unveraendert | `Externe HDD` | OK ✓ |
|
||||||
|
|
||||||
|
### 1.5 Rollen-Konsistenz und Partitions-Layout
|
||||||
|
|
||||||
|
| Laufwerk | Soll-Rolle | Ist | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| C: | Windows + kleine Programme | Disk 0, 167 GB SATA | OK |
|
||||||
|
| D: | Daten & Projekte | Disk 1, 168 GB SATA | OK |
|
||||||
|
| E: | Games | Disk 2, **930 GB** NVMe (nach F-Merge) | OK ✓ |
|
||||||
|
| F: | Altes Windows (löschen) | **Nicht mehr vorhanden** | Abgeschlossen ✓ |
|
||||||
|
| G: | Arbeits-SSD, Homelab/Dev | Disk 3, 931 GB NVMe | OK |
|
||||||
|
| H: | Externe Backup-HDD | Disk 4, 7.28 TB USB | OK |
|
||||||
|
|
||||||
|
E: und das ehemalige F: sind jetzt eine einzige 930 GB Partition auf Disk 2. Layout ist sauber.
|
||||||
|
|
||||||
|
### 1.6 Fachliche Gesamtbewertung der Struktur
|
||||||
|
|
||||||
|
**Stärken:**
|
||||||
|
- Die Nummernstruktur auf D: ist vollständig angelegt und die Known Folders zeigen bis auf Desktop korrekt dorthin.
|
||||||
|
- Der Doppelbestand ist fast vollständig bereinigt — das war die größte Risikoquelle.
|
||||||
|
- F: ist weg, E: ist auf volle Disk-Kapazität gewachsen — die BCD-Bereinigung und Partition-Erweiterung wurde sauber abgeschlossen.
|
||||||
|
- Label-Benennung konsistent.
|
||||||
|
- G: ist operational (repos, Tools, Gitea_Clone vorhanden).
|
||||||
|
|
||||||
|
**Lücken und Inkonsistenzen:**
|
||||||
|
1. **Desktop-Redirect weicht von Doku ab** (Ist: `D:\00_Inbox\Desktop`, Doku: `D:\Micha\Desktop`). Da `D:\Micha\Desktop` nicht existiert und der Desktop funktioniert, ist die Doku das Problem, nicht das System.
|
||||||
|
2. **`E:\_Standalone` fehlt** — laut Doku angelegt, tatsächlich nicht vorhanden. Kein funktionaler Schaden, aber Inkonsistenz zur Rollenbeschreibung.
|
||||||
|
3. **`D:\11_Bilder` hat ReadOnly-Attribut** auf Ordner-Ebene gesetzt — ungewöhnlich, keine erkennbare Ursache. Kein Showstopper, aber prüfenswert.
|
||||||
|
4. **`G:\Apps`, `G:\Workspace`** sind nicht in der Soll-Doku definiert. Kein Problem an sich, aber für spätere Audits hilfreich zu dokumentieren.
|
||||||
|
5. **`D:\WSL`** nicht dokumentiert — WSL-Datenpfade dort gehören explizit erwähnt.
|
||||||
|
6. **`D:\13_Musik`** ist leer (0 Dateien) — entweder war `D:\Micha\Musik` schon leer, oder die Kopie ist ausgeblieben. Zu prüfen ob Musik aus PostDelta-Backup nachgezogen werden muss.
|
||||||
|
|
||||||
|
**Gesamturteil:** Die Struktur ist in sich schlüssig und der Umbau ist zu ~95% abgeschlossen. Die verbleibenden Punkte sind kleine Doku-Lücken und ein fehlender Ordner, kein strukturelles Problem.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. OS-Baseline
|
||||||
|
|
||||||
|
| Feld | Wert | Bewertung |
|
||||||
|
|---|---|---|
|
||||||
|
| Edition | Windows 11 Pro | OK |
|
||||||
|
| Build | 26200 (Insider/Preview-Build) | Achtung: kein Stable-Channel-Build |
|
||||||
|
| Aktivierung | OEM_DM, aktiv | OK |
|
||||||
|
| Installiert | 2026-05-10 | ~25 Tage alt |
|
||||||
|
| Letzter Boot | 2026-06-05 07:57 | Frisch gebootet |
|
||||||
|
| Ausstehende Updates | 0 | OK |
|
||||||
|
| Reboot pending | Nein | OK |
|
||||||
|
|
||||||
|
**Befund Build 26200:** Das ist ein Windows Insider/Canary-Channel Build, kein Produktions-Release. Für einen Nerd-Einsatz vertretbar, aber mit dem Wissen verbunden, dass Insider-Builds weniger stabil sind und keine LTS-Garantie haben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Security
|
||||||
|
|
||||||
|
### Defender
|
||||||
|
- Aktiv, TamperProtection an, Signaturen aktuell. **OK.**
|
||||||
|
- Ausschlüsse und ASR-Regeln: nur als Admin lesbar — **kein Befund, aber blind spot.**
|
||||||
|
|
||||||
|
### Firewall
|
||||||
|
- Alle drei Profile aktiv. DefaultInboundAction `NotConfigured` bedeutet im Windows-Default: eingehend blockieren, ausgehend erlauben. **OK.**
|
||||||
|
- Port 27036 (Steam Remote Play) lauscht auf `0.0.0.0` — also LAN-seitig offen. Erwartetes Steam-Verhalten, aber explizit im Bewusstsein halten.
|
||||||
|
|
||||||
|
### BitLocker
|
||||||
|
- Nicht prüfbar ohne Admin. **Blind spot — Empfehlung: BitLocker für C: und D: aktivieren.**
|
||||||
|
|
||||||
|
### Secure Boot / TPM
|
||||||
|
- Nicht prüfbar ohne Admin. Hardware MSI MS-7D32 unterstützt beides. Status unbekannt.
|
||||||
|
|
||||||
|
### UAC
|
||||||
|
- Standard-Konfiguration korrekt (Secure Desktop aktiv). **OK.**
|
||||||
|
|
||||||
|
### Lokale Admins
|
||||||
|
- `Administrator` (Built-in) + `michi`. Zwei Accounts in Admins ist normal für einen Einzel-PC. OK.
|
||||||
|
|
||||||
|
### SSH Key Permissions
|
||||||
|
- `id_ed25519` hat `VORDEFINIERT\Administratoren FullControl` — das ist zu weit offen.
|
||||||
|
- SSH-Clients unter Windows tolerieren das, aber best practice ist: nur der eigene User darf lesen.
|
||||||
|
- **Empfehlung:** `icacls` Berechtigungen auf User only setzen (als Admin ausführen).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Storage & Boot
|
||||||
|
|
||||||
|
- Alle 5 physischen Disks: **Healthy / OK.**
|
||||||
|
- Wear-Level via `Get-StorageReliabilityCounter`: keine Ausgabe (SATA-SSDs und USB HDD liefern keine WMI-Daten). CrystalDiskInfo ist installiert — dort manuell prüfen.
|
||||||
|
- Die zwei Intel SATA SSDs (Disk 0 + 1) sind **180 GB** — typische Einzel-Partition-Auslastung auf C: ~36% und D: ~11%, reichlich Luft.
|
||||||
|
- **BCD:** ohne Admin nicht lesbar. Doku bestätigt sauberen Zustand nach Cleanup + Neustarttest.
|
||||||
|
- **WinRE:** ohne Admin nicht lesbar. Doku sagt Disabled — muss vor künftiger Partitionsarbeit aktiviert werden.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Netzwerk
|
||||||
|
|
||||||
|
- Ethernet: 192.168.178.103, DNS auf Kallilabcore (AdGuard). **Korrekt.**
|
||||||
|
- Tailscale: aktiv, dieser Rechner als `baerchen-1` online, direkter Pfad zu `kallilabcore`. **OK.**
|
||||||
|
- Kein SSH-Config — alle SSH-Verbindungen laufen ohne Host-Aliases. Funktional, aber unpraktisch.
|
||||||
|
- Lauschende Ports: Keine auffälligen Exposition nach außen außer SMB (139/445 — LAN-normal) und Steam 27036.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Remote-Management / SSH
|
||||||
|
|
||||||
|
- Kein `~\.ssh\config` vorhanden. Empfehlung: Host-Aliases anlegen (z.B. `Host kallilabcore`).
|
||||||
|
- SSH-Key vorhanden und aktuell.
|
||||||
|
- **Key-Rechte zu weit (s. Security).**
|
||||||
|
- Docker contexts: `desktop-linux` aktiv. Docker Desktop läuft.
|
||||||
|
- kubectl: keine Contexts — erwartet (kein k8s im Homelab).
|
||||||
|
- Tailscale: direkter Pfad zu Homelab aktiv, SSH über Tailscale-IP funktioniert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Dev-Toolchain
|
||||||
|
|
||||||
|
| Tool | Version | Bewertung |
|
||||||
|
|---|---|---|
|
||||||
|
| git | 2.54.0 | Aktuell, OK |
|
||||||
|
| Python | 3.13.13 | Aktuell, OK |
|
||||||
|
| Node.js | 24.16.0 (LTS) | Aktuell, OK |
|
||||||
|
| Go | 1.26.4 | Aktuell, OK |
|
||||||
|
| Commit-Signing | nicht konfiguriert | Optional, aber für Homelab-GitOps empfohlen |
|
||||||
|
|
||||||
|
WSL Ubuntu ist installiert aber gestoppt. docker-desktop läuft als WSL2-Backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Hardware & Performance
|
||||||
|
|
||||||
|
- i5-14600KF, 14C/20T, 31.8 GB RAM — für Homelab-Dev-Rechner gut ausgestattet.
|
||||||
|
- Energieplan: **Ausbalanciert** — für einen Gaming- und Dev-Rechner suboptimal. `Höchstleistung` oder `Ultimative Leistung` wäre bei dauerhafter Nutzung besser.
|
||||||
|
- Keine echten Gerätekonflikte in PnP (alle "Unknown" sind erwartet: ghosted devices, Netzwerkgeräte, VSS).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Autostart & Persistenz
|
||||||
|
|
||||||
|
Läuft automatisch: Brave Update, Steam, Razer Synapse, Docker Desktop, iCUE, Realtek Audio, Tailscale, Ollama.
|
||||||
|
|
||||||
|
**Auffällig:** `SoftLanding\CreativeManagementTask` — unbekannter Scheduled Task, nicht einem Standard-Produkt zuzuordnen. Sollte manuell im Task Scheduler geprüft werden (Quelle, Executable, Publisher).
|
||||||
|
|
||||||
|
OneDrive läuft mit drei Tasks (Startup + Update) — falls Daten-Sync nicht gewünscht ist, sollte OneDrive deaktiviert werden, da es Dokumente/Bilder/etc. stummschalten könnte (bekanntes Windows-Verhalten nach Known-Folder-Redirect).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Zuverlässigkeit
|
||||||
|
|
||||||
|
| Event ID | Anzahl | Beschreibung | Risiko |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 20 | 70 | Defender KB4052623 Update-Fehler (0x80240016) | Niedrig — Timing, Defender aktuell |
|
||||||
|
| 10010 | 15 | DCOM Server Timeout | Niedrig — Windows-Hintergrund |
|
||||||
|
| 7000 | 3 | Steam Service Start fehlgeschlagen | Niedrig — Race Condition beim Boot |
|
||||||
|
| 7023 | 3 | Windows Modules Installer beendet mit Fehler | Mittel — Update-Abbrüche prüfen |
|
||||||
|
| **6008** | **2** | **Unerwartetes Herunterfahren 2026-05-19 13:56** | **Mittel — einmaliger BSOD/Stromausfall** |
|
||||||
|
| 7034 | 2 | MSI Center Service Absturz | Niedrig |
|
||||||
|
|
||||||
|
- **Kein Crash-Dump** vorhanden (`C:\Windows\Minidump` leer). Entweder ist kein BSOD gewesen (Stromausfall), oder Dump-Einstellungen schreiben nicht.
|
||||||
|
- Empfehlung: Dump-Einstellungen auf "Kleiner Speicherauszug" oder "Vollständiger Speicherauszug" prüfen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Homelab-Server (ausstehend)
|
||||||
|
|
||||||
|
**Status: NICHT DURCHGEFÜHRT**
|
||||||
|
|
||||||
|
SSH-Config ist leer — kein Host-Alias konfiguriert. Tailscale zeigt `kallilabcore` als aktiv auf `100.80.98.33` / `192.168.178.58`.
|
||||||
|
|
||||||
|
**Bitte bestätigen:**
|
||||||
|
- SSH-User für Kallilabcore (wahrscheinlich `root`?)
|
||||||
|
- Soll ich `ssh root@192.168.178.58` oder über Tailscale-IP verwenden?
|
||||||
|
|
||||||
|
Nach Bestätigung wird der Homelab-Teil nachgezogen und dieser Report ergänzt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Gesamt-Findings (priorisiert)
|
||||||
|
|
||||||
|
### Kritisch / Handlungsbedarf vor nächster Partitionsarbeit
|
||||||
|
| # | Befund | Begründung |
|
||||||
|
|---|---|---|
|
||||||
|
| K1 | WinRE ist Disabled (laut Doku) | Ohne WinRE kein automatisches Recovery. Muss aktiviert werden bevor weitere Disk-Ops. |
|
||||||
|
| K2 | BitLocker-Status unbekannt (kein Admin) | C: und D: sollten verschlüsselt sein — aktuell Blind Spot. |
|
||||||
|
|
||||||
|
### Mittel / Zeitnah klären
|
||||||
|
| # | Befund |
|
||||||
|
|---|---|
|
||||||
|
| M1 | Desktop-Redirect zeigt auf `D:\00_Inbox\Desktop`, Doku sagt `D:\Micha\Desktop` — Doku aktualisieren |
|
||||||
|
| M2 | `E:\_Standalone` fehlt — Ordner anlegen oder aus Doku streichen |
|
||||||
|
| M3 | SSH Private Key Permissions zu weit (Admins haben FullControl) |
|
||||||
|
| M4 | Energieplan "Ausbalanciert" — für Gaming/Dev `Höchstleistung` empfohlen |
|
||||||
|
| M5 | `SoftLanding\CreativeManagementTask` unbekannt — Quelle und Publisher prüfen |
|
||||||
|
| M6 | Unerwartetes Herunterfahren 2026-05-19 — Ursache klären (Stromausfall? BSOD ohne Dump?) |
|
||||||
|
| M7 | `D:\11_Bilder` hat ReadOnly-Attribut — Ursache und Auswirkung prüfen |
|
||||||
|
|
||||||
|
### Niedrig / Nice-to-have
|
||||||
|
| # | Befund |
|
||||||
|
|---|---|
|
||||||
|
| N1 | SSH-Config leer — Host-Aliases anlegen |
|
||||||
|
| N2 | Git commit.gpgsign nicht gesetzt — für GitOps-Commits empfohlen |
|
||||||
|
| N3 | `D:\Micha\Videos` noch vorhanden (1 leere Datei) — bereinigen |
|
||||||
|
| N4 | `G:\Apps`, `G:\Workspace` nicht in Doku — dokumentieren oder strukturieren |
|
||||||
|
| N5 | `D:\WSL` nicht in Doku — erwähnen |
|
||||||
|
| N6 | `D:\13_Musik` leer — Musik aus PostDelta-Backup nachziehen? |
|
||||||
|
| N7 | OneDrive läuft (3 Tasks) — prüfen ob Sync für D:\10_Dokumente etc. gewünscht |
|
||||||
|
| N8 | Energiesparmodus-Dump-Einstellungen prüfen (kein Dump für 6008-Event) |
|
||||||
|
| N9 | `D:\DumpStack.log` ist ein Artefakt aus der alten D:-Nutzung, kann bereinigt werden |
|
||||||
|
| N10 | Insider-Build 26200 — bewusste Entscheidung, aber dokumentieren |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Nächste Schritte (empfohlen, nicht ausgeführt)
|
||||||
|
|
||||||
|
1. **Homelab-SSH-Zugang bestätigen** und Homelab-Audit nachziehen.
|
||||||
|
2. **WinRE aktivieren** (als Admin: `reagentc /enable`) — Voraussetzung für künftige Disk-Ops.
|
||||||
|
3. **BitLocker Status prüfen** (als Admin: `Get-BitLockerVolume`) und ggf. für C:/D: aktivieren.
|
||||||
|
4. **SSH-Key-Permissions straffen**: `icacls $env:USERPROFILE\.ssh\id_ed25519 /inheritance:r /grant:r "$env:USERNAME:F"` (als Admin).
|
||||||
|
5. **`SoftLanding\CreativeManagementTask` untersuchen** — im Task Scheduler Quelle und Aktion prüfen.
|
||||||
|
6. **Doku `laufwerks-neustruktur-2026-06-04.md`** unter Abschnitt Desktop-Befund korrigieren: Ist-Ziel `D:\00_Inbox\Desktop`.
|
||||||
|
7. **`E:\_Standalone`** anlegen falls geplant.
|
||||||
|
8. **`D:\Micha\Videos`** prüfen und ggf. löschen.
|
||||||
|
9. **CrystalDiskInfo** für SSD Wear-Level öffnen und Werte dokumentieren.
|
||||||
|
10. **Energieplan** auf `Höchstleistung` oder `Ultimative Leistung` umstellen.
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
# Homelab-Optimierung — Assessment 2026-06-10
|
||||||
|
|
||||||
|
Read-only-Analyse des Repos (Stand `master`, lokale Arbeitskopie 2026-06-10).
|
||||||
|
Keine produktiven Änderungen durchgeführt. Alle Empfehlungen sind Vorschläge
|
||||||
|
mit Rollback-Plan; nichts wurde deployed.
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Das KalliLab-CORE-Homelab ist für ein Ein-Host-Setup ungewöhnlich reif:
|
||||||
|
GitOps mit Gitea+Komodo, sauberes Netzmodell (frontend/backend/app-intern),
|
||||||
|
Authelia mit 2FA-Catch-all, belegte Restore-Drills für alle Tier-1/2-Dienste,
|
||||||
|
Off-site-Borg nach Hetzner, DR-Workstation-Kit, Monitoring mit Prometheus/
|
||||||
|
Loki/Grafana/Alertmanager→ntfy. Die Doku-Disziplin ist das eigentliche Asset.
|
||||||
|
|
||||||
|
Die größten realen Lücken liegen nicht in der Architektur, sondern in der
|
||||||
|
**Container-Betriebsebene**: 20 von 30 Stacks haben keinen Healthcheck, kein
|
||||||
|
einziger Container hat Memory-/CPU-Limits, und mehrere Images laufen auf
|
||||||
|
mutablen Tags (`release`, `latest`, `:2`), bei denen Renovate-Digest-Bumps
|
||||||
|
faktisch unkontrollierte Versionssprünge sind — am kritischsten bei Immich.
|
||||||
|
Dazu kommen zwei strukturelle Risiken: **AdGuard ist DNS-SPOF ohne Fallback**
|
||||||
|
(hat bereits einen Teil-Deploy-Ausfall verursacht) und **Borg-Backups sind
|
||||||
|
vom Host aus löschbar** (append-only bewusst abgelehnt, aber die kostenlose
|
||||||
|
Kompensation — Hetzner-Storage-Box-Snapshots — ist nicht aktiviert).
|
||||||
|
|
||||||
|
## Gesamtbewertung
|
||||||
|
|
||||||
|
| Bereich | Note | Begründung |
|
||||||
|
|---|---|---|
|
||||||
|
| Architektur | **sehr gut** | klares Netzmodell, dokumentierte Ausnahmen, ein Ingress, Compose-first konsequent |
|
||||||
|
| Netzwerk/DNS/Proxy | **gut, ein SPOF** | Traefik v3 labelbasiert sauber; AdGuard+Unbound ohne zweiten Resolver — bekannter Vorfall (Bulk-Deploy-DNS-Ausfall, `docs/runbooks/komodo-bulk-deploy-dns.md`) |
|
||||||
|
| Container-Betrieb | **mittel** | 10/30 Stacks mit Healthcheck, 0 Ressourcen-Limits, mutable Tags hinter Digests versteckt |
|
||||||
|
| Storage/Backups | **sehr gut** | Borg→Hetzner, Dumps, H:/-Nearline, Restore-Drills mit Reports belegt; offen: Backup-Löschschutz |
|
||||||
|
| Security/Secrets | **gut** | `_FILE`/Stack-ENV konsequent, 2FA-Catch-all, WAN nur 443/tcp; `no-new-privileges` nur in 10/30 Stacks trotz P8-Pflichtregel |
|
||||||
|
| Monitoring/Alerting | **gut** | Prometheus/Blackbox/Loki/ntfy-Kette steht; Monitoring-Stack selbst hat keine Healthchecks und überwacht sich nicht selbst |
|
||||||
|
| Automatisierung/IaC | **sehr gut** | Komodo-Webhooks, Renovate, Posture-Check, Critical-Events-Watcher; manuelle Sync-Ausnahmen (traefik/dynamic, Authelia-Config) sind dokumentiert, aber fehleranfällig |
|
||||||
|
| Ausfallsicherheit | **bewusst begrenzt** | Ein Host, keine USV (geparkt Q3/2026), kein WAN-Failover — als akzeptiertes Risiko dokumentiert, das ist legitim |
|
||||||
|
| Strom/Kosten | **keine Daten** | keine Verbrauchsmessung im Repo sichtbar — siehe offene Fragen |
|
||||||
|
|
||||||
|
## Top 10 Verbesserungen nach Mehrwert
|
||||||
|
|
||||||
|
### 1. Immich vom `release`-Tag auf Versions-Tag pinnen
|
||||||
|
- **Beobachtung:** `apps/immich/docker-compose.yml:4` nutzt `immich-server:release@sha256:...` (ebenso ML). Renovate aktualisiert Digests — beim `release`-Tag ist ein "Digest-Update" in Wahrheit ein Major-/Minor-Versionssprung, ohne dass es im PR-Titel sichtbar wird. Immich ist berüchtigt für Breaking Changes zwischen Minors.
|
||||||
|
- **Warum relevant:** Ein gemergter "harmloser" Digest-PR kann Immich unangekündigt auf eine inkompatible Version heben (DB-Migrationen, ML-Modell-Wechsel).
|
||||||
|
- **Änderung:** Tag auf die konkret laufende Version umstellen (z. B. `immich-server:v2.x.y@sha256:<aktueller Digest>`), gleiche Vorgehensweise wie bei Mealie/Paperless. Laufende Version ermitteln: `docker exec immich_server cat /usr/src/app/package.json | grep version` oder Immich-UI → Version.
|
||||||
|
- **Verifikation:** Renovate erzeugt danach Versions-PRs statt stiller Digest-PRs; `docker inspect immich_server --format '{{.Config.Image}}'` zeigt den Versionstag.
|
||||||
|
- **Rollback:** Commit revert; Digest bleibt identisch, kein Redeploy-Zwang.
|
||||||
|
- **Nebenwirkungen:** keine zur Laufzeit (Digest unverändert). | Nutzen: **hoch** | Risiko: niedrig | Aufwand: klein | sofort
|
||||||
|
- Gleiches Muster prüfen für: `komodo:2`, `ddns-updater:latest`, `scrutiny:latest-omnibus`, `glances:latest-full` sowie tag-lose digest-only Images (`mail-archiver`, `borg-ui`, `ntfy` — Version im Compose unsichtbar).
|
||||||
|
|
||||||
|
### 2. Hetzner-Storage-Box-Snapshots als Ransomware-/Fehlbedienungsschutz aktivieren
|
||||||
|
- **Beobachtung:** Borg `append-only` wurde am 2026-06-01 bewusst verworfen (forced-command brach Key-Auth). Damit kann jeder mit dem Borg-Key (Host, borg-ui-Container mit `/local/secrets`-Mount) Archive **löschen** — ein kompromittierter Host vernichtet auch das Off-site-Backup.
|
||||||
|
- **Warum relevant:** Das ist die einzige verbliebene Lücke in einer sonst sehr guten Backup-Kette.
|
||||||
|
- **Änderung:** In der Hetzner-Robot-Konsole automatische Snapshots der Storage Box aktivieren (z. B. täglich, 7–14 Tage Retention). Snapshots sind host-seitig nicht löschbar und im Storage-Box-Preis enthalten.
|
||||||
|
- **Verifikation:** Robot-Konsole zeigt Snapshot-Liste; nach 2 Tagen: zwei Snapshots vorhanden. Restore-Probe: einzelne Datei aus Snapshot über das Snapshot-Verzeichnis lesen.
|
||||||
|
- **Rollback:** Snapshots deaktivieren — rein additiv, keine Auswirkung auf Borg.
|
||||||
|
- **Nebenwirkungen:** Snapshots zählen ggf. anteilig aufs Quota (aktuell 65 GB / 1 TB — viel Luft). | Nutzen: **sehr hoch** | Risiko: niedrig | Aufwand: klein (<30 min) | sofort
|
||||||
|
|
||||||
|
### 3. DNS-Fallback gegen den AdGuard-SPOF
|
||||||
|
- **Beobachtung:** AdGuard ist einziger LAN-Resolver. Der dokumentierte Vorfall (Bulk-Deploy: AdGuard-Recreate → Host ohne DNS → Komodo-Pulls scheitern) ist genau dieses Muster; das Runbook behandelt nur das Symptom.
|
||||||
|
- **Warum relevant:** Jeder AdGuard-Ausfall (Update, OOM, Disk) nimmt LAN + Host-DNS gleichzeitig mit — auch die Reparaturfähigkeit (Image-Pulls!) hängt daran.
|
||||||
|
- **Änderung (gestuft):**
|
||||||
|
- a) Host-Ebene: zweiten Nameserver (z. B. `1.1.1.1`) in der Unraid-Netzwerkkonfig als Fallback hinter `192.168.178.58` eintragen. Damit kann der Host immer Images pullen.
|
||||||
|
- b) LAN-Ebene: in der FRITZ!Box als zweiten lokalen DNS die FRITZ!Box selbst (oder einen Public DNS) hinterlegen — bewusster Trade-off: bei AdGuard-Down kein Ad-Blocking statt kein Internet.
|
||||||
|
- **Verifikation:** `docker stop adguard` im Wartungsfenster → `nslookup gitea.com` auf dem Host funktioniert weiterhin; danach `docker start adguard`.
|
||||||
|
- **Rollback:** Nameserver-Eintrag entfernen.
|
||||||
|
- **Nebenwirkungen:** DNS-Anfragen können am Filter vorbeilaufen, solange AdGuard down ist (gewollt); Fallback-Resolver sieht dann Anfragen (Privacy-Abwägung). | Nutzen: **hoch** | Risiko: niedrig | Aufwand: klein | diese Woche
|
||||||
|
|
||||||
|
### 4. Healthchecks für die App-Stacks nachrüsten
|
||||||
|
- **Beobachtung:** Nur 10 von 30 Compose-Dateien definieren Healthchecks (traefik, gitea, vaultwarden, authelia, postgresql17, redis, komodo, bentopdf, glances, hermes). **Ohne:** Nextcloud (App+DB+Redis), Immich (alle 4), Paperless, Mealie, Mail-Archiver, n8n, AdGuard, Unbound und der komplette Monitoring-Stack (11 Services).
|
||||||
|
- **Warum relevant:** Ohne Healthcheck meldet Docker "Up", auch wenn die App hängt; der Critical-Events-Watcher sieht nur `die`/`oom`, keine Hänger. Prometheus-Blackbox prüft nur HTTP-Routen von außen.
|
||||||
|
- **Änderung:** Pro Stack einen minimalen Healthcheck ergänzen, priorisiert: Nextcloud (`curl -f http://localhost/status.php`), Paperless, Mealie, n8n, Unbound (`drill @127.0.0.1 cloudflare.com` bzw. `unbound-control status`), AdGuard. Stackweise deployen, nicht als Bulk (siehe DNS-Runbook!).
|
||||||
|
- **Verifikation:** `docker ps --format '{{.Names}} {{.Status}}'` zeigt `(healthy)`; cAdvisor/Glance zeigen Health-Status.
|
||||||
|
- **Rollback:** Healthcheck-Block entfernen, Redeploy — kein Datenrisiko.
|
||||||
|
- **Nebenwirkungen:** Falsch kalibrierte Checks (zu kurze `start_period`) können Flapping erzeugen; konservativ starten (`interval: 60s`, `retries: 5`). | Nutzen: **hoch** | Risiko: niedrig | Aufwand: mittel | diesen Monat
|
||||||
|
|
||||||
|
### 5. Memory-Limits für die größten Verbraucher
|
||||||
|
- **Beobachtung:** Kein einziger Service hat `mem_limit`/`deploy.resources`. Auf einem Ein-Host-System konkurrieren ~50 Container; ein Speicherleck (Immich-ML, Nextcloud-PHP, Loki) kann den Host-OOM-Killer auslösen, der dann beliebige Tier-1-Container trifft (Postgres!).
|
||||||
|
- **Warum relevant:** Der OOM-Killer wählt nach Score, nicht nach Wichtigkeit. Limits machen den Blast-Radius deterministisch: die fehlerhafte App stirbt, nicht die Datenbank.
|
||||||
|
- **Änderung:** Erst messen: `docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}'` über ein paar Tage (oder cAdvisor-Dashboard `container_memory_working_set_bytes`). Dann Limits = Peak × 1,5 für die Top-5-Verbraucher (typisch: immich-ml, nextcloud, paperless, plex, prometheus) setzen.
|
||||||
|
- **Verifikation:** `docker inspect <c> --format '{{.HostConfig.Memory}}'`; Grafana-Panel Memory vs. Limit; keine neuen `oom`-Events im Critical-Events-Log.
|
||||||
|
- **Rollback:** Limit-Zeilen entfernen, Redeploy.
|
||||||
|
- **Nebenwirkungen:** Zu knappe Limits OOM-killen die App selbst — deshalb messen statt raten, und Limits nur bei unkritischen Apps zuerst. | Nutzen: **hoch** | Risiko: mittel | Aufwand: mittel | diesen Monat
|
||||||
|
|
||||||
|
### 6. `no-new-privileges` flächendeckend gemäß P8
|
||||||
|
- **Beobachtung:** Architektur-Regel P8 verlangt `no-new-privileges:true` standardmäßig; gesetzt ist es nur in 10 von 30 Stacks. Es fehlt u. a. bei allen Apps mit WAN-Exposition (Nextcloud, Immich, Paperless, Mealie, ntfy, n8n).
|
||||||
|
- **Warum relevant:** Billige Defense-in-Depth gegen Privilege-Escalation nach App-Kompromittierung — genau bei den exponierten Diensten am wertvollsten. Aktuell: dokumentierte Regel ≠ gelebter Stand (Policy-Drift).
|
||||||
|
- **Änderung:** `security_opt: ["no-new-privileges:true"]` in die fehlenden Stacks, stackweise mit Smoke-Test. Vorsicht bei Images mit s6/sudo-Setup (LSIO-Images wie speedtest/code-server haben es teils schon — prüfen) und bei Plex (Host-Netz, zuerst testen).
|
||||||
|
- **Verifikation:** `docker inspect <c> --format '{{.HostConfig.SecurityOpt}}'`; Posture-/Policy-Check erweitern, damit Drift künftig alarmiert.
|
||||||
|
- **Rollback:** Zeile entfernen, Redeploy.
|
||||||
|
- **Nebenwirkungen:** Container, die intern setuid brauchen (selten: einige Init-Systeme), starten nicht — fällt im Smoke-Test sofort auf. | Nutzen: mittel | Risiko: niedrig | Aufwand: mittel | diesen Monat
|
||||||
|
|
||||||
|
### 7. traefik/dynamic-Sync automatisieren statt manuell
|
||||||
|
- **Beobachtung:** `traefik/dynamic/*` (middlewares, tls, dashboards, plex) wird laut dokumentierter Ausnahme **manuell** auf den Host synchronisiert. Das ist die klassische Quelle für "Repo sagt A, Host macht B" — besonders heikel, weil hier Auth-Middlewares definiert sind.
|
||||||
|
- **Warum relevant:** Ein vergessener Sync nach einer Middleware-Änderung kann unbemerkt eine Schutzschicht im Live-Zustand alt lassen; auffallen würde es erst beim Audit.
|
||||||
|
- **Änderung:** Kleines Sync-Skript analog `services/authelia-diff.sh`: Repo-Spiegel `/mnt/user/services/homelab-infra/traefik/dynamic/` per `rsync --checksum --dry-run` gegen `/mnt/user/appdata/traefik/dynamic/` diffen; Diff ≠ leer → ntfy-Warnung über den bestehenden Posture-Check. (Stufe 2 optional: automatisch syncen; erst nur alarmieren.)
|
||||||
|
- **Verifikation:** Testweise eine Whitespace-Änderung im Repo-Spiegel → Posture-Check meldet `traefik_dynamic_drift`.
|
||||||
|
- **Rollback:** Check aus dem Posture-Skript entfernen; rein lesend, kein Produktionsrisiko.
|
||||||
|
- **Nebenwirkungen:** keine (read-only Check). | Nutzen: mittel | Risiko: niedrig | Aufwand: klein | diese Woche
|
||||||
|
|
||||||
|
### 8. Watchdog für den Monitoring-Stack selbst (Dead-Man's-Switch)
|
||||||
|
- **Beobachtung:** Die Alert-Kette ist Prometheus → Alertmanager → Bridge → ntfy. Fällt ein Glied (oder der ganze Monitoring-Stack) aus, kommen schlicht **keine** Alerts mehr — Stille ist nicht von "alles gut" unterscheidbar. Kein Healthcheck im Monitoring-Compose.
|
||||||
|
- **Warum relevant:** Das Monitoring überwacht alles außer sich selbst.
|
||||||
|
- **Änderung:** Dauerhaft feuernde `Watchdog`-Alert-Rule in `monitoring/prometheus/alerts.yml` + externen Heartbeat-Empfänger: einfachste Variante ist healthchecks.io (free) — Alertmanager-Route schickt den Watchdog alle 5 min an die Heartbeat-URL; bleibt er aus, alarmiert healthchecks.io per Mail/Push von außen.
|
||||||
|
- **Verifikation:** `docker stop monitoring-prometheus` im Wartungsfenster → externe Benachrichtigung nach ~10 min; danach Start.
|
||||||
|
- **Rollback:** Rule + Route entfernen.
|
||||||
|
- **Nebenwirkungen:** neue (kleine) externe Abhängigkeit — in `docs/EXTERNAL_DEPENDENCIES.md` eintragen. | Nutzen: **hoch** | Risiko: niedrig | Aufwand: klein | diese Woche
|
||||||
|
|
||||||
|
### 9. Lokale Arbeitskopie sauber halten (GitOps-Hygiene)
|
||||||
|
- **Beobachtung:** Die lokale Arbeitskopie hat aktuell 6 modifizierte Dateien und 2 untracked Artefakte (u. a. `docs/KalliLab_CORE_Audit_2026-06-06.pdf`, `ops/h-drive-nearline/README.md`), die nicht committed sind. Bei "Gitea = Quelle der Wahrheit" ist eine dauerhaft schmutzige Arbeitskopie ein Drift-Risiko (Änderungen gehen bei Pull-Konflikten verloren oder landen versehentlich in fremden Commits).
|
||||||
|
- **Warum relevant:** Genau die Drift-Klasse, vor der `docs/GITOPS_DRIFT_RUNBOOK.md` warnt — nur auf Ebene 2 (lokaler Clone) statt Ebene 4.
|
||||||
|
- **Änderung:** Modifizierte Doku-Dateien reviewen und committen oder verwerfen; PDF entweder committen (wenn es Referenz ist) oder in `.gitignore`/außerhalb des Repos ablegen; `ops/h-drive-nearline/README.md` committen.
|
||||||
|
- **Verifikation:** `git status` zeigt clean tree (bis auf bewusste Arbeit).
|
||||||
|
- **Rollback:** n/a (Aufräumarbeit). | Nutzen: mittel | Risiko: niedrig | Aufwand: klein (<30 min) | sofort
|
||||||
|
|
||||||
|
### 10. Doku-Drift-Fixes (klein, aber Vertrauensbasis)
|
||||||
|
- **Beobachtung:** `HOMELAB_ARCHITECTURE_MASTER_V2.md` nennt "Redis-Caches auf `redis:7.4-alpine` vereinheitlicht" — real laufen alle auf `redis:8.8.0-alpine`. Ebenso "PostgreSQL 17"-Pfade/Servicenamen bei PG 18 (letzteres ist dokumentiert bewusst, ersteres nicht).
|
||||||
|
- **Warum relevant:** Das Masterdokument ist laut eigener Regel die erste Lesepflicht für jeden (auch KI-)Eingriff; veraltete Fakten dort erzeugen falsche Entscheidungen.
|
||||||
|
- **Änderung:** Redis-Abschnitt in Sektion 13 auf 8.8 aktualisieren; bei Gelegenheit einen Mini-Check ins Posture-/Audit-Ritual: "stimmen Versionsangaben im Master noch?"
|
||||||
|
- **Verifikation:** `grep -n "7.4-alpine" HOMELAB_ARCHITECTURE_MASTER_V2.md` → leer.
|
||||||
|
- **Rollback:** trivial (Doku). | Nutzen: niedrig–mittel | Risiko: keiner | Aufwand: klein | sofort
|
||||||
|
|
||||||
|
## Top 5 Risiken (zuerst entschärfen)
|
||||||
|
|
||||||
|
1. **Löschbare Off-site-Backups** — Host-Kompromittierung oder ein falscher `borg delete` vernichtet auch Hetzner. → Empfehlung 2 (Snapshots). Bis dahin ist das DR-Konzept gegen Ransomware unvollständig.
|
||||||
|
2. **DNS-SPOF AdGuard** — bereits einmal real eingetreten (Teil-Deploy 2026-06); betrifft auch die Selbstheilungsfähigkeit (Image-Pulls). → Empfehlung 3.
|
||||||
|
3. **Verdeckte Versionssprünge via `release`/`latest`-Digest-Bumps** — v. a. Immich (DB-Migrationen!). → Empfehlung 1.
|
||||||
|
4. **OOM-Kaskade ohne Limits** — ein Leck in einer Tier-3-App kann Postgres killen. → Empfehlung 5. (Der Critical-Events-Watcher meldet das nur, verhindert es nicht.)
|
||||||
|
5. **Blinde Alert-Kette** — Monitoring-Ausfall = Stille statt Alarm. → Empfehlung 8.
|
||||||
|
|
||||||
|
Bewusst akzeptierte Risiken (USV geparkt, ein Host, kein WAN-Failover, kein
|
||||||
|
zweites Off-site-Ziel) sind dokumentiert und werden hier nicht erneut
|
||||||
|
aufgemacht — die Entscheidungen sind nachvollziehbar.
|
||||||
|
|
||||||
|
## Quick Wins unter 30 Minuten
|
||||||
|
|
||||||
|
| Quick Win | Wirkung | Kommando/Weg |
|
||||||
|
|---|---|---|
|
||||||
|
| Hetzner-Snapshots aktivieren | Backup-Löschschutz | Robot-Konsole → Storage Box → Snapshots (Empf. 2) |
|
||||||
|
| Host-DNS-Fallback eintragen | Selbstheilung bei AdGuard-Down | Unraid Settings → Network → DNS 2 = `1.1.1.1` (Empf. 3a) |
|
||||||
|
| Arbeitskopie aufräumen | GitOps-Hygiene | `git status`, committen/verwerfen (Empf. 9) |
|
||||||
|
| Redis-Doku-Drift fixen | Master-Doku wieder korrekt | Sektion 13 editieren (Empf. 10) |
|
||||||
|
| Memory-Baseline ziehen | Grundlage für Limits | `docker stats --no-stream` auf dem Host, Output archivieren |
|
||||||
|
| Watchdog-Rule anlegen | Vorbereitung Dead-Man's-Switch | `alerts.yml` + healthchecks.io-Account (Empf. 8) |
|
||||||
|
|
||||||
|
## 30-Tage-Optimierungsplan
|
||||||
|
|
||||||
|
**Woche 1 — Risiko-Entschärfung (alles klein):**
|
||||||
|
Hetzner-Snapshots (Empf. 2) · Host-DNS-Fallback + Stop/Start-Test (Empf. 3a) ·
|
||||||
|
Immich-Tag-Pinning (Empf. 1) · Arbeitskopie aufräumen (Empf. 9) ·
|
||||||
|
Memory-Baseline starten.
|
||||||
|
|
||||||
|
**Woche 2 — Beobachtbarkeit:**
|
||||||
|
Dead-Man's-Switch produktiv (Empf. 8) · traefik/dynamic-Drift-Check in den
|
||||||
|
Posture-Check (Empf. 7) · Healthchecks für Nextcloud, Paperless, Mealie, n8n
|
||||||
|
(Empf. 4, stackweise).
|
||||||
|
|
||||||
|
**Woche 3 — Hardening:**
|
||||||
|
`no-new-privileges` für alle WAN-exponierten Apps (Empf. 6) · Healthchecks
|
||||||
|
für AdGuard/Unbound/Monitoring-Kern · restliche Mutable-Tag-Kandidaten pinnen
|
||||||
|
(komodo, scrutiny, glances, ddns-updater, tag-lose digest-only Images).
|
||||||
|
|
||||||
|
**Woche 4 — Stabilität:**
|
||||||
|
Memory-Limits aus der Baseline für die Top-5-Verbraucher (Empf. 5) ·
|
||||||
|
FRITZ!Box-DNS-Fallback-Entscheidung (Empf. 3b) · Doku nachziehen
|
||||||
|
(Master Sektion 13, SERVICE_CATALOG, dieses Dokument abhaken).
|
||||||
|
|
||||||
|
## Größere Projekte mit hohem Nutzen (später)
|
||||||
|
|
||||||
|
- **End-to-end-DR-Drill** sobald zweite Hardware existiert (bereits geplant,
|
||||||
|
bleibt der wertvollste offene Beweis).
|
||||||
|
- **Strom-/Kostentransparenz:** smarte Steckdose mit Messfunktion (z. B.
|
||||||
|
Shelly Plug S) vor den Unraid-Host, Werte via Home Assistant → InfluxDB 3 →
|
||||||
|
Grafana. Erst messen, dann ggf. optimieren (Spindown-Policy, CPU-Governor).
|
||||||
|
Messbarkeit: W-Dauerlast und kWh/Monat als Grafana-Panel.
|
||||||
|
- **USV-Review Q3/2026** wie geparkt — nach Strommessung lässt sich die
|
||||||
|
USV-Dimensionierung direkt ableiten.
|
||||||
|
- **Renovate-Policy verfeinern:** Digest-PRs für mutable Tags entweder
|
||||||
|
abschalten oder mit Warn-Label versehen, damit Befund 1 strukturell nicht
|
||||||
|
zurückkommt.
|
||||||
|
|
||||||
|
## Konkrete Verifikationskommandos (Sammlung, alle read-only)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Health-Status aller Container
|
||||||
|
docker ps --format '{{.Names}}\t{{.Status}}' | sort
|
||||||
|
|
||||||
|
# Memory-Baseline
|
||||||
|
docker stats --no-stream --format '{{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}' | sort -k3 -hr | head -15
|
||||||
|
|
||||||
|
# Welche Container ohne no-new-privileges laufen
|
||||||
|
docker ps -q | xargs docker inspect --format '{{.Name}} {{.HostConfig.SecurityOpt}}' | grep -v no-new-privileges
|
||||||
|
|
||||||
|
# Effektive Image-Referenzen (mutable Tags erkennen)
|
||||||
|
docker ps --format '{{.Names}}\t{{.Image}}' | grep -E 'latest|release|:2$|:[0-9]+$'
|
||||||
|
|
||||||
|
# DNS-Fallback-Test (Wartungsfenster!)
|
||||||
|
docker stop adguard && nslookup gitea.com && docker start adguard
|
||||||
|
|
||||||
|
# Borg-Snapshot-Gegenprobe (nach Aktivierung, von der Storage Box)
|
||||||
|
ssh -p 23 u565255@u565255.your-storagebox.de ls .snapshots/ 2>/dev/null || echo "via Robot-Konsole prüfen"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rollback-Hinweise (generell)
|
||||||
|
|
||||||
|
- Jede Compose-Änderung: Revert-Commit nach Gitea pushen → Komodo deployed
|
||||||
|
den Vorzustand; Datenpfade bleiben unberührt (alle Empfehlungen hier sind
|
||||||
|
config-only, keine Daten-/Volume-Migrationen).
|
||||||
|
- Healthchecks/Limits/security_opt: Zeilen entfernen + Redeploy genügt.
|
||||||
|
- Host-DNS/FRITZ!Box-Einträge: Eintrag löschen, sofort wirksam.
|
||||||
|
- Hetzner-Snapshots und Dead-Man's-Switch sind rein additiv.
|
||||||
|
- Nichts in diesem Dokument erfordert `push --force`, History-Rewrite oder
|
||||||
|
Löschoperationen auf Datenpfaden.
|
||||||
|
|
||||||
|
## Offene Fragen an den Operator
|
||||||
|
|
||||||
|
1. **Strom:** Gibt es eine Messung des Host-Verbrauchs (W idle/last)? Ohne
|
||||||
|
Zahl ist der Bereich Kosten/Strom nicht bewertbar. → Shelly/Messsteckdose?
|
||||||
|
2. **RAM-Ausstattung des Hosts:** Wie viel RAM hat Kallilabcore gesamt und
|
||||||
|
wie ist die aktuelle Auslastung (`free -h`)? Bestimmt, wie aggressiv
|
||||||
|
Memory-Limits sinnvoll sind.
|
||||||
|
3. **Renovate-Verhalten gewollt?** Sollen Digest-Bumps auf `release`/`latest`
|
||||||
|
weiter automatisch als PRs kommen, oder ist die Pinning-Strategie aus
|
||||||
|
Empfehlung 1 die gewünschte Linie für alle Stacks?
|
||||||
|
4. **healthchecks.io o. ä. als externe Abhängigkeit akzeptabel?** Alternativ
|
||||||
|
ginge ein ntfy-basierter Heartbeat von einem zweiten Gerät (z. B. dem
|
||||||
|
Gaming-PC per Scheduled Task) — null neue Cloud-Abhängigkeit.
|
||||||
|
5. **FRITZ!Box-DNS-Fallback (3b):** Filterlücke bei AdGuard-Down akzeptieren
|
||||||
|
oder lieber nur den Host-Fallback (3a) umsetzen?
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Runbook: Komodo Bulk-Deploy schlaegt mit DNS `connection refused` fehl
|
||||||
|
|
||||||
|
Stand: 2026-06-10 · Typ: Runbook / ADR-light · Status: Sofortmassnahme empfohlen, noch nicht umgesetzt
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Ein Bulk-Merge (z. B. Renovate-Sammel-PR) loest gleichzeitig viele Komodo-Stack-Webhooks aus. Komodo startet parallele `DeployStack`. Nur ein Teil der Stacks deployt, der Rest bleibt auf dem alten Image. In der Deploy-Stufe **Compose Pull** stehen Fehler wie:
|
||||||
|
|
||||||
|
```
|
||||||
|
Get "https://registry-1.docker.io/v2/": dial tcp: lookup registry-1.docker.io
|
||||||
|
on 192.168.178.58:53: read udp ...->192.168.178.58:53: read: connection refused
|
||||||
|
```
|
||||||
|
|
||||||
|
Manuelles Re-Deploy der betroffenen Stacks danach funktioniert (AdGuard ist dann wieder oben).
|
||||||
|
|
||||||
|
## Ursache
|
||||||
|
|
||||||
|
Der Host nutzt **AdGuard Home als einzigen Resolver** (`/etc/resolv.conf` = nur `nameserver 192.168.178.58`, keine `/etc/docker/daemon.json`). AdGuard laeuft selbst als Container auf dem Host und bindet `0.0.0.0:53`. Wird der `adguard`-Stack im selben Batch neu deployt, faellt Port 53 fuer Sekunden aus. Alle parallelen `docker compose pull` der anderen Stacks koennen `registry-1.docker.io` dann nicht aufloesen -> `connection refused` -> Deploy `success=false`.
|
||||||
|
|
||||||
|
Es ist **kein** Webhook-, Auth- oder Docker-Hub-Rate-Limit-Problem: Webhooks authentifizieren sauber, `webhook_enabled=true`, Fehlerbild ist `connection refused` auf den eigenen DNS-Port direkt nach AdGuard-Recreate. Fuer den Pull-Pfad zaehlt der Docker-Daemon/Go-Resolver (iteriert ueber die `resolv.conf`-Server und springt bei Socket-Fehlern zum naechsten), nicht der glibc-Client.
|
||||||
|
|
||||||
|
## Sofortmassnahme (Schicht 1)
|
||||||
|
|
||||||
|
Unraid -> Settings -> Network Settings -> `eth0`:
|
||||||
|
|
||||||
|
- DNS server 1: `192.168.178.58` (AdGuard, bleibt)
|
||||||
|
- **DNS server 2: `192.168.178.1`** (FritzBox) -> Apply
|
||||||
|
|
||||||
|
Damit ueberleben Registry-Pulls einen kurzen AdGuard-Ausfall via Resolver-Failover. Im Normalbetrieb wird weiter DNS1 (AdGuard) genutzt, der Filter bleibt aktiv.
|
||||||
|
|
||||||
|
Pruefen / Bedingungen:
|
||||||
|
|
||||||
|
- **Kein `options rotate`** in `/etc/resolv.conf` (sonst dauerhafter Filter-Bypass). Aktuell nicht gesetzt; nach Apply erneut pruefen.
|
||||||
|
- Router muss oeffentliche Namen **selbst** aufloesen und nicht intern an AdGuard zurueckleiten.
|
||||||
|
- Hinweis zur Verifikation: Ein `nslookup registry-1.docker.io 192.168.178.1` bei laufendem AdGuard ist ein gutes Signal, aber **kein letzter Beweis**. Wasserdicht: AdGuard kurz stoppen und `dig @192.168.178.1 registry-1.docker.io`, oder FritzBox-Upstream / AdGuard-Querylog pruefen.
|
||||||
|
|
||||||
|
Rollback: DNS server 2 leeren + Apply.
|
||||||
|
|
||||||
|
## Betriebsregel (Schicht 2)
|
||||||
|
|
||||||
|
- **AdGuard und Unbound nicht gemeinsam mit abhaengigen Stacks im Bulk deployen.** DNS-Infrastruktur immer separat / einzeln deployen, nicht waehrend 20+ parallele Pulls laufen.
|
||||||
|
- Renovate-PRs gestaffelt mergen (eine Etappe pro Deploy) statt Sammel-Merge. Deckt dieses Problem fuer den Normalbetrieb bereits ab.
|
||||||
|
|
||||||
|
## Spaeter optional
|
||||||
|
|
||||||
|
- Komodo-Deploys serialisieren: statt vieler paralleler Stack-Webhooks eine **Procedure** (sequenzielle Stages) oder **Resource Sync** mit `after`-Ordering. Trifft die Ursache direkter, ist aber ein groesserer Umbau und **kein Renovate-Blocker**.
|
||||||
|
- Host-DNS vom AdGuard-Container entkoppeln (AdGuard eigene IP via macvlan, Host-Resolver auf Router/Unbound), damit `:53` am Host nicht exklusiv am Container-Lifecycle haengt.
|
||||||
|
|
||||||
|
## Verworfen
|
||||||
|
|
||||||
|
- `/etc/docker/daemon.json` mit `"dns": [...]`: wirkt nur fuer Container-DNS, nicht fuer Daemon-eigene Image-Pulls.
|
||||||
|
- AdGuard `network_mode: host`: beim Recreate ist der DNS-Prozess trotzdem weg; macht aus dem Single Point of Failure keinen HA-Resolver.
|
||||||
|
|
||||||
|
## Referenzen
|
||||||
|
|
||||||
|
- Diagnose-Zugriff: SSH `root@192.168.178.58`; Komodo-Mongo (`docker exec komodo-mongo`, DB `komodo`, Collections `Stack`/`Update`); Gitea SQLite `/data/gitea/gitea.db` (Tabelle `webhook`, `repo_id=3`).
|
||||||
|
- Verwandt: `docs/WORKFLOW.md` (DNS-Regeln fuer Container), `docs/GITOPS_DRIFT_RUNBOOK.md`.
|
||||||
|
</content>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
adguard:
|
adguard:
|
||||||
image: adguard/adguardhome:v0.107.76@sha256:7157eb1dc3b26c7af1d6898759a7b3f7d0fa09891fbd2d3caa6abc1057a9179b
|
image: adguard/adguardhome:v0.107.77@sha256:e6f2b8bcda06064ab055b44933a4f0e983c35558b9cdb8d2e7ab1efcee36d890
|
||||||
container_name: adguard
|
container_name: adguard
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
services:
|
|
||||||
tailscale:
|
|
||||||
image: tailscale/tailscale:stable@sha256:25cde9ad76020b0e29229136d0c38b5962e9a0e1774ffac9b0df68e4a37d6cf0
|
|
||||||
container_name: Tailscale-Docker
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
- NET_RAW
|
|
||||||
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
|
|
||||||
devices:
|
|
||||||
- /dev/net/tun:/dev/net/tun
|
|
||||||
|
|
||||||
environment:
|
|
||||||
- TZ=Europe/Berlin
|
|
||||||
- TS_HOSTNAME=kallilab-core
|
|
||||||
- TS_STATE_DIR=/state
|
|
||||||
- TS_AUTH_ONCE=true
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- /mnt/user/appdata/tailscale:/state
|
|
||||||
@@ -25,7 +25,7 @@ services:
|
|||||||
- cadvisor
|
- cadvisor
|
||||||
|
|
||||||
alertmanager:
|
alertmanager:
|
||||||
image: prom/alertmanager:v0.32.1@sha256:51a825c2a40acc3e338fdd00d622e01ec090f72be2b3ea46be0839cd47a4d286
|
image: prom/alertmanager:v0.32.2@sha256:b85533a2eb45865835315810315f6951331b2dbc8c93a6cf9a51e156a006a706
|
||||||
container_name: monitoring-alertmanager
|
container_name: monitoring-alertmanager
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command:
|
command:
|
||||||
@@ -66,15 +66,18 @@ services:
|
|||||||
image: prom/blackbox-exporter:v0.28.0@sha256:e753ff9f3fc458d02cca5eddab5a77e1c175eee484a8925ac7d524f04366c2fc
|
image: prom/blackbox-exporter:v0.28.0@sha256:e753ff9f3fc458d02cca5eddab5a77e1c175eee484a8925ac7d524f04366c2fc
|
||||||
container_name: monitoring-blackbox-exporter
|
container_name: monitoring-blackbox-exporter
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# Use AdGuard so *.kaleschke.info resolves to the internal Traefik IP.
|
||||||
|
# External resolvers (1.1.1.1/8.8.8.8) return the public WAN IP, which
|
||||||
|
# causes hairpin-NAT timeouts when probing from inside the Docker network.
|
||||||
dns:
|
dns:
|
||||||
- 1.1.1.1
|
- 172.23.0.3
|
||||||
- 8.8.8.8
|
|
||||||
command:
|
command:
|
||||||
- --config.file=/etc/blackbox_exporter/blackbox.yml
|
- --config.file=/etc/blackbox_exporter/blackbox.yml
|
||||||
volumes:
|
volumes:
|
||||||
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro
|
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro
|
||||||
networks:
|
networks:
|
||||||
- monitoring_net
|
- monitoring_net
|
||||||
|
- dns_net
|
||||||
expose:
|
expose:
|
||||||
- "9115"
|
- "9115"
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -115,7 +118,7 @@ services:
|
|||||||
- loki
|
- loki
|
||||||
|
|
||||||
grafana:
|
grafana:
|
||||||
image: grafana/grafana:13.0.1@sha256:0f86bada30d65ef9d0183b90c1e2682ac92d53d95da8bed322b984ea78a4a73a
|
image: grafana/grafana:13.0.2@sha256:5dad0df181cb644a14e13617b913b261a54f7d4fd4510721dba420929f35bea2
|
||||||
container_name: monitoring-grafana
|
container_name: monitoring-grafana
|
||||||
user: "0"
|
user: "0"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -129,6 +132,20 @@ services:
|
|||||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
GF_USERS_ALLOW_SIGN_UP: "false"
|
||||||
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
||||||
GF_PLUGINS_PREINSTALL_DISABLED: "true"
|
GF_PLUGINS_PREINSTALL_DISABLED: "true"
|
||||||
|
# --- Authelia OIDC SSO (2026-06-06) ---
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
|
||||||
|
GF_AUTH_GENERIC_OAUTH_NAME: Authelia
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_ID: grafana
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET__FILE: /run/secrets/grafana_oidc_client_secret
|
||||||
|
GF_AUTH_GENERIC_OAUTH_SCOPES: "openid profile email groups"
|
||||||
|
GF_AUTH_GENERIC_OAUTH_AUTH_URL: https://auth.kaleschke.info/api/oidc/authorization
|
||||||
|
GF_AUTH_GENERIC_OAUTH_TOKEN_URL: https://auth.kaleschke.info/api/oidc/token
|
||||||
|
GF_AUTH_GENERIC_OAUTH_API_URL: https://auth.kaleschke.info/api/oidc/userinfo
|
||||||
|
GF_AUTH_GENERIC_OAUTH_USE_PKCE: "true"
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP: "true"
|
||||||
|
# Proof: alle OIDC-Logins als Admin; spaeter ueber groups verfeinern
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: "'Admin'"
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT: "false"
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
- -c
|
- -c
|
||||||
@@ -145,6 +162,7 @@ services:
|
|||||||
secrets:
|
secrets:
|
||||||
- monitoring_grafana_admin_password
|
- monitoring_grafana_admin_password
|
||||||
- monitoring_grafana_influxdb_token
|
- monitoring_grafana_influxdb_token
|
||||||
|
- grafana_oidc_client_secret
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -160,7 +178,8 @@ services:
|
|||||||
- traefik.http.routers.monitoring-grafana.entrypoints=websecure
|
- traefik.http.routers.monitoring-grafana.entrypoints=websecure
|
||||||
- traefik.http.routers.monitoring-grafana.tls=true
|
- traefik.http.routers.monitoring-grafana.tls=true
|
||||||
- traefik.http.routers.monitoring-grafana.tls.certresolver=le
|
- traefik.http.routers.monitoring-grafana.tls.certresolver=le
|
||||||
- traefik.http.routers.monitoring-grafana.middlewares=authelia@file,secure-headers@file
|
# ForwardAuth bewusst entfernt 2026-06-06: Grafana macht jetzt eigenes OIDC-SSO gegen Authelia
|
||||||
|
- traefik.http.routers.monitoring-grafana.middlewares=secure-headers@file
|
||||||
- traefik.http.services.monitoring-grafana.loadbalancer.server.port=3000
|
- traefik.http.services.monitoring-grafana.loadbalancer.server.port=3000
|
||||||
|
|
||||||
grafana-dashboard-importer:
|
grafana-dashboard-importer:
|
||||||
@@ -318,7 +337,7 @@ services:
|
|||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
influxdb3-core:
|
influxdb3-core:
|
||||||
image: influxdb:3.9.2-core@sha256:31ad94df2248134989b2cf73d965e51dd5f35dfae22d7ed8f4776b12e6f69f4e
|
image: influxdb:3.9.3-core@sha256:c27c9b2ca2625b5b6966f0b09baa448102310e63a471fd60dff22646a2522e29
|
||||||
container_name: monitoring-influxdb3-core
|
container_name: monitoring-influxdb3-core
|
||||||
user: "0"
|
user: "0"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -351,6 +370,8 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
frontend_net:
|
frontend_net:
|
||||||
external: true
|
external: true
|
||||||
|
dns_net:
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
prometheus_data:
|
prometheus_data:
|
||||||
@@ -364,5 +385,7 @@ secrets:
|
|||||||
file: /mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt
|
file: /mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt
|
||||||
monitoring_grafana_influxdb_token:
|
monitoring_grafana_influxdb_token:
|
||||||
file: /mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt
|
file: /mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt
|
||||||
|
grafana_oidc_client_secret:
|
||||||
|
file: /mnt/user/appdata/secrets/grafana_oidc_client_secret
|
||||||
influxdb3_admin_token:
|
influxdb3_admin_token:
|
||||||
file: /mnt/user/appdata/secrets/influxdb3_admin_token.json
|
file: /mnt/user/appdata/secrets/influxdb3_admin_token.json
|
||||||
|
|||||||
@@ -90,18 +90,16 @@ The live Unraid User Scripts execute repo scripts from `/mnt/user/services/homel
|
|||||||
|
|
||||||
## Explicitly Not Backed Up as Raw Live DB Files
|
## Explicitly Not Backed Up as Raw Live DB Files
|
||||||
|
|
||||||
- `/mnt/user/appdata/postgresql17`
|
|
||||||
- `/mnt/user/appdata/postgresql18`
|
- `/mnt/user/appdata/postgresql18`
|
||||||
- `/mnt/user/appdata/mealie/postgres`
|
|
||||||
- `/mnt/user/appdata/mealie/postgres18`
|
- `/mnt/user/appdata/mealie/postgres18`
|
||||||
- `/mnt/user/appdata/immich_postgres`
|
|
||||||
- `/mnt/user/appdata/immich_postgres_vectorchord`
|
- `/mnt/user/appdata/immich_postgres_vectorchord`
|
||||||
- `/mnt/user/appdata/nextcloud/postgres`
|
|
||||||
- `/mnt/user/appdata/nextcloud/postgres18`
|
- `/mnt/user/appdata/nextcloud/postgres18`
|
||||||
- `/mnt/user/appdata/komodo/mongo`
|
- `/mnt/user/appdata/komodo/mongo`
|
||||||
- `/mnt/user/appdata/redis`
|
- `/mnt/user/appdata/redis`
|
||||||
- `/mnt/user/appdata/scrutiny/influxdb`
|
- `/mnt/user/appdata/scrutiny/influxdb`
|
||||||
|
|
||||||
|
Archived PG18/VectorChord rollback volumes under `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602` are retained only as temporary rollback material, not as primary backup truth.
|
||||||
|
|
||||||
## Low-Priority / Rebuildable
|
## Low-Priority / Rebuildable
|
||||||
|
|
||||||
These are not part of the first-class Borg scope:
|
These are not part of the first-class Borg scope:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
borg-ui:
|
borg-ui:
|
||||||
image: ainullcode/borg-ui@sha256:b44c0a92b650d80f215a986dadda5c2604c61eb28a7571e19c046eff41d761e7
|
image: ainullcode/borg-ui@sha256:0922157e8f77a1b2bd23cd09366a458ea6de07fd9306aa1485f9cfe623eca17f
|
||||||
container_name: borg-ui
|
container_name: borg-ui
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
code-server:
|
code-server:
|
||||||
image: lscr.io/linuxserver/code-server:4.122.0@sha256:0caf1b65ebec84b94397108b56da6c33f124c5390f5832da94e75f4609c0e2ad
|
image: lscr.io/linuxserver/code-server:4.123.0@sha256:cb261a7f87674b445e0fd66d87d55900c1b823d276c727ab0d168a75e69e9992
|
||||||
container_name: code-server
|
container_name: code-server
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:v2.63.5@sha256:aefb0c20de10ef8b617995ca5522479ad40d41e6386bd01946a345c6026ff31c
|
image: filebrowser/filebrowser:v2.63.14@sha256:1ec9b0c68297550c92f4a93feed432850c2993b261706cc3cc2e808f94a95e76
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -497,12 +497,6 @@ pages:
|
|||||||
description: Upstream Resolver
|
description: Upstream Resolver
|
||||||
category: network
|
category: network
|
||||||
hide: false
|
hide: false
|
||||||
Tailscale-Docker:
|
|
||||||
name: Tailscale
|
|
||||||
icon: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/tailscale.svg
|
|
||||||
description: VPN
|
|
||||||
category: network
|
|
||||||
hide: false
|
|
||||||
ddns-updater:
|
ddns-updater:
|
||||||
name: DDNS Updater
|
name: DDNS Updater
|
||||||
icon: mdi:cloud-sync
|
icon: mdi:cloud-sync
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM nousresearch/hermes-agent:v2026.5.29
|
FROM nousresearch/hermes-agent:v2026.6.5
|
||||||
|
|
||||||
USER root
|
USER root
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@
|
|||||||
"dump_file": null,
|
"dump_file": null,
|
||||||
"data_paths": ["/mnt/user/appdata/postgresql18"],
|
"data_paths": ["/mnt/user/appdata/postgresql18"],
|
||||||
"first_check": "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?",
|
"first_check": "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?",
|
||||||
"notes": "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad bleibt nur Rollback-Altstand"
|
"notes": "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17 archiviert"
|
||||||
},
|
},
|
||||||
"komodo-core": {
|
"komodo-core": {
|
||||||
"description": "GitOps UI / API / Stack-Manager",
|
"description": "GitOps UI / API / Stack-Manager",
|
||||||
@@ -202,7 +202,7 @@
|
|||||||
"dump_file": "immich.dump",
|
"dump_file": "immich.dump",
|
||||||
"data_paths": ["/mnt/user/appdata/immich_postgres_vectorchord"],
|
"data_paths": ["/mnt/user/appdata/immich_postgres_vectorchord"],
|
||||||
"first_check": "immich_default Netz? Disk-Space? pg_isready?",
|
"first_check": "immich_default Netz? Disk-Space? pg_isready?",
|
||||||
"notes": "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad bleibt nur Rollback-Altstand"
|
"notes": "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs archiviert"
|
||||||
},
|
},
|
||||||
"immich_redis": {
|
"immich_redis": {
|
||||||
"description": "Immich Cache",
|
"description": "Immich Cache",
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ services:
|
|||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/postgresql18
|
- /mnt/user/appdata/postgresql18
|
||||||
first_check: "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?"
|
first_check: "backend_net Konnektivitaet? Disk-Space auf /mnt/user/appdata? pg_isready im Container?"
|
||||||
notes: "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad bleibt nur Rollback-Altstand"
|
notes: "Dumps per Dienst unter dumps/latest; raw DB nicht primaerer Restore-Weg; alter PG17-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/postgresql17 archiviert"
|
||||||
|
|
||||||
komodo-core:
|
komodo-core:
|
||||||
description: GitOps UI / API / Stack-Manager
|
description: GitOps UI / API / Stack-Manager
|
||||||
@@ -231,8 +231,8 @@ services:
|
|||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/paperless-gpt/data
|
- /mnt/user/appdata/paperless-gpt/data
|
||||||
- /mnt/user/appdata/paperless-gpt/prompts
|
- /mnt/user/appdata/paperless-gpt/prompts
|
||||||
first_check: "Paperless API erreichbar? LLM/Ollama erreichbar? API Token gesetzt?"
|
first_check: "Paperless API erreichbar? OpenAI API Key gesetzt? Provider/Model auf openai/gpt-5.4-mini?"
|
||||||
notes: "API Token als Stack ENV; abhaengig von laufendem Paperless"
|
notes: "PAPERLESS_API_TOKEN und OPENAI_API_KEY als Stack ENV; kein lokaler Ollama-Zugriff"
|
||||||
|
|
||||||
immich_server:
|
immich_server:
|
||||||
description: Foto-/Video-App
|
description: Foto-/Video-App
|
||||||
@@ -263,7 +263,7 @@ services:
|
|||||||
data_paths:
|
data_paths:
|
||||||
- /mnt/user/appdata/immich_postgres_vectorchord
|
- /mnt/user/appdata/immich_postgres_vectorchord
|
||||||
first_check: "immich_default Netz? Disk-Space? pg_isready?"
|
first_check: "immich_default Netz? Disk-Space? pg_isready?"
|
||||||
notes: "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad bleibt nur Rollback-Altstand"
|
notes: "PG14 mit VectorChord/pgvector; nie ins frontend_net; immich_default Netz isoliert; alter immich_postgres-Pfad ist unter /mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/immich-postgres-pgvecto-rs archiviert"
|
||||||
|
|
||||||
immich_redis:
|
immich_redis:
|
||||||
description: Immich Cache
|
description: Immich Cache
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ services:
|
|||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
# MongoDB – Datenbank fuer Komodo Core
|
# MongoDB – Datenbank fuer Komodo Core
|
||||||
# Netz: komodo_net (internal: true) – niemals frontend_net
|
# Netz: komodo_net (internal: true) – niemals frontend_net
|
||||||
|
# ACHTUNG: Dieser Stack wird NICHT aus diesem Repo deployed. Der komodo-Stack
|
||||||
|
# ist in Komodo inline (file_contents) verwaltet (Bootstrap-/Self-Stack).
|
||||||
|
# Diese Datei ist nur Doku/Spiegel; Aenderungen hier wirken NICHT zur Laufzeit.
|
||||||
|
# ops/komodo/** ist in renovate.json ignorePaths. Siehe docs/RENOVATE.md.
|
||||||
|
# Digest = aktuell real laufender Stand (kein Renovate-Auto-Update).
|
||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
komodo-mongo:
|
komodo-mongo:
|
||||||
image: mongo:8.0.23@sha256:44aa79ae28ff80b56fe58681b66cda9336706df408a5175a6c04988aa54610d3
|
image: mongo:8.0.23@sha256:44aa79ae28ff80b56fe58681b66cda9336706df408a5175a6c04988aa54610d3
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
param(
|
||||||
|
[string]$ReportPath = "G:\Gitea_Clone\homelab-infra\docs\audit\dr-workstation-readiness-2026-06-06.md"
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Invoke-Capture {
|
||||||
|
param([string]$Command)
|
||||||
|
|
||||||
|
$output = & cmd.exe /c $Command 2>&1
|
||||||
|
[pscustomobject]@{
|
||||||
|
Command = $Command
|
||||||
|
ExitCode = $LASTEXITCODE
|
||||||
|
Output = ($output | ForEach-Object { ([string]$_).Replace("`0", "") })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-WslCapture {
|
||||||
|
param([string]$Bash)
|
||||||
|
Invoke-Capture -Command ('wsl -d Ubuntu -- bash -lc ' + '"' + ($Bash.Replace('"', '\"')) + '"')
|
||||||
|
}
|
||||||
|
|
||||||
|
$checks = [ordered]@{}
|
||||||
|
$checks["wsl_status"] = Invoke-Capture -Command "wsl --status"
|
||||||
|
$checks["wsl_list"] = Invoke-Capture -Command "wsl --list --verbose"
|
||||||
|
$checks["ubuntu_os"] = Invoke-WslCapture -Bash "lsb_release -a 2>/dev/null || cat /etc/os-release; uname -r"
|
||||||
|
$checks["tools"] = Invoke-WslCapture -Bash "command -v borg || true; borg --version 2>/dev/null || true; command -v ssh || true; ssh -V 2>&1 || true; command -v git || true; git --version 2>/dev/null || true"
|
||||||
|
$checks["sudo"] = Invoke-WslCapture -Bash "sudo -n true >/dev/null 2>&1 && echo sudo-noprompt-ok || echo sudo-password-needed"
|
||||||
|
$checks["wsl_ssh_files"] = Invoke-WslCapture -Bash "ls -la ~/.ssh 2>/dev/null || true; test -f ~/dr-smoke.sh && ls -la ~/dr-smoke.sh || true"
|
||||||
|
$checks["github_dr_key_smoke"] = Invoke-WslCapture -Bash "GIT_SSH_COMMAND='ssh -i ~/.ssh/dr-readonly -o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=8' git ls-remote git@github.com:michaelkaleschke-spec/homelab-infra.git HEAD 2>&1 | sed -n '1,5p'"
|
||||||
|
$checks["hetzner_dr_key_smoke"] = Invoke-WslCapture -Bash "ssh -i ~/.ssh/dr-hetzner -o BatchMode=yes -o IdentitiesOnly=yes -o ConnectTimeout=8 -p 23 u565255@u565255.your-storagebox.de 'ls' 2>&1 | sed -n '1,10p'"
|
||||||
|
|
||||||
|
$borgInstalled = ($checks["tools"].Output -match "borg \d")
|
||||||
|
$githubOk = ($checks["github_dr_key_smoke"].Output -match "HEAD")
|
||||||
|
$hetznerOk = ($checks["hetzner_dr_key_smoke"].Output -match "hetzner_borg_appdata_critical")
|
||||||
|
$sudoNeedsPassword = ($checks["sudo"].Output -match "sudo-password-needed")
|
||||||
|
$drSmokeExists = ($checks["wsl_ssh_files"].Output -match "dr-smoke.sh")
|
||||||
|
|
||||||
|
$lines = @()
|
||||||
|
$lines += "# DR-Workstation Readiness - 2026-06-06"
|
||||||
|
$lines += ""
|
||||||
|
$lines += "Automatisch erzeugter lokaler Readiness-Check fuer den Operator-PC. Es wurden keine Secret-Werte, Passphrases oder Private-Key-Inhalte ausgegeben."
|
||||||
|
$lines += ""
|
||||||
|
$lines += "## Zusammenfassung"
|
||||||
|
$lines += ""
|
||||||
|
$lines += "| Check | Ergebnis |"
|
||||||
|
$lines += "|---|---|"
|
||||||
|
$lines += '| WSL2 Ubuntu | vorhanden (`Ubuntu 24.04`, WSL Version 2) |'
|
||||||
|
$lines += "| SSH/Git in WSL | vorhanden |"
|
||||||
|
$lines += "| GitHub-Read-Smoke mit DR-Key | " + ($(if ($githubOk) { "ok" } else { "nicht ok" })) + " |"
|
||||||
|
$lines += "| Borg Client | " + ($(if ($borgInstalled) { "installiert" } else { "fehlt" })) + " |"
|
||||||
|
$lines += "| Hetzner Storage Box mit DR-Key | " + ($(if ($hetznerOk) { "ok" } else { "nicht ok" })) + " |"
|
||||||
|
$lines += '| `~/dr-smoke.sh` | ' + ($(if ($drSmokeExists) { "vorhanden" } else { "fehlt" })) + ' |'
|
||||||
|
$lines += "| WSL sudo ohne Passwortprompt | " + ($(if ($sudoNeedsPassword) { "nein, Operator muss Passwort eingeben" } else { "ja" })) + " |"
|
||||||
|
$lines += ""
|
||||||
|
$lines += "## Bewertung"
|
||||||
|
$lines += ""
|
||||||
|
$lines += "- Der lokale WSL2-/Ubuntu-Unterbau ist vorhanden."
|
||||||
|
$lines += '- Die DR-Key-Arbeitskopien liegen in WSL unter `~/.ssh/dr-readonly` und `~/.ssh/dr-hetzner`.'
|
||||||
|
$lines += "- GitHub-Read-Smoke und Hetzner-SSH-Smoke sind erfolgreich."
|
||||||
|
$lines += '- `borgbackup` ist installiert.'
|
||||||
|
$lines += "- Der vollstaendige Bare-Metal-DR-Smoke wartet nur noch auf die interaktive Borg-Passphrase."
|
||||||
|
$lines += ""
|
||||||
|
$lines += "## Naechste Operator-Schritte"
|
||||||
|
$lines += ""
|
||||||
|
$lines += "In Ubuntu ausfuehren:"
|
||||||
|
$lines += ""
|
||||||
|
$lines += '```bash'
|
||||||
|
$lines += "bash ~/dr-smoke.sh"
|
||||||
|
$lines += '```'
|
||||||
|
$lines += ""
|
||||||
|
$lines += 'Borg fragt dabei interaktiv nach der Borg-Repo-Passphrase. Diese Passphrase wurde nicht auf `baerchen` gefunden und wird bewusst nicht dauerhaft gespeichert.'
|
||||||
|
$lines += ""
|
||||||
|
$lines += "## Rohchecks"
|
||||||
|
$lines += ""
|
||||||
|
foreach ($name in $checks.Keys) {
|
||||||
|
$check = $checks[$name]
|
||||||
|
$lines += "### $name"
|
||||||
|
$lines += ""
|
||||||
|
$lines += '- ExitCode: `' + $check.ExitCode + '`'
|
||||||
|
$lines += ""
|
||||||
|
$lines += '```text'
|
||||||
|
$lines += ($check.Output | ForEach-Object {
|
||||||
|
$_ -replace ([regex]::Escape($env:USERPROFILE)), '%USERPROFILE%'
|
||||||
|
})
|
||||||
|
$lines += '```'
|
||||||
|
$lines += ""
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $ReportPath) | Out-Null
|
||||||
|
while ($lines.Count -gt 0 -and $lines[-1] -eq "") {
|
||||||
|
$lines = $lines[0..($lines.Count - 2)]
|
||||||
|
}
|
||||||
|
$lines -join "`r`n" | Set-Content -LiteralPath $ReportPath -Encoding UTF8
|
||||||
|
Write-Host "Report written: $ReportPath"
|
||||||
Executable
+106
@@ -0,0 +1,106 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOST_IP="${HOST_IP:-192.168.178.58}"
|
||||||
|
FRITZBOX_URL="${FRITZBOX_URL:-http://192.168.178.1:49000/tr64desc.xml}"
|
||||||
|
BORG_DB="${BORG_DB:-/mnt/user/appdata/borg-ui/data/borg.db}"
|
||||||
|
REPO_ROOT="${REPO_ROOT:-/mnt/user/services/homelab-infra}"
|
||||||
|
|
||||||
|
section() {
|
||||||
|
printf '\n## %s\n\n' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
section "FRITZBox"
|
||||||
|
if fritz_xml="$(curl -fsS --max-time 5 "$FRITZBOX_URL")"; then
|
||||||
|
printf '%s\n' "$fritz_xml" | grep -E '<friendlyName>|<modelName>|<Display>' | sed -E 's/^[[:space:]]+//'
|
||||||
|
else
|
||||||
|
echo "FRITZBox TR-064 descriptor not reachable at $FRITZBOX_URL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ipv6_fw="$(
|
||||||
|
curl -fsS --max-time 5 \
|
||||||
|
-H 'Content-Type: text/xml; charset="utf-8"' \
|
||||||
|
-H 'SOAPACTION: "urn:schemas-upnp-org:service:WANIPv6FirewallControl:1#GetFirewallStatus"' \
|
||||||
|
--data '<?xml version="1.0"?><s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"><s:Body><u:GetFirewallStatus xmlns:u="urn:schemas-upnp-org:service:WANIPv6FirewallControl:1" /></s:Body></s:Envelope>' \
|
||||||
|
http://192.168.178.1:49000/igd2upnp/control/WANIPv6Firewall1
|
||||||
|
)"; then
|
||||||
|
firewall_enabled="$(printf '%s\n' "$ipv6_fw" | sed -n 's:.*<FirewallEnabled>\(.*\)</FirewallEnabled>.*:\1:p')"
|
||||||
|
pinhole_allowed="$(printf '%s\n' "$ipv6_fw" | sed -n 's:.*<InboundPinholeAllowed>\(.*\)</InboundPinholeAllowed>.*:\1:p')"
|
||||||
|
echo "IPv6 FirewallEnabled: ${firewall_enabled:-unknown}"
|
||||||
|
echo "IPv6 InboundPinholeAllowed: ${pinhole_allowed:-unknown}"
|
||||||
|
else
|
||||||
|
echo "FRITZBox IPv6 firewall status not reachable via TR-064."
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Host IPv6"
|
||||||
|
global_ipv6="$(
|
||||||
|
ip -6 addr show scope global \
|
||||||
|
| awk '/inet6 / {print $2}' \
|
||||||
|
| grep -Ev '^(fd|fe80:)' || true
|
||||||
|
)"
|
||||||
|
if [[ -n "$global_ipv6" ]]; then
|
||||||
|
echo "Provider/global IPv6 addresses present:"
|
||||||
|
printf '%s\n' "$global_ipv6"
|
||||||
|
else
|
||||||
|
echo "No provider/global IPv6 address on host; only ULA/link-local/Tailscale may be present."
|
||||||
|
fi
|
||||||
|
tailscale ip -4 2>/dev/null | sed 's/^/Tailscale IPv4: /' || true
|
||||||
|
tailscale ip -6 2>/dev/null | sed 's/^/Tailscale IPv6: /' || true
|
||||||
|
|
||||||
|
section "DNS"
|
||||||
|
for name in \
|
||||||
|
kaleschke.info \
|
||||||
|
vault.kaleschke.info \
|
||||||
|
git.kaleschke.info \
|
||||||
|
cloud.kaleschke.info \
|
||||||
|
traefik.kaleschke.info; do
|
||||||
|
echo "$name"
|
||||||
|
dig +short @1.1.1.1 "$name" A | sed 's/^/ public A /' || true
|
||||||
|
dig +short @1.1.1.1 "$name" AAAA | sed 's/^/ public AAAA /' || true
|
||||||
|
done
|
||||||
|
|
||||||
|
section "Host Listeners"
|
||||||
|
ss -ltnp | awk '
|
||||||
|
/:(80|443|222|53|8082|8181)[[:space:]]/ ||
|
||||||
|
/100\.80\.98\.33:8082/ ||
|
||||||
|
/127\.0\.0\.1:8181/ {print}
|
||||||
|
' | sort -k4
|
||||||
|
|
||||||
|
section "External Port Smoke"
|
||||||
|
public_ipv4="$(curl -4 -fsS --max-time 5 https://ifconfig.co 2>/dev/null || true)"
|
||||||
|
if [[ -z "$public_ipv4" ]]; then
|
||||||
|
public_ipv4="$(dig +short @1.1.1.1 kaleschke.info A | tail -n 1)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for check in \
|
||||||
|
"$public_ipv4 443 expected-open" \
|
||||||
|
"$public_ipv4 80 expected-closed" \
|
||||||
|
"$public_ipv4 222 expected-closed"; do
|
||||||
|
set -- $check
|
||||||
|
target="$1"
|
||||||
|
port="$2"
|
||||||
|
expected="$3"
|
||||||
|
if timeout 5 bash -c "cat < /dev/null > /dev/tcp/$target/$port" 2>/dev/null; then
|
||||||
|
result="open"
|
||||||
|
else
|
||||||
|
result="closed"
|
||||||
|
fi
|
||||||
|
printf '%s:%s %s (%s)\n' "$target" "$port" "$result" "$expected"
|
||||||
|
done
|
||||||
|
|
||||||
|
section "Borg UI Repository"
|
||||||
|
if [[ -f "$BORG_DB" ]]; then
|
||||||
|
sqlite3 -header -csv "$BORG_DB" \
|
||||||
|
"select name,repository_type,path,remote_path,last_backup,last_check,borg_version,custom_flags from repositories order by id;"
|
||||||
|
sqlite3 -header -csv "$BORG_DB" \
|
||||||
|
"select id,repository,status,archive_name,started_at,completed_at,nfiles from backup_jobs order by id desc limit 3;"
|
||||||
|
else
|
||||||
|
echo "Borg UI database not found: $BORG_DB"
|
||||||
|
fi
|
||||||
|
|
||||||
|
section "Restore Freshness"
|
||||||
|
if [[ -x "$REPO_ROOT/ops/restore-tests/run-restore-checks.sh" ]]; then
|
||||||
|
"$REPO_ROOT/ops/restore-tests/run-restore-checks.sh" freshness
|
||||||
|
else
|
||||||
|
echo "Restore freshness script not executable: $REPO_ROOT/ops/restore-tests/run-restore-checks.sh"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
param(
|
||||||
|
[string]$HostLanIp = "192.168.178.58",
|
||||||
|
[string]$FritzBoxIp = "192.168.178.1",
|
||||||
|
[ValidateSet("LanPreflight", "Guest")]
|
||||||
|
[string]$Mode = "LanPreflight",
|
||||||
|
[string]$ReportPath = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Test-TcpPort {
|
||||||
|
param(
|
||||||
|
[string]$RemoteHost,
|
||||||
|
[int]$Port,
|
||||||
|
[int]$TimeoutMs = 1500
|
||||||
|
)
|
||||||
|
|
||||||
|
$client = [System.Net.Sockets.TcpClient]::new()
|
||||||
|
try {
|
||||||
|
$async = $client.BeginConnect($RemoteHost, $Port, $null, $null)
|
||||||
|
$ok = $async.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
|
||||||
|
if (-not $ok) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
$client.EndConnect($async)
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
} finally {
|
||||||
|
$client.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-Result {
|
||||||
|
param(
|
||||||
|
[System.Collections.Generic.List[object]]$Results,
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Target,
|
||||||
|
[bool]$Reachable,
|
||||||
|
[string]$ExpectedGuest,
|
||||||
|
[string]$Risk
|
||||||
|
)
|
||||||
|
|
||||||
|
$Results.Add([pscustomobject]@{
|
||||||
|
Name = $Name
|
||||||
|
Target = $Target
|
||||||
|
Reachable = $Reachable
|
||||||
|
ExpectedFromGuest = $ExpectedGuest
|
||||||
|
RiskIfReachableFromGuest = $Risk
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$adapters = Get-NetIPConfiguration |
|
||||||
|
Where-Object { $_.IPv4Address -and $_.NetAdapter.Status -eq "Up" } |
|
||||||
|
Select-Object InterfaceAlias,
|
||||||
|
@{Name="IPv4";Expression={$_.IPv4Address.IPAddress -join ", "}},
|
||||||
|
@{Name="Gateway";Expression={$_.IPv4DefaultGateway.NextHop -join ", "}},
|
||||||
|
@{Name="DnsServer";Expression={$_.DNSServer.ServerAddresses -join ", "}}
|
||||||
|
|
||||||
|
$results = [System.Collections.Generic.List[object]]::new()
|
||||||
|
|
||||||
|
Add-Result $results "Unraid HTTP/LAN" "${HostLanIp}:80" (Test-TcpPort $HostLanIp 80) "blocked" "Guest can reach LAN web entrypoint directly"
|
||||||
|
Add-Result $results "Unraid HTTPS/LAN" "${HostLanIp}:443" (Test-TcpPort $HostLanIp 443) "blocked" "Guest can reach LAN HTTPS entrypoint directly"
|
||||||
|
Add-Result $results "Gitea SSH/LAN" "${HostLanIp}:222" (Test-TcpPort $HostLanIp 222) "blocked" "Guest can reach Git SSH"
|
||||||
|
Add-Result $results "AdGuard Admin/LAN" "${HostLanIp}:8082" (Test-TcpPort $HostLanIp 8082) "blocked" "Guest can reach AdGuard admin UI"
|
||||||
|
Add-Result $results "InfluxDB LAN" "${HostLanIp}:8181" (Test-TcpPort $HostLanIp 8181) "blocked" "Guest can reach InfluxDB writer endpoint"
|
||||||
|
Add-Result $results "FRITZ!Box LAN UI" "${FritzBoxIp}:80" (Test-TcpPort $FritzBoxIp 80) "blocked-or-guest-gateway-only" "Guest can reach main router UI"
|
||||||
|
|
||||||
|
$risk = if ($Mode -eq "Guest") {
|
||||||
|
$results | Where-Object {
|
||||||
|
$_.ExpectedFromGuest -like "blocked*" -and $_.Reachable
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$results | Where-Object {
|
||||||
|
$_.Name -in @("AdGuard Admin/LAN", "InfluxDB LAN") -and $_.Reachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||||
|
$lines = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$lines.Add("# Guest/IoT Isolation Check")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("Timestamp: $timestamp")
|
||||||
|
$lines.Add("Mode: $Mode")
|
||||||
|
$lines.Add("Host LAN IP: $HostLanIp")
|
||||||
|
$lines.Add("FRITZ!Box IP: $FritzBoxIp")
|
||||||
|
$lines.Add("Risk count: $($risk.Count)")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("## Active Network Adapters")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("| Interface | IPv4 | Gateway | DNS |")
|
||||||
|
$lines.Add("|---|---|---|---|")
|
||||||
|
foreach ($adapter in $adapters) {
|
||||||
|
$lines.Add("| $($adapter.InterfaceAlias) | $($adapter.IPv4) | $($adapter.Gateway) | $($adapter.DnsServer) |")
|
||||||
|
}
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("## Port Tests")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("| Name | Target | Reachable | Expected from guest Wi-Fi | Risk if reachable from guest |")
|
||||||
|
$lines.Add("|---|---|---:|---|---|")
|
||||||
|
foreach ($result in $results) {
|
||||||
|
$lines.Add("| $($result.Name) | $($result.Target) | $($result.Reachable) | $($result.ExpectedFromGuest) | $($result.RiskIfReachableFromGuest) |")
|
||||||
|
}
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("## Interpretation")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("- `LanPreflight`: reachable `80/443/222` can be normal; `8082` and `8181` should still be blocked.")
|
||||||
|
$lines.Add("- `Guest`: all listed LAN targets should be blocked. Public domains may still work via the internet path.")
|
||||||
|
$lines.Add("- A non-zero risk count means the selected mode failed.")
|
||||||
|
|
||||||
|
$text = $lines -join [Environment]::NewLine
|
||||||
|
|
||||||
|
if ($ReportPath) {
|
||||||
|
$parent = Split-Path -Parent $ReportPath
|
||||||
|
if ($parent) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
||||||
|
}
|
||||||
|
Set-Content -Path $ReportPath -Value $text -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output $text
|
||||||
|
|
||||||
|
if ($risk.Count -gt 0) {
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOST_LAN_IP="${HOST_LAN_IP:-192.168.178.58}"
|
||||||
|
TAILSCALE_IP="${TAILSCALE_IP:-100.80.98.33}"
|
||||||
|
FRITZBOX_TR064_URL="${FRITZBOX_TR064_URL:-http://192.168.178.1:49000/tr64desc.xml}"
|
||||||
|
REPORT_ROOT="${REPORT_ROOT:-/mnt/user/backups/restore-reports}"
|
||||||
|
STAMP="$(date +%F-%H%M%S)"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/guest-iot-preflight-$STAMP.md"
|
||||||
|
|
||||||
|
mkdir -p "$REPORT_ROOT"
|
||||||
|
|
||||||
|
tcp_check() {
|
||||||
|
local host="$1"
|
||||||
|
local port="$2"
|
||||||
|
timeout 2 bash -c "cat < /dev/null > /dev/tcp/$host/$port" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
result_row() {
|
||||||
|
local name="$1"
|
||||||
|
local target="$2"
|
||||||
|
local expectation="$3"
|
||||||
|
local status="$4"
|
||||||
|
printf '| %s | `%s` | %s | %s |\n' "$name" "$target" "$status" "$expectation"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Guest/IoT Preflight"
|
||||||
|
echo
|
||||||
|
echo "Timestamp: $(date '+%F %T')"
|
||||||
|
echo "Scope: host-side read-only checks before enabling FRITZ!Box guest/IoT network"
|
||||||
|
echo
|
||||||
|
echo "## FRITZ!Box TR-064"
|
||||||
|
echo
|
||||||
|
if curl -fsS --max-time 5 "$FRITZBOX_TR064_URL" >/tmp/guest-iot-fritzbox-tr064.xml 2>/dev/null; then
|
||||||
|
model="$(grep -o '<friendlyName>[^<]*' /tmp/guest-iot-fritzbox-tr064.xml | head -n1 | sed 's/<friendlyName>//')"
|
||||||
|
echo "- TR-064 descriptor reachable: yes"
|
||||||
|
echo "- Model: ${model:-unknown}"
|
||||||
|
else
|
||||||
|
echo "- TR-064 descriptor reachable: no"
|
||||||
|
fi
|
||||||
|
rm -f /tmp/guest-iot-fritzbox-tr064.xml
|
||||||
|
echo
|
||||||
|
echo "## Host listeners"
|
||||||
|
echo
|
||||||
|
echo '```text'
|
||||||
|
ss -ltnp | sort -k4 | grep -E ':(53|80|443|222|8082|8181)[[:space:]]' || true
|
||||||
|
echo '```'
|
||||||
|
echo
|
||||||
|
echo "## Port reachability from host namespace"
|
||||||
|
echo
|
||||||
|
echo "| Check | Target | Status | Expectation |"
|
||||||
|
echo "|---|---|---|---|"
|
||||||
|
|
||||||
|
for port in 80 443 222 53; do
|
||||||
|
if tcp_check "$HOST_LAN_IP" "$port"; then
|
||||||
|
result_row "LAN service" "$HOST_LAN_IP:$port" "may be reachable from normal LAN; must be blocked from guest Wi-Fi" "reachable"
|
||||||
|
else
|
||||||
|
result_row "LAN service" "$HOST_LAN_IP:$port" "not reachable from host namespace or UDP-only" "blocked"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if tcp_check "$HOST_LAN_IP" 8082; then
|
||||||
|
result_row "AdGuard Admin via LAN IP" "$HOST_LAN_IP:8082" "should be blocked" "reachable"
|
||||||
|
else
|
||||||
|
result_row "AdGuard Admin via LAN IP" "$HOST_LAN_IP:8082" "should be blocked" "blocked"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if tcp_check "$TAILSCALE_IP" 8082; then
|
||||||
|
result_row "AdGuard Admin via Tailscale IP" "$TAILSCALE_IP:8082" "operator path should work" "reachable"
|
||||||
|
else
|
||||||
|
result_row "AdGuard Admin via Tailscale IP" "$TAILSCALE_IP:8082" "operator path should work" "blocked"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if tcp_check "$HOST_LAN_IP" 8181; then
|
||||||
|
result_row "InfluxDB via LAN IP" "$HOST_LAN_IP:8181" "should be blocked unless HA LAN writer is reintroduced" "reachable"
|
||||||
|
else
|
||||||
|
result_row "InfluxDB via LAN IP" "$HOST_LAN_IP:8181" "should be blocked unless HA LAN writer is reintroduced" "blocked"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "## Next operator step"
|
||||||
|
echo
|
||||||
|
echo "Enable FRITZ!Box guest Wi-Fi only after confirming LAN isolation is active. Then connect a phone/laptop to guest Wi-Fi and run:"
|
||||||
|
echo
|
||||||
|
echo '```powershell'
|
||||||
|
echo 'G:\Gitea_Clone\homelab-infra\ops\maintenance\check-guest-iot-isolation.ps1 -Mode Guest'
|
||||||
|
echo '```'
|
||||||
|
} | tee "$REPORT_FILE"
|
||||||
|
|
||||||
|
echo "Guest/IoT preflight report: $REPORT_FILE"
|
||||||
Executable
+125
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# check-unraid-flash-backup.sh
|
||||||
|
#
|
||||||
|
# Read-only Validierung des Unraid-Flash-Backup-Artefakts
|
||||||
|
# (`unraid-flash-config.tar.gz`) ohne produktive Extraktion.
|
||||||
|
#
|
||||||
|
# Prueft:
|
||||||
|
# 1. Artefakt, Checksumme und Manifest sind vorhanden
|
||||||
|
# 2. Artefakt ist frisch genug (Standard: <= 36 h)
|
||||||
|
# 3. `sha256sum -c` ist OK
|
||||||
|
# 4. Archiv enthaelt die array-/identitaetsdefinierenden Kern-Configs
|
||||||
|
#
|
||||||
|
# Es wird NICHTS extrahiert. `tar -tzf` listet nur Eintragsnamen.
|
||||||
|
# Das Artefakt enthaelt Host-Konfiguration inkl. SSH-Host-Keys, passwd/shadow
|
||||||
|
# und Tailscale-State und ist wie Secret-Material zu behandeln. Dieses Skript
|
||||||
|
# gibt bewusst nur Datei-/Eintragsnamen aus, niemals Inhalte.
|
||||||
|
#
|
||||||
|
# Exit-Codes:
|
||||||
|
# 0 alles OK
|
||||||
|
# 1 Validierung fehlgeschlagen (fehlende Datei, Checksumme falsch,
|
||||||
|
# fehlende Kern-Config)
|
||||||
|
# 2 Artefakt aelter als erlaubt (Frische-Warnung)
|
||||||
|
|
||||||
|
DUMPS_DIR="${DUMPS_DIR:-/mnt/user/backups/borg/dumps/latest}"
|
||||||
|
ARTIFACT="${ARTIFACT:-unraid-flash-config.tar.gz}"
|
||||||
|
MAX_AGE_HOURS="${MAX_AGE_HOURS:-36}"
|
||||||
|
|
||||||
|
# Kern-Configs, die ein brauchbares Flash-Restore mindestens enthalten muss.
|
||||||
|
CRITICAL_FILES=(
|
||||||
|
"config/super.dat" # Array-/Disk-Zuordnung
|
||||||
|
"config/disk.cfg" # Array-Einstellungen
|
||||||
|
"config/ident.cfg" # Hostname/Identitaet
|
||||||
|
"config/share.cfg" # Share-Grundeinstellungen
|
||||||
|
"config/network.cfg" # Netzwerk
|
||||||
|
"config/docker.cfg" # Docker-Settings
|
||||||
|
"config/go" # Boot-Script
|
||||||
|
"config/domain.cfg" # VM/Domain-Settings
|
||||||
|
)
|
||||||
|
|
||||||
|
fail=0
|
||||||
|
|
||||||
|
artifact_path="$DUMPS_DIR/$ARTIFACT"
|
||||||
|
sha_path="$artifact_path.sha256"
|
||||||
|
manifest_path="$DUMPS_DIR/unraid-flash-config.manifest.txt"
|
||||||
|
|
||||||
|
echo "## Unraid Flash Backup Validierung"
|
||||||
|
echo "Verzeichnis: $DUMPS_DIR"
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 1. Existenz
|
||||||
|
for f in "$artifact_path" "$sha_path" "$manifest_path"; do
|
||||||
|
if [ -f "$f" ]; then
|
||||||
|
echo "OK vorhanden: $(basename "$f")"
|
||||||
|
else
|
||||||
|
echo "FEHLER fehlt: $(basename "$f")"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Wenn das Artefakt fehlt, hat alles Weitere keinen Sinn.
|
||||||
|
if [ ! -f "$artifact_path" ]; then
|
||||||
|
echo "Abbruch: Artefakt nicht vorhanden."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Frische
|
||||||
|
now_epoch="$(date +%s)"
|
||||||
|
file_epoch="$(stat -c %Y "$artifact_path")"
|
||||||
|
age_hours=$(( (now_epoch - file_epoch) / 3600 ))
|
||||||
|
echo "Alter des Artefakts: ${age_hours} h (Grenze: ${MAX_AGE_HOURS} h)"
|
||||||
|
stale=0
|
||||||
|
if [ "$age_hours" -gt "$MAX_AGE_HOURS" ]; then
|
||||||
|
echo "WARNUNG Artefakt ist aelter als ${MAX_AGE_HOURS} h."
|
||||||
|
stale=1
|
||||||
|
else
|
||||||
|
echo "OK Artefakt ist frisch."
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
|
||||||
|
# 3. Checksumme
|
||||||
|
if [ -f "$sha_path" ]; then
|
||||||
|
if ( cd "$DUMPS_DIR" && sha256sum -c "$(basename "$sha_path")" ) ; then
|
||||||
|
echo "OK sha256 stimmt."
|
||||||
|
else
|
||||||
|
echo "FEHLER sha256-Pruefung fehlgeschlagen."
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Kern-Configs (nur Namen, keine Extraktion)
|
||||||
|
echo "## Kern-Configs im Archiv"
|
||||||
|
listing="$(tar -tzf "$artifact_path")"
|
||||||
|
entry_count="$(printf '%s\n' "$listing" | wc -l | tr -d ' ')"
|
||||||
|
echo "Eintraege im Archiv: $entry_count"
|
||||||
|
for cf in "${CRITICAL_FILES[@]}"; do
|
||||||
|
if printf '%s\n' "$listing" | grep -qxF "$cf"; then
|
||||||
|
echo "OK $cf"
|
||||||
|
else
|
||||||
|
echo "FEHLER $cf fehlt im Archiv"
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Manifest-Kopf zur Orientierung (enthaelt keine Secret-Werte)
|
||||||
|
if [ -f "$manifest_path" ]; then
|
||||||
|
echo "## Manifest"
|
||||||
|
cat "$manifest_path"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$fail" -ne 0 ]; then
|
||||||
|
echo "ERGEBNIS: FEHLGESCHLAGEN"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ "$stale" -ne 0 ]; then
|
||||||
|
echo "ERGEBNIS: OK, aber Frische-Warnung"
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
echo "ERGEBNIS: OK"
|
||||||
|
exit 0
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# DR-Workstation Quartals-Smoke
|
||||||
|
#
|
||||||
|
# Prueft Git-Read, Hetzner-SSH und Borg-Repo-Erreichbarkeit vom Operator-PC.
|
||||||
|
# Speichert keine Passphrase. Borg fragt interaktiv nach der Repo-Passphrase.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
GITHUB_KEY="${GITHUB_KEY:-$HOME/.ssh/dr-readonly}"
|
||||||
|
HETZNER_KEY="${HETZNER_KEY:-$HOME/.ssh/dr-hetzner}"
|
||||||
|
GITHUB_REPO="${GITHUB_REPO:-git@github.com:michaelkaleschke-spec/homelab-infra.git}"
|
||||||
|
BORG_REPO="${BORG_REPO:-ssh://u565255@u565255.your-storagebox.de/./hetzner_borg_appdata_critical}"
|
||||||
|
|
||||||
|
echo "=== Tooling ==="
|
||||||
|
command -v ssh
|
||||||
|
command -v git
|
||||||
|
command -v borg
|
||||||
|
borg --version
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== Key files ==="
|
||||||
|
test -r "$GITHUB_KEY" || { echo "Missing GitHub key: $GITHUB_KEY" >&2; exit 1; }
|
||||||
|
test -r "$HETZNER_KEY" || { echo "Missing Hetzner key: $HETZNER_KEY" >&2; exit 1; }
|
||||||
|
ls -l "$GITHUB_KEY" "$HETZNER_KEY"
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== GitHub Deploy-Key ==="
|
||||||
|
GIT_SSH_COMMAND="ssh -i $GITHUB_KEY -o IdentitiesOnly=yes -o BatchMode=yes" \
|
||||||
|
git ls-remote "$GITHUB_REPO" HEAD
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== Hetzner SSH-Login ==="
|
||||||
|
ssh -i "$HETZNER_KEY" -o IdentitiesOnly=yes -o BatchMode=yes -p 23 \
|
||||||
|
u565255@u565255.your-storagebox.de "ls" | head -5
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "=== Borg-Repo ==="
|
||||||
|
export BORG_RSH="ssh -i $HETZNER_KEY -o IdentitiesOnly=yes -p 23"
|
||||||
|
borg info "$BORG_REPO" | head -12
|
||||||
|
echo
|
||||||
|
|
||||||
|
echo "DR-Smoke OK ($(date '+%F %T'))"
|
||||||
@@ -3,6 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
MODE="dry-run"
|
MODE="dry-run"
|
||||||
CUTOFF_DATE="2026-06-02"
|
CUTOFF_DATE="2026-06-02"
|
||||||
|
ARCHIVE_ROOT="/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602"
|
||||||
|
|
||||||
if [[ "${1:-}" == "--execute" ]]; then
|
if [[ "${1:-}" == "--execute" ]]; then
|
||||||
MODE="execute"
|
MODE="execute"
|
||||||
@@ -23,10 +24,10 @@ if [[ "$MODE" == "execute" && "$today" < "$CUTOFF_DATE" ]]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
declare -a CANDIDATES=(
|
declare -a CANDIDATES=(
|
||||||
"/mnt/user/appdata/postgresql17|/mnt/user/appdata/postgresql18|shared PostgreSQL 17 rollback"
|
"/mnt/user/appdata/postgresql17|/mnt/user/appdata/postgresql18|postgresql17|shared PostgreSQL 17 rollback"
|
||||||
"/mnt/user/appdata/mealie/postgres|/mnt/user/appdata/mealie/postgres18|Mealie PostgreSQL 17 rollback"
|
"/mnt/user/appdata/mealie/postgres|/mnt/user/appdata/mealie/postgres18|mealie-postgres17|Mealie PostgreSQL 17 rollback"
|
||||||
"/mnt/user/appdata/nextcloud/postgres|/mnt/user/appdata/nextcloud/postgres18|Nextcloud PostgreSQL 17 rollback"
|
"/mnt/user/appdata/nextcloud/postgres|/mnt/user/appdata/nextcloud/postgres18|nextcloud-postgres17|Nextcloud PostgreSQL 17 rollback"
|
||||||
"/mnt/user/appdata/immich_postgres|/mnt/user/appdata/immich_postgres_vectorchord|Immich pgvecto.rs rollback"
|
"/mnt/user/appdata/immich_postgres|/mnt/user/appdata/immich_postgres_vectorchord|immich-postgres-pgvecto-rs|Immich pgvecto.rs rollback"
|
||||||
)
|
)
|
||||||
|
|
||||||
require_container_healthy() {
|
require_container_healthy() {
|
||||||
@@ -48,9 +49,10 @@ require_container_healthy() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
echo "Alt-volume release check"
|
echo "Alt-volume archive check"
|
||||||
echo "Mode: $MODE"
|
echo "Mode: $MODE"
|
||||||
echo "Date: $today"
|
echo "Date: $today"
|
||||||
|
echo "Archive: $ARCHIVE_ROOT"
|
||||||
echo
|
echo
|
||||||
|
|
||||||
require_container_healthy postgresql17
|
require_container_healthy postgresql17
|
||||||
@@ -68,37 +70,58 @@ if [[ -x /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.s
|
|||||||
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
/mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mapfile -t active_mounts < <(docker inspect $(docker ps -q) --format '{{range .Mounts}}{{println .Source}}{{end}}' 2>/dev/null || true)
|
mapfile -t active_mounts < <(docker inspect $(docker ps -aq) --format '{{range .Mounts}}{{println .Source}}{{end}}' 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ "$MODE" == "execute" ]]; then
|
||||||
|
mkdir -p "$ARCHIVE_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
for entry in "${CANDIDATES[@]}"; do
|
for entry in "${CANDIDATES[@]}"; do
|
||||||
IFS='|' read -r old_path active_path label <<< "$entry"
|
IFS='|' read -r old_path active_path archive_name label <<< "$entry"
|
||||||
|
archive_path="$ARCHIVE_ROOT/$archive_name"
|
||||||
|
|
||||||
if [[ ! -d "$active_path" ]]; then
|
if [[ ! -d "$active_path" ]]; then
|
||||||
echo "Missing active path for $label: $active_path" >&2
|
echo "Missing active path for $label: $active_path" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ ! -d "$old_path" ]]; then
|
if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then
|
||||||
echo "Already absent: $old_path ($label)"
|
echo "Refusing: old path is still mounted by a container: $old_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$old_path" && -d "$archive_path" ]]; then
|
||||||
|
echo "Refusing: both old path and archive path exist for $label." >&2
|
||||||
|
echo "Old: $old_path" >&2
|
||||||
|
echo "Archive: $archive_path" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -d "$archive_path" ]]; then
|
||||||
|
size="$(du -sh "$archive_path" 2>/dev/null | awk '{print $1}')"
|
||||||
|
echo "Archived: $archive_path ($label, $size)"
|
||||||
|
echo
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if printf '%s\n' "${active_mounts[@]}" | grep -Fxq "$old_path"; then
|
if [[ ! -d "$old_path" ]]; then
|
||||||
echo "Refusing: old path is still mounted by a running container: $old_path" >&2
|
echo "Absent and not archived: $old_path ($label)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
size="$(du -sh "$old_path" 2>/dev/null | awk '{print $1}')"
|
size="$(du -sh "$old_path" 2>/dev/null | awk '{print $1}')"
|
||||||
echo "Candidate: $old_path ($label, $size)"
|
echo "Candidate: $old_path ($label, $size)"
|
||||||
echo "Active: $active_path"
|
echo "Active: $active_path"
|
||||||
|
echo "Archive: $archive_path"
|
||||||
|
|
||||||
if [[ "$MODE" == "execute" ]]; then
|
if [[ "$MODE" == "execute" ]]; then
|
||||||
rm -rf --one-file-system "$old_path"
|
mv "$old_path" "$archive_path"
|
||||||
echo "Removed: $old_path"
|
printf '%s MOVE %s -> %s size=%s\n' "$(date -Is)" "$old_path" "$archive_path" "$size" >> "$ARCHIVE_ROOT/MANIFEST.txt"
|
||||||
|
echo "Moved: $archive_path"
|
||||||
else
|
else
|
||||||
echo "Dry-run: would remove $old_path"
|
echo "Dry-run: would move $old_path to $archive_path"
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Alt-volume release check completed."
|
echo "Alt-volume archive check completed."
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ Ziel:
|
|||||||
## Geplante Struktur
|
## Geplante Struktur
|
||||||
|
|
||||||
- `schedule.md`: Intervalle und Verantwortlichkeiten
|
- `schedule.md`: Intervalle und Verantwortlichkeiten
|
||||||
|
- `common.sh`: gemeinsame Helfer fuer Borg-Lookup, Borg-Extract und Compose-Cleanup; prueft vor Borg-Operationen auch `borg-ui:/data/borg.db` und `borg-ui:/local/secrets/borg_repo_passphrase.txt`
|
||||||
- `vaultwarden-restore-test.ps1`: erster Mini-Restore-Ablauf
|
- `vaultwarden-restore-test.ps1`: erster Mini-Restore-Ablauf
|
||||||
- `vaultwarden-restore-test.sh`: hosttauglicher Vaultwarden-Restore-Job
|
- `vaultwarden-restore-test.sh`: hosttauglicher Vaultwarden-Restore-Job
|
||||||
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
|
- `vaultwarden-plan.md`: konkreter Vaultwarden-Testplan
|
||||||
@@ -37,9 +38,21 @@ Ziel:
|
|||||||
- `immich-plan.md`: konkreter Immich-Testplan
|
- `immich-plan.md`: konkreter Immich-Testplan
|
||||||
- `immich-runbook.md`: Operator-Runbook fuer den ersten Immich-Lauf
|
- `immich-runbook.md`: Operator-Runbook fuer den ersten Immich-Lauf
|
||||||
- `immich-compose.test.yml`: isolierte Testinstanz fuer Immich inkl. VectorChord/pgvector-Test-Postgres und Test-Redis
|
- `immich-compose.test.yml`: isolierte Testinstanz fuer Immich inkl. VectorChord/pgvector-Test-Postgres und Test-Redis
|
||||||
|
- `authelia-restore-test.sh`: Authelia-Restore-Job (Config-Smoke; Erstlauf 2026-06-03 erfolgreich)
|
||||||
|
- `authelia-compose.test.yml`: isolierte Testinstanz fuer Authelia inkl. Test-Postgres, Filesystem-Notifier (kein echter SMTP-Versand)
|
||||||
|
- `authelia-plan.md`: konkreter Authelia-Testplan
|
||||||
|
- `authelia-runbook.md`: Operator-Runbook fuer den ersten Authelia-Lauf
|
||||||
|
- `adguard-restore-test.sh`: AdGuard-Home-Restore-Job (Config + isolierter Container + HTTP/DNS-Smoke; Erstlauf 2026-06-06 erfolgreich)
|
||||||
|
- `adguard-compose.test.yml`: isolierte AdGuard-Testinstanz auf localhost-Ports `13001` und `15353`
|
||||||
|
- `redis-restore-test.sh`: Redis-8-Restore-Job (Pre-Cutover-Artefakt + isolierter Container + PING/INFO/DBSIZE; Erstlauf 2026-06-06 erfolgreich)
|
||||||
|
- `redis-compose.test.yml`: isolierte Redis-8-Testinstanz auf localhost-Port `16379`
|
||||||
|
- `nextcloud-restore-test.sh`: Nextcloud-Restore-Job (Scaffold; **blockiert** durch Unraid shfs-chmod-Inkompatibilitaet - siehe unten)
|
||||||
|
- `nextcloud-compose.test.yml`: isolierte Testinstanz fuer Nextcloud inkl. Test-Postgres und Test-Redis
|
||||||
|
|
||||||
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
||||||
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
||||||
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
||||||
|
- `negative-freshness-alert-test.sh`: sicherer Negativtest fuer den Frische-Alarmweg; nutzt synthetische leere Testpfade unter `/mnt/user/backups/restore-lab/freshness-negative`, veraendert keine produktiven Dumps und sendet bei erkanntem Fehler einen Test-Alert nach `homelab-alerts`
|
||||||
- `run-restore-checks.sh`: hosttauglicher Dispatcher
|
- `run-restore-checks.sh`: hosttauglicher Dispatcher
|
||||||
- `common.sh`: gemeinsame Host-Helferfunktionen
|
- `common.sh`: gemeinsame Host-Helferfunktionen
|
||||||
- `automation-plan.md`: Host-Job- und Automatisierungsmodell
|
- `automation-plan.md`: Host-Job- und Automatisierungsmodell
|
||||||
@@ -82,9 +95,15 @@ Aktuell ist das erste validierte Muster vorhanden.
|
|||||||
- echter Gitea-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
|
- echter Paperless-Restore am 2026-05-07 erfolgreich verifiziert
|
||||||
- Immich-Restore-Test am 2026-05-27 erfolgreich verifiziert; Test-Postgres wurde nach der VectorChord-Migration am 2026-05-31 auf das produktive Immich-Postgres-Image umgestellt
|
- Immich-Restore-Test am 2026-05-27 erfolgreich verifiziert; Test-Postgres wurde nach der VectorChord-Migration am 2026-05-31 auf das produktive Immich-Postgres-Image umgestellt
|
||||||
|
- Authelia-Restore-Smoke am 2026-06-03 erfolgreich verifiziert; bewusst ohne produktiven Dump-Restore wegen Storage-Encryption-Key-Kopplung
|
||||||
|
- AdGuard-Home-Restore-Smoke am 2026-06-06 erfolgreich verifiziert; Borg-Config-Restore, HTTP `/control/status` 401, DNS-Smoke ok, 7 Filterlisten-Eintraege, Report `/mnt/user/backups/restore-reports/adguard-2026-06-06.md`
|
||||||
|
- Redis-8-Restore-Smoke am 2026-06-06 erfolgreich verifiziert; Pre-Cutover-Artefakt, Redis 8.8, PING ok, AOF aktiv, DBSIZE 1, Report `/mnt/user/backups/restore-reports/redis-2026-06-06.md`
|
||||||
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
||||||
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
||||||
- V1-Ablauf weiter ohne `ntfy`, mit Bereinigung nach Erfolg
|
- `ntfy`-Wrapper ist fuer Host-Jobs verfuegbar
|
||||||
- naechster grosser Kandidat ist ein erneuter Immich-Lauf nach VectorChord-Migration mit Zeitmessung; danach in die Rotation aufnehmen
|
- Frische-Negativtest ist als sicherer Host-Job verfuegbar und am 2026-06-06 auf Unraid validiert: `ops/restore-tests/run-restore-checks.sh freshness-negative`. Ergebnis: synthetischer leerer Dump-Pfad erzeugte 10 Criticals, Test-Alert ging nach `homelab-alerts`, produktive Dump-Pfade blieben unangetastet. Report: `/mnt/user/backups/restore-reports/freshness-negative-2026-06-06-130320.md`.
|
||||||
|
- Nextcloud-Restore-Test: Scaffold existiert, aber **blockiert**. Nextcloud 33 fuehrt zur Laufzeit `chmod()` auf Dateien unter `/var/www/html` aus (`OC_Util.php:486`). Auf Unraids FUSE/shfs User-Shares ist `chmod` strukturell nicht moeglich, was zu permanenter 503 fuehrt. Loesungsoptionen: (a) Restore-Lab auf ein Cache-Drive statt User Share legen, (b) Docker-Volumes statt Bind-Mounts verwenden, (c) tmpfs-Mount fuer html/ + `rsync` der Borg-Daten hinein. Bis dahin ist Nextcloud als Backlog-Item dokumentiert.
|
||||||
|
- Komodo-Mongo-Daten-Restore am 2026-06-03 erfolgreich: 86904 Dokumente (inkl. 32 Stacks), Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`
|
||||||
|
- naechste grosse Kandidaten sind Mailarchiver und Mealie; Nextcloud bleibt blockiert (shfs-chmod)
|
||||||
|
|
||||||
Vor dem ersten echten Testlauf muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
Vor dem ersten echten Testlauf je neuem Dienst muessen Zielpfade, Quellpfade und Bereinigungsschritte bewusst freigegeben werden.
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
services:
|
||||||
|
restoretest-adguard:
|
||||||
|
image: adguard/adguardhome:v0.107.76@sha256:7157eb1dc3b26c7af1d6898759a7b3f7d0fa09891fbd2d3caa6abc1057a9179b
|
||||||
|
container_name: restoretest-adguard
|
||||||
|
restart: "no"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:15353:53/tcp"
|
||||||
|
- "127.0.0.1:15353:53/udp"
|
||||||
|
- "127.0.0.1:13001:80/tcp"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/adguard/work:/opt/adguardhome/work
|
||||||
|
- /mnt/user/backups/restore-lab/adguard/conf:/opt/adguardhome/conf
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
Executable
+181
@@ -0,0 +1,181 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# AdGuard Home Restore Smoke Test
|
||||||
|
#
|
||||||
|
# Scope:
|
||||||
|
# - Borg-Extract von /local/appdata/adguard/conf
|
||||||
|
# - YAML-/Strukturcheck fuer AdGuardHome.yaml
|
||||||
|
# - Start einer isolierten Testinstanz auf localhost-Ports
|
||||||
|
# - HTTP-Smoke gegen Admin-UI/API
|
||||||
|
# - DNS-Smoke gegen localhost:15353, falls ein passender Resolver-Client vorhanden ist
|
||||||
|
|
||||||
|
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/adguard"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/adguard-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/adguard-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/adguard-$(date +%F).md"
|
||||||
|
TEST_HTTP="http://127.0.0.1:13001"
|
||||||
|
TEST_DNS_PORT="15353"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
AdGuard Home restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
Borg source: local/appdata/adguard/conf
|
||||||
|
Test HTTP endpoint: $TEST_HTTP
|
||||||
|
Test DNS endpoint: 127.0.0.1:$TEST_DNS_PORT
|
||||||
|
Scope: Config-Restore + isolated AdGuard boot + HTTP/DNS smoke
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "adguard" "$RESTORE_ROOT"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT/conf" "$RESTORE_ROOT/work"
|
||||||
|
|
||||||
|
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/adguard-extract" "local/appdata/adguard/conf"
|
||||||
|
|
||||||
|
if [ ! -d "$EXTRACT_DIR/local/appdata/adguard/conf" ]; then
|
||||||
|
echo "AdGuard conf path missing in Borg archive" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp -a "$EXTRACT_DIR/local/appdata/adguard/conf/." "$RESTORE_ROOT/conf/"
|
||||||
|
chmod -R a+rX "$RESTORE_ROOT/conf"
|
||||||
|
chmod -R a+rwX "$RESTORE_ROOT/work"
|
||||||
|
|
||||||
|
config_file="$RESTORE_ROOT/conf/AdGuardHome.yaml"
|
||||||
|
if [ ! -s "$config_file" ]; then
|
||||||
|
echo "Missing restored AdGuardHome.yaml" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v ruby >/dev/null 2>&1; then
|
||||||
|
ruby -e 'require "yaml"; YAML.load_file(ARGV.fetch(0))' "$config_file"
|
||||||
|
yaml_check="ruby-yaml-ok"
|
||||||
|
else
|
||||||
|
grep -q '^dns:' "$config_file"
|
||||||
|
grep -q '^http:' "$config_file"
|
||||||
|
yaml_check="basic-structure-ok"
|
||||||
|
fi
|
||||||
|
|
||||||
|
filter_count="$(grep -c '^[[:space:]]*-[[:space:]]*enabled:' "$config_file" 2>/dev/null || true)"
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-adguard >/dev/null
|
||||||
|
|
||||||
|
http_status=""
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
http_status="$(curl -s -o /tmp/adguard-body.html -w '%{http_code}' \
|
||||||
|
"$TEST_HTTP/control/status" || true)"
|
||||||
|
if [ "$http_status" = "200" ] || [ "$http_status" = "401" ] || [ "$http_status" = "403" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$http_status" != "200" ] && [ "$http_status" != "401" ] && [ "$http_status" != "403" ]; then
|
||||||
|
echo "AdGuard HTTP smoke failed: status=$http_status" >&2
|
||||||
|
docker logs --tail 80 restoretest-adguard >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dns_status="not-run"
|
||||||
|
dns_detail="no dig/drill command available"
|
||||||
|
if command -v dig >/dev/null 2>&1; then
|
||||||
|
if dig @127.0.0.1 -p "$TEST_DNS_PORT" git.kaleschke.info A +time=3 +tries=1 >/tmp/adguard-dig.out 2>&1; then
|
||||||
|
dns_status="ok"
|
||||||
|
dns_detail="$(grep -E '^[[:alnum:].-]+[[:space:]]+[0-9]+[[:space:]]+IN[[:space:]]+A[[:space:]]+' /tmp/adguard-dig.out | head -1 || true)"
|
||||||
|
else
|
||||||
|
dns_status="failed"
|
||||||
|
dns_detail="$(tail -20 /tmp/adguard-dig.out | tr '\n' ' ')"
|
||||||
|
fi
|
||||||
|
elif command -v drill >/dev/null 2>&1; then
|
||||||
|
if drill -p "$TEST_DNS_PORT" git.kaleschke.info @127.0.0.1 >/tmp/adguard-drill.out 2>&1; then
|
||||||
|
dns_status="ok"
|
||||||
|
dns_detail="$(grep -E '^[[:alnum:].-]+\\.[[:space:]]+[0-9]+[[:space:]]+IN[[:space:]]+A[[:space:]]+' /tmp/adguard-drill.out | head -1 || true)"
|
||||||
|
else
|
||||||
|
dns_status="failed"
|
||||||
|
dns_detail="$(tail -20 /tmp/adguard-drill.out | tr '\n' ' ')"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$dns_status" = "failed" ]; then
|
||||||
|
echo "AdGuard DNS smoke failed: $dns_detail" >&2
|
||||||
|
docker logs --tail 80 restoretest-adguard >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# AdGuard Home Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`adguard\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Test container: \`restoretest-adguard\`
|
||||||
|
- Test HTTP endpoint: \`$TEST_HTTP/control/status\`
|
||||||
|
- Test DNS endpoint: \`127.0.0.1:$TEST_DNS_PORT\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract of conf: \`ok\`
|
||||||
|
- Restored config file: \`AdGuardHome.yaml\`
|
||||||
|
- Config check: \`$yaml_check\`
|
||||||
|
- Filter-list-like entries counted: \`$filter_count\`
|
||||||
|
- HTTP status from /control/status: \`$http_status\`
|
||||||
|
- DNS smoke: \`$dns_status\`
|
||||||
|
- DNS detail: \`$dns_detail\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Productive AdGuard DNS port 53 and admin port 8082 were NOT used.
|
||||||
|
- Test ports were bound to localhost only: \`127.0.0.1:15353\` and \`127.0.0.1:13001\`.
|
||||||
|
- Login credentials are part of the restored AdGuardHome.yaml and were not printed.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "AdGuard restore test ok -> $REPORT_FILE"
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
services:
|
||||||
|
restoretest-authelia-postgres:
|
||||||
|
# Gleiche Major-Version wie shared PostgreSQL 18 in Produktion.
|
||||||
|
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
||||||
|
container_name: restoretest-authelia-postgres
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_USER: authelia
|
||||||
|
POSTGRES_DB: authelia
|
||||||
|
POSTGRES_PASSWORD: restoretest-authelia-db
|
||||||
|
PGDATA: /var/lib/postgresql/18/docker
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/authelia/postgres:/var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U authelia -d authelia"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-authelia:
|
||||||
|
# Gleicher Image-Digest wie security/authelia/docker-compose.yml in Produktion.
|
||||||
|
image: authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b
|
||||||
|
container_name: restoretest-authelia
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
restoretest-authelia-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
command:
|
||||||
|
- authelia
|
||||||
|
- --config=/config/configuration.yml
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
# Wegwerf-Secrets nur fuer den isolierten Smoke. Niemals produktive
|
||||||
|
# Authelia-Secrets in diesem Compose verwenden. Die produktiven
|
||||||
|
# authelia_*_FILE-Mounts werden bewusst NICHT eingebunden.
|
||||||
|
AUTHELIA_SESSION_SECRET: restoretest-authelia-session-secret-placeholder-32
|
||||||
|
AUTHELIA_STORAGE_ENCRYPTION_KEY: restoretest-authelia-storage-enc-key-placeholder-32
|
||||||
|
AUTHELIA_STORAGE_POSTGRES_PASSWORD: restoretest-authelia-db
|
||||||
|
# server.address wird in der vom Skript erzeugten configuration.yml
|
||||||
|
# gesetzt (tcp://0.0.0.0:9091). Eine zusaetzliche ENV waere
|
||||||
|
# redundant - und in Authelia 4.39 nicht als Doppel-Underscore
|
||||||
|
# akzeptiert (war Ursache des "configuration environment variable
|
||||||
|
# not expected"-Warnings im Lauf 2026-06-03).
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/authelia/test-config:/config
|
||||||
|
ports:
|
||||||
|
# nur 127.0.0.1, keine Public-Route, keine Traefik-Labels
|
||||||
|
- "127.0.0.1:19091:9091"
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Authelia Restore Test Plan
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
|
||||||
|
Nachweisen, dass die Authelia-Konfiguration aus dem produktiven Borg-Archiv in einer isolierten Testumgebung wieder lauffaehig ist und der HTTP-Health-Endpunkt antwortet, ohne dass dabei produktive Secrets, produktives Postgres oder produktiver SMTP-Versand beruehrt werden.
|
||||||
|
|
||||||
|
Bewusst **nicht** Teil dieses Tests:
|
||||||
|
|
||||||
|
- Restore mit produktiven Authelia-Secrets. Der Test nutzt ausschliesslich Wegwerf-Werte fuer `AUTHELIA_SESSION_SECRET`, `AUTHELIA_STORAGE_ENCRYPTION_KEY` und `AUTHELIA_STORAGE_POSTGRES_PASSWORD`. SMTP- und Legacy-JWT-Env-Werte werden bewusst nicht gesetzt, damit Authelia keinen `notifier.smtp`-Block oder deprecated `jwt_secret` aus Env erzeugt.
|
||||||
|
- SMTP-Realanruf an GMX. Die minimale Test-Konfiguration setzt nur den Filesystem-Notifier.
|
||||||
|
- Forward-Auth gegen Traefik. Test laeuft nur auf `127.0.0.1:19091`, keine Traefik-Route.
|
||||||
|
- WebAuthn-/Duo-/OIDC-Identity-Provider-Endpunkte. Smoke prueft `/api/health`.
|
||||||
|
- **pg_restore des produktiven `postgresql17-authelia.dump`**. Authelia verschluesselt Storage-Werte mit `AUTHELIA_STORAGE_ENCRYPTION_KEY`. Ein Restore mit produktiven Daten in eine Test-Instanz mit Wegwerf-Key schlaegt im Startup-Check **by design** fehl ("the configured encryption key does not appear to be valid for this database"). Frische des produktiven Dumps wird ueber `check-restore-freshness.sh` ueberwacht; Daten-Decrypt-Drill ist eine separate DR-Aufgabe und braucht eine eigene Sicherheits-Choreographie mit kontrollierter Schluessel-Verwendung. Beobachtet im Erstlauf 2026-06-03 (Commit-Reihe `cacf77b..8d71dfb`); seit dem 2026-06-03-Folgecommit ist der Dump-Restore explizit aus dem Smoke entfernt.
|
||||||
|
|
||||||
|
## Quelle
|
||||||
|
|
||||||
|
- Backup-Quelle: produktives Borg-Archiv (`hetzner_borg_appdata_critical`)
|
||||||
|
- fachlich relevante Pfade im Archiv:
|
||||||
|
- `local/appdata/authelia/config` (verpflichtend)
|
||||||
|
- `local/borg-dumps/latest/postgresql17-authelia.dump` (existiert ggf. im Archiv; wird vom Smoke bewusst NICHT eingespielt, siehe oben)
|
||||||
|
- produktive Secrets unter `/mnt/user/appdata/secrets/authelia_*.txt` werden **nicht** gemountet
|
||||||
|
|
||||||
|
## Test-Ziel
|
||||||
|
|
||||||
|
- Restore-Lab: `/mnt/user/backups/restore-lab/authelia`
|
||||||
|
- Testdatenpfade:
|
||||||
|
- `/mnt/user/backups/restore-lab/authelia/config` (restaurierte Originalkonfiguration + `configuration.yml.original`)
|
||||||
|
- `/mnt/user/backups/restore-lab/authelia/test-config` (Runtime-Mount mit minimaler Test-`configuration.yml`)
|
||||||
|
- `/mnt/user/backups/restore-lab/authelia/postgres` (Test-Postgres-Datadir)
|
||||||
|
- `/mnt/user/backups/restore-lab/authelia/dumps/latest/postgresql17-authelia.dump` (falls extrahiert)
|
||||||
|
- `/mnt/user/backups/restore-lab/authelia/test-config/notifier/notifications.txt` (Filesystem-Notifier-Ausgabe)
|
||||||
|
- Testcontainer:
|
||||||
|
- `restoretest-authelia` (Image-Pin wie Produktion)
|
||||||
|
- `restoretest-authelia-postgres` (postgres:18.4, gleiche Major wie shared Postgres)
|
||||||
|
- Testport: `127.0.0.1:19091:9091`
|
||||||
|
- Report-Ziel: `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md`
|
||||||
|
|
||||||
|
## Schutzregeln
|
||||||
|
|
||||||
|
- produktive Pfade `/mnt/user/appdata/authelia/*` werden **nicht** beschrieben
|
||||||
|
- produktive Secret-Dateien `/mnt/user/appdata/secrets/authelia_*.txt` werden **nicht** gemountet
|
||||||
|
- produktive shared PostgreSQL 18 wird **nicht** angesprochen (`test-config/configuration.yml` definiert nur Test-Postgres)
|
||||||
|
- echter SMTP-Versand wird **nicht** ausgeloest (`test-config/configuration.yml` definiert nur Filesystem-Notifier)
|
||||||
|
- produktive Domain `auth.kaleschke.info` wird **nicht** uebernommen
|
||||||
|
- Testcontainer publishen nur auf `127.0.0.1`, keine LAN-/Tailscale-Bindung
|
||||||
|
- Borg-Passphrase wird aus `/mnt/user/appdata/secrets/borg_repo_passphrase.txt` gelesen und nirgendwo geloggt
|
||||||
|
|
||||||
|
## Geplanter Ablauf
|
||||||
|
|
||||||
|
1. Restore-Lab-Pfade leer anlegen
|
||||||
|
2. `local/appdata/authelia/config` aus dem aktuellsten Borg-Archiv extrahieren
|
||||||
|
3. minimale `test-config/configuration.yml` erzeugen; restaurierte Begleitdateien wie `users_database.yml` bleiben im Runtime-Mount, produktive externe Abhaengigkeiten werden nicht uebernommen; `notifier` auf Filesystem, `ntp.disable_startup_check: true`, `storage` auf Test-Postgres
|
||||||
|
4. Test-Postgres mit `ops/restore-tests/authelia-compose.test.yml` **frisch** hochfahren (keine Daten aus Dump - siehe Encryption-Key-Begruendung oben)
|
||||||
|
5. `authelia config validate` gegen `test-config/configuration.yml` laufen lassen
|
||||||
|
6. `restoretest-authelia` starten und HTTP-Health `http://127.0.0.1:19091/api/health` pollen
|
||||||
|
7. Report unter `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md` schreiben
|
||||||
|
8. Testcontainer stoppen und Restore-Lab bereinigen (`--keep-data` ueberschreibt)
|
||||||
|
|
||||||
|
## Smoke-Test
|
||||||
|
|
||||||
|
Minimal erfolgreich:
|
||||||
|
|
||||||
|
- Borg-Extract der Authelia-Config gelingt
|
||||||
|
- Test-Postgres startet `healthy`
|
||||||
|
- `authelia config validate` laeuft ohne Fehler durch
|
||||||
|
- HTTP `200` auf `/api/health` innerhalb 120 s
|
||||||
|
|
||||||
|
Optional spaeter:
|
||||||
|
|
||||||
|
- vollstaendigen Auth-Flow gegen Test-User aus `users_database.yml` durchspielen
|
||||||
|
- WebAuthn-Endpunkt /api/secondfactor/webauthn pruefen
|
||||||
|
- ForwardAuth-Pfad gegen Mock-Backend testen
|
||||||
|
|
||||||
|
## Bekannte Komplikationen
|
||||||
|
|
||||||
|
| Risiko | Beschreibung | Mitigation |
|
||||||
|
|---|---|---|
|
||||||
|
| Testkonfig-Schema-Drift | Authelia erwartet nach Upgrade andere Keys in der Minimal-Konfig | bei `config validate`-Fehler Test-Block im Skript anpassen |
|
||||||
|
| SMTP-Startup-Check blockiert Start | Wenn Authelia trotz `disable_startup_check` SMTP probiert | Container-Logs lesen, ggf. Notifier-Block weiter haerten |
|
||||||
|
| NTP-Lookup im Test-Netz | Container hat keinen DNS-Resolver fuer `time.cloudflare.com` | im Smoke per `ntp.disable_startup_check: true` deaktiviert |
|
||||||
|
| Storage-Encryption-Key vs. Dump | siehe "Bewusst nicht Teil dieses Tests" - der Smoke laeuft FRISCH ohne Dump | by design - Daten-Decrypt-Drill ist separate Aufgabe |
|
||||||
|
| identity_validation Schema-Drift | Aelteres/neueres Authelia-Schema erwartet andere Keys | Validate-Config Output lesen, ggf. Test-Block anpassen |
|
||||||
|
| users_database.yml mit produktiven Hashes | Daten werden ins Restore-Lab kopiert, aber niemals gemountet auf produktive Domain | OK; Testpfad ist isoliert, kein Browser-Zugang ueber LAN |
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- Skript- und Compose-Scaffold abgelegt am 2026-06-02
|
||||||
|
- Erstlauf am 2026-06-03 erfolgreich: Config aus Borg, minimale Test-Konfiguration, frisches Test-Postgres, HTTP `/api/health` `200`, Report `/mnt/user/backups/restore-reports/authelia-2026-06-03.md`
|
||||||
|
- Fuer die Rotation vorgesehen: zweiter Samstag in geraden Monaten, 07:30
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Authelia Restore Smoke Test
|
||||||
|
#
|
||||||
|
# Nicht-destruktiver Restore-Smoke-Test fuer Authelia.
|
||||||
|
#
|
||||||
|
# Was dieser Smoke nachweist:
|
||||||
|
# - Authelia-Config kann aus dem produktiven Borg-Archiv extrahiert werden
|
||||||
|
# - die restaurierten Begleitdateien (users_database.yml etc.) sind lesbar
|
||||||
|
# - eine minimale Test-Konfiguration, die diese Begleitdateien nutzt und
|
||||||
|
# produktive externe Abhaengigkeiten (Postgres/SMTP) durch Wegwerf-Backends
|
||||||
|
# ersetzt, ist gegen den produktiven Authelia-Image-Pin valide
|
||||||
|
# (`authelia config validate`)
|
||||||
|
# - Authelia startet damit gegen ein frisches Test-Postgres und antwortet
|
||||||
|
# auf `/api/health`
|
||||||
|
#
|
||||||
|
# Was dieser Smoke bewusst NICHT nachweist:
|
||||||
|
# - Daten-Restore des produktiven authelia.dump. Authelia verschluesselt
|
||||||
|
# Storage-Werte mit AUTHELIA_STORAGE_ENCRYPTION_KEY; ein Restore mit
|
||||||
|
# produktiven Daten in eine Test-Instanz mit Wegwerf-Encryption-Key
|
||||||
|
# schlaegt im Startup-Check fehl ("the configured encryption key does
|
||||||
|
# not appear to be valid for this database"). Daten-Decrypt ist eine
|
||||||
|
# eigene DR-Aufgabe mit kontrollierter Schluessel-Verwendung, nicht
|
||||||
|
# Teil dieses Smokes. Frische des Dumps wird ueber
|
||||||
|
# check-restore-freshness.sh ueberwacht.
|
||||||
|
# - vollstaendiger Login-/2FA-/ForwardAuth-Flow.
|
||||||
|
#
|
||||||
|
# Produktive Authelia-Container, produktive Postgres-DB, produktive Secrets
|
||||||
|
# und produktiver SMTP-Versand 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/authelia"
|
||||||
|
RESTORED_CONFIG_DIR="$RESTORE_ROOT/config"
|
||||||
|
TEST_CONFIG_DIR="$RESTORE_ROOT/test-config"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/authelia-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/authelia-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/authelia-$(date +%F).md"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Authelia restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
ReportRoot: $REPORT_ROOT
|
||||||
|
Expected Borg source paths:
|
||||||
|
- local/appdata/authelia/config
|
||||||
|
Planned isolation:
|
||||||
|
- Test-Postgres: postgres:18.4 mit Wegwerf-Credentials, FRISCH
|
||||||
|
- Test-Authelia: authelia/authelia:4.39.20 (Image-Pin wie Produktion)
|
||||||
|
- Wegwerf-Secrets ausschliesslich im Test-Compose
|
||||||
|
- test-config/configuration.yml wird im Restore-Lab erzeugt:
|
||||||
|
* storage -> Test-Postgres (kein produktives Postgres erreicht)
|
||||||
|
* notifier -> Filesystem (KEIN SMTP-Versand)
|
||||||
|
* session -> lokaler Smoke ohne produktive Session-Secrets
|
||||||
|
* ntp -> disable_startup_check (kein DNS im isolierten Test-Netz)
|
||||||
|
- Test endpoint: 127.0.0.1:19091/api/health (no Traefik, no public domain)
|
||||||
|
|
||||||
|
Bewusst NICHT Teil dieses Smokes:
|
||||||
|
- pg_restore von postgresql17-authelia.dump. Authelia verschluesselt
|
||||||
|
Storage-Werte mit AUTHELIA_STORAGE_ENCRYPTION_KEY; ein Restore in eine
|
||||||
|
Test-Instanz mit Wegwerf-Key ist by design nicht boot-faehig.
|
||||||
|
Dump-Frische wird via check-restore-freshness.sh ueberwacht.
|
||||||
|
|
||||||
|
Smoke-Test:
|
||||||
|
- authelia config validate gegen test-config/configuration.yml
|
||||||
|
- HTTP 200 von /api/health
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "authelia" "$RESTORE_ROOT"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORED_CONFIG_DIR" "$TEST_CONFIG_DIR" "$RESTORE_ROOT/postgres"
|
||||||
|
|
||||||
|
archive="$(latest_archive_name)"
|
||||||
|
repo="$(borg_repo_url)"
|
||||||
|
|
||||||
|
if [ -z "$archive" ] || [ -z "$repo" ]; then
|
||||||
|
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 1: Config aus Borg extrahieren
|
||||||
|
borg_extract "/restore/authelia-extract" "local/appdata/authelia/config"
|
||||||
|
if [ ! -d "$EXTRACT_DIR/local/appdata/authelia/config" ]; then
|
||||||
|
echo "Authelia config path missing in Borg archive" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp -a "$EXTRACT_DIR/local/appdata/authelia/config/." "$RESTORED_CONFIG_DIR/"
|
||||||
|
|
||||||
|
# Stufe 2: Minimale Test-Konfiguration erzeugen.
|
||||||
|
# Die restaurierte Originalkonfig bleibt als Diagnosematerial erhalten. Der
|
||||||
|
# Smoke nutzt bewusst eine neu geschriebene Test-Config, damit keine produktiven
|
||||||
|
# Blocks (SMTP, echtes Postgres, Session/JWT-Altkeys) hineinmergen koennen.
|
||||||
|
ORIGINAL_CONFIG_FILE="$RESTORED_CONFIG_DIR/configuration.yml"
|
||||||
|
TEST_CONFIG_FILE="$TEST_CONFIG_DIR/configuration.yml"
|
||||||
|
if [ ! -f "$ORIGINAL_CONFIG_FILE" ]; then
|
||||||
|
echo "configuration.yml missing in restored config dir" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Kopiere alle Begleitdateien (z. B. users_database.yml) in einen separaten
|
||||||
|
# Runtime-Mount. configuration.yml wird danach vollstaendig neu geschrieben.
|
||||||
|
cp -a "$RESTORED_CONFIG_DIR/." "$TEST_CONFIG_DIR/"
|
||||||
|
cp "$ORIGINAL_CONFIG_FILE" "$RESTORED_CONFIG_DIR/configuration.yml.original"
|
||||||
|
|
||||||
|
cat > "$TEST_CONFIG_FILE" <<'YAML'
|
||||||
|
---
|
||||||
|
# Minimal-Konfiguration nur fuer den Restore-Smoke.
|
||||||
|
|
||||||
|
theme: dark
|
||||||
|
|
||||||
|
server:
|
||||||
|
address: tcp://0.0.0.0:9091
|
||||||
|
|
||||||
|
log:
|
||||||
|
level: info
|
||||||
|
|
||||||
|
authentication_backend:
|
||||||
|
file:
|
||||||
|
path: /config/users_database.yml
|
||||||
|
password:
|
||||||
|
algorithm: argon2id
|
||||||
|
iterations: 3
|
||||||
|
key_length: 32
|
||||||
|
salt_length: 16
|
||||||
|
memory: 65536
|
||||||
|
parallelism: 4
|
||||||
|
|
||||||
|
access_control:
|
||||||
|
# Authelia 4.39 verlangt: wenn KEINE Regeln gesetzt sind, muss default_policy
|
||||||
|
# 'two_factor' oder 'one_factor' sein. 'bypass' ist als Default-Policy ohne
|
||||||
|
# explizite Regeln nicht erlaubt. Fuer den Smoke ist das egal: /api/health
|
||||||
|
# ist ein public Endpunkt und laeuft nicht durch access_control.
|
||||||
|
default_policy: two_factor
|
||||||
|
|
||||||
|
regulation:
|
||||||
|
max_retries: 3
|
||||||
|
find_time: 2m
|
||||||
|
ban_time: 5m
|
||||||
|
|
||||||
|
totp:
|
||||||
|
issuer: kaleschke.info
|
||||||
|
period: 30
|
||||||
|
skew: 1
|
||||||
|
|
||||||
|
storage:
|
||||||
|
postgres:
|
||||||
|
address: tcp://restoretest-authelia-postgres:5432
|
||||||
|
database: authelia
|
||||||
|
username: authelia
|
||||||
|
# Passwort kommt ueber AUTHELIA_STORAGE_POSTGRES_PASSWORD ENV.
|
||||||
|
|
||||||
|
notifier:
|
||||||
|
disable_startup_check: true
|
||||||
|
filesystem:
|
||||||
|
filename: /config/notifier/notifications.txt
|
||||||
|
|
||||||
|
ntp:
|
||||||
|
# Test-Netz hat keinen DNS-Resolver fuer time.cloudflare.com; ohne diesen
|
||||||
|
# Schalter loggt Authelia "Could not determine the clock offset" und der
|
||||||
|
# Startup-Check kann fehlschlagen.
|
||||||
|
disable_startup_check: true
|
||||||
|
|
||||||
|
session:
|
||||||
|
cookies:
|
||||||
|
- name: authelia_session_restoretest
|
||||||
|
domain: kaleschke.info
|
||||||
|
authelia_url: https://auth.kaleschke.info
|
||||||
|
default_redirection_url: https://glance.kaleschke.info
|
||||||
|
expiration: 1h
|
||||||
|
inactivity: 5m
|
||||||
|
|
||||||
|
identity_validation:
|
||||||
|
reset_password:
|
||||||
|
jwt_secret: restoretest-authelia-reset-password-jwt-secret-placeholder-64bytes
|
||||||
|
jwt_lifespan: 5m
|
||||||
|
jwt_algorithm: HS256
|
||||||
|
YAML
|
||||||
|
|
||||||
|
mkdir -p "$TEST_CONFIG_DIR/notifier"
|
||||||
|
chmod -R a+rwX "$TEST_CONFIG_DIR/notifier"
|
||||||
|
|
||||||
|
# Stufe 3: Test-Postgres hochfahren (FRISCH, keine Daten aus Dump).
|
||||||
|
# Authelia legt sein Schema beim ersten Start selbst an und schreibt eine
|
||||||
|
# Encryption-Probe mit AUTHELIA_STORAGE_ENCRYPTION_KEY. Ein Restore des
|
||||||
|
# produktiven authelia.dump in diese Instanz wuerde die Encryption-Probe
|
||||||
|
# mit einem anderen Key vorbelegen und Authelia beim Startup-Check
|
||||||
|
# ablehnen lassen ("the configured encryption key does not appear to be
|
||||||
|
# valid for this database"). Genau aus diesem Grund laeuft der Smoke
|
||||||
|
# bewusst auf einer leeren DB. Frische des produktiven Dumps wird
|
||||||
|
# separat in check-restore-freshness.sh ueberwacht.
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-authelia-postgres >/dev/null
|
||||||
|
until docker exec restoretest-authelia-postgres pg_isready -U authelia -d authelia >/dev/null 2>&1; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Stufe 4: config validate im Container-Kontext, gegen minimale Test-Config
|
||||||
|
validate_status="ok"
|
||||||
|
if ! docker run --rm \
|
||||||
|
-e AUTHELIA_SESSION_SECRET=restoretest-authelia-session-secret-placeholder-32 \
|
||||||
|
-e AUTHELIA_STORAGE_ENCRYPTION_KEY=restoretest-authelia-storage-enc-key-placeholder-32 \
|
||||||
|
-e AUTHELIA_STORAGE_POSTGRES_PASSWORD=restoretest-authelia-db \
|
||||||
|
-v "$TEST_CONFIG_DIR:/config" \
|
||||||
|
authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b \
|
||||||
|
authelia config validate --config /config/configuration.yml \
|
||||||
|
>/tmp/authelia-validate.log 2>&1; then
|
||||||
|
validate_status="failed"
|
||||||
|
cat /tmp/authelia-validate.log >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 5: Authelia-Container starten. Das Compose nutzt test-config als
|
||||||
|
# /config-Mount mit isolierten Test-Backends.
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-authelia >/dev/null
|
||||||
|
|
||||||
|
http_status=""
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
http_status="$(curl -s -o /tmp/authelia-body.html -w '%{http_code}' \
|
||||||
|
http://127.0.0.1:19091/api/health || true)"
|
||||||
|
if [ "$http_status" = "200" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$http_status" != "200" ]; then
|
||||||
|
echo "Authelia HTTP health failed: status=$http_status" >&2
|
||||||
|
docker logs --tail 120 restoretest-authelia >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Authelia Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`authelia\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Test containers:
|
||||||
|
- \`restoretest-authelia\`
|
||||||
|
- \`restoretest-authelia-postgres\` (fresh schema, no productive dump)
|
||||||
|
- Test endpoint: \`http://127.0.0.1:19091/api/health\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract of config: \`ok\`
|
||||||
|
- configuration.yml present in archive: \`ok\`
|
||||||
|
- test runtime configuration.yml written: \`ok\`
|
||||||
|
- \`authelia config validate\`: \`$validate_status\`
|
||||||
|
- HTTP /api/health status: \`$http_status\`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Dieser Smoke prueft: Borg-Restore der Config, Validate gegen Produktions-Image,
|
||||||
|
Authelia-Boot gegen frische Test-Postgres + Wegwerf-Encryption-Key,
|
||||||
|
HTTP-Health-Endpoint antwortet.
|
||||||
|
|
||||||
|
Bewusst NICHT Teil des Smokes: pg_restore des produktiven authelia.dump.
|
||||||
|
Authelia verschluesselt Storage-Werte mit \`AUTHELIA_STORAGE_ENCRYPTION_KEY\`;
|
||||||
|
ein Restore mit produktiven Daten in eine Test-Instanz mit Wegwerf-Key
|
||||||
|
schlaegt im Startup-Check by design fehl. Frische des produktiven Dumps
|
||||||
|
wird in \`check-restore-freshness.sh\` ueberwacht; Daten-Decrypt-Drill ist
|
||||||
|
eine separate DR-Aufgabe.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Test ran without Traefik and without the productive domain \`auth.kaleschke.info\`.
|
||||||
|
- Productive Authelia secrets under \`/mnt/user/appdata/secrets/authelia_*.txt\` were NOT mounted.
|
||||||
|
- Notifier was forced to filesystem (\`/config/notifier/notifications.txt\`); no SMTP call to GMX.
|
||||||
|
- Storage forced to isolated test postgres; productive shared PostgreSQL 18 was NOT touched.
|
||||||
|
- NTP startup-check disabled in test config (kein DNS-Resolver im isolierten Compose-Netz).
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "Authelia restore test ok -> $REPORT_FILE"
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Authelia Restore Runbook
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Skript und Test-Compose sind validiert. **Erstlauf 2026-06-03 erfolgreich**: Config aus Borg extrahiert, minimale Test-Konfiguration validiert, frisches Test-Postgres gestartet, HTTP `/api/health` `200`. Report: `/mnt/user/backups/restore-reports/authelia-2026-06-03.md`. Authelia ist Tier-1-kritisch, deshalb bleibt dieser Test bewusst konservativ: Smoke-Test prueft nur Config-Validate + HTTP-Health, kein vollstaendiger Auth-Flow und kein produktiver Dump-Restore.
|
||||||
|
|
||||||
|
## Vorbedingungen
|
||||||
|
|
||||||
|
- Borg-Quelle ist verfuegbar
|
||||||
|
- `borg-ui`-Container laeuft
|
||||||
|
- Borg-Passphrase-Datei vorhanden: `/mnt/user/appdata/secrets/borg_repo_passphrase.txt`
|
||||||
|
- `borg-ui` mountet die Passphrase im Container als `/local/secrets/borg_repo_passphrase.txt`
|
||||||
|
- aktuelles Borg-Archiv enthaelt `local/appdata/authelia/config`
|
||||||
|
- optional: `local/borg-dumps/latest/postgresql17-authelia.dump`
|
||||||
|
- Testpfade unter `/mnt/user/backups/restore-lab/` und `/mnt/user/backups/restore-reports/` sind freigegeben
|
||||||
|
- Port `127.0.0.1:19091` frei
|
||||||
|
- freier Speicher unter `/mnt/user/backups/restore-lab/authelia` (~200 MB reichen)
|
||||||
|
|
||||||
|
## Bestaetigter Host-Stand (Soll)
|
||||||
|
|
||||||
|
- produktiver Authelia-Container: `authelia` mit Image `authelia/authelia:4.39.20@sha256:1b363e9279e742397966333f364e0876ae02bf5c876de73e83af6d48c57ff51b`
|
||||||
|
- produktiver Config-Pfad: `/mnt/user/appdata/authelia/config`
|
||||||
|
- produktive Secrets: `/mnt/user/appdata/secrets/authelia_*.txt` (werden vom Test **nicht** gebraucht)
|
||||||
|
- produktive Storage: shared PostgreSQL 18 (wird vom Test **nicht** angesprochen)
|
||||||
|
|
||||||
|
## Erster Lauf - trockene Variante
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/authelia-restore-test.sh --what-if
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartete Ausgabe: nur Plan-Output, kein Docker-Start, kein Borg-Extract.
|
||||||
|
|
||||||
|
## Erster Lauf - echter Test (Operator-freigegeben)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash /mnt/user/services/homelab-infra/ops/restore-tests/authelia-restore-test.sh --keep-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Bei Erfolg:
|
||||||
|
|
||||||
|
- Report unter `/mnt/user/backups/restore-reports/authelia-YYYY-MM-DD.md`
|
||||||
|
- Restore-Lab-Daten bleiben mit `--keep-data` erhalten
|
||||||
|
- ohne `--keep-data` wird das Restore-Lab geloescht; bei Fehler wird es nach `/mnt/user/backups/restore-lab/_failed/authelia-...` verschoben
|
||||||
|
|
||||||
|
## Smoke-Test-Pruefungen
|
||||||
|
|
||||||
|
Minimal erwartet im Report:
|
||||||
|
|
||||||
|
- Borg extract of config: `ok`
|
||||||
|
- Test-Postgres healthy
|
||||||
|
- `authelia config validate`: `ok`
|
||||||
|
- HTTP /api/health status: `200`
|
||||||
|
|
||||||
|
## Fehlerfaelle
|
||||||
|
|
||||||
|
| Symptom | Ursache | Massnahme |
|
||||||
|
|---|---|---|
|
||||||
|
| `config validate` failt mit `notifier` Block | Testkonfig enthaelt mehr als einen Notifier | `test-config/configuration.yml` pruefen; Minimal-Test-Block im Skript anpassen |
|
||||||
|
| `config validate` failt mit `session.domain` | aelteres/neueres Schema | Test-`session:`-Block an reales Authelia-Schema anpassen |
|
||||||
|
| `config validate` failt mit `access_control` default_policy | Authelia >=4.39 verlangt ohne Rules `two_factor`/`one_factor` | Test-Block ist bereits auf `two_factor` gesetzt; bei weiterer Schema-Aenderung anpassen |
|
||||||
|
| HTTP-Timeout 120 s | Authelia haengt in Postgres-Schema-Migration | `docker logs --tail 200 restoretest-authelia` lesen, ggf. Wartezeit erhoehen |
|
||||||
|
| `encryption key does not appear to be valid for this database` | jemand hat `pg_restore` des produktiven Dumps wieder eingebaut | `pg_restore` ist seit `2026-06-03` bewusst NICHT mehr Teil dieses Smokes - siehe Plan/Skript-Doku; nicht re-aktivieren ohne kontrollierte Encryption-Key-Choreographie |
|
||||||
|
| SMTP-Connect im Log | Testkonfig oder Env erzeugt unerwartet SMTP | `test-config/configuration.yml` und `AUTHELIA_*SMTP*` Env pruefen |
|
||||||
|
| `Could not determine the clock offset` | DNS-Lookup `time.cloudflare.com` failt im isolierten Test-Netz | `ntp.disable_startup_check: true` ist im Test-Config-Block bereits gesetzt; bei Aenderung beibehalten |
|
||||||
|
| `configuration environment variable not expected: AUTHELIA__SERVER__ADDRESS` | Doppel-Underscore ENV im Compose | seit `2026-06-03` entfernt; `server.address` kommt aus configuration.yml |
|
||||||
|
|
||||||
|
## Cleanup
|
||||||
|
|
||||||
|
- bei Erfolg ohne `--keep-data`: `rm -rf /mnt/user/backups/restore-lab/authelia` und Extract-Cache
|
||||||
|
- bei Fehler: Datenpfad wird via `preserve_on_failure` nach `/mnt/user/backups/restore-lab/_failed/authelia-...` umbenannt
|
||||||
|
|
||||||
|
Produktive Authelia-Container, produktive Secrets, produktive Postgres-DB und produktiver SMTP-Account werden niemals beruehrt.
|
||||||
|
|
||||||
|
## Schedule
|
||||||
|
|
||||||
|
Empfohlener Schedule nach erfolgreichem Erstlauf: zweimonatlich (2. Samstag in geraden Monaten), damit nicht mit Paperless kollidierend.
|
||||||
|
|
||||||
|
## Festgelegte Entscheidungen
|
||||||
|
|
||||||
|
- Test-Compose nutzt denselben Image-Digest wie Produktion.
|
||||||
|
- Wegwerf-Secrets ausschliesslich im Test-Compose; niemals produktive Authelia-Secrets einsetzen.
|
||||||
|
- Test-Postgres ist isoliert; produktive shared PostgreSQL 18 wird nicht angesprochen.
|
||||||
|
- Notifier wird auf Filesystem umgebogen; KEIN echter SMTP-Versand.
|
||||||
|
- Test-Port nur auf `127.0.0.1:19091`, keine LAN-/Traefik-Anbindung.
|
||||||
|
- Borg-Passphrase wird aus Host-Secret-Datei gelesen und nirgendwo geloggt.
|
||||||
@@ -25,6 +25,69 @@ check_file_age_days() {
|
|||||||
echo $(( (now_epoch - mtime) / 86400 ))
|
echo $(( (now_epoch - mtime) / 86400 ))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# pg_restore --list als billiger Header-Check fuer Custom-Format-Dumps;
|
||||||
|
# erkennt Korruption, die mit reinem "exists+nonempty" durchrutscht. Wir
|
||||||
|
# brauchen kein laufendes Postgres; der Check liest nur die Toc-Section.
|
||||||
|
PG_DUMPS="postgresql17-paperless.dump postgresql17-mailarchiver.dump postgresql17-authelia.dump mealie.dump immich.dump nextcloud.dump"
|
||||||
|
is_pg_custom_dump() {
|
||||||
|
case " $PG_DUMPS " in *" $1 "*) return 0;; *) return 1;; esac
|
||||||
|
}
|
||||||
|
|
||||||
|
pg_header_ok() {
|
||||||
|
local path="$1"
|
||||||
|
if ! command -v pg_restore >/dev/null 2>&1; then
|
||||||
|
# ohne Host-pg_restore: in laufendem Postgres-Container probieren
|
||||||
|
if command -v docker >/dev/null 2>&1 && docker inspect postgresql17 >/dev/null 2>&1; then
|
||||||
|
if docker exec -i postgresql17 pg_restore --list < "$path" >/dev/null 2>&1; then
|
||||||
|
return 0 # Header valide
|
||||||
|
else
|
||||||
|
return 1 # Header korrupt
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 2 # nicht pruefbar (kein pg_restore, kein Container)
|
||||||
|
fi
|
||||||
|
pg_restore --list "$path" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
check_pg_header() {
|
||||||
|
local dump="$1"
|
||||||
|
local path="$2"
|
||||||
|
local age="$3"
|
||||||
|
local missing_mode="${4:-critical}"
|
||||||
|
|
||||||
|
if [ ! -f "$path" ]; then
|
||||||
|
if [ "$missing_mode" = "optional" ]; then
|
||||||
|
info+=("DUMP_OPTIONAL_MISSING $dump")
|
||||||
|
else
|
||||||
|
critical+=("DUMP_MISSING $dump")
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ ! -s "$path" ]; then
|
||||||
|
critical+=("DUMP_EMPTY $dump")
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
||||||
|
if [ "$missing_mode" = "optional" ]; then
|
||||||
|
warnings+=("DUMP_OPTIONAL_STALE $dump age=${age}h")
|
||||||
|
else
|
||||||
|
critical+=("DUMP_STALE $dump age=${age}h")
|
||||||
|
fi
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if pg_header_ok "$path"; then
|
||||||
|
rc=0
|
||||||
|
else
|
||||||
|
rc=$?
|
||||||
|
fi
|
||||||
|
case "$rc" in
|
||||||
|
0) info+=("DUMP_OK $dump age=${age}h header=ok") ;;
|
||||||
|
1) critical+=("DUMP_HEADER_INVALID $dump (pg_restore --list failed)") ;;
|
||||||
|
2) info+=("DUMP_OK $dump age=${age}h header=unchecked") ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
for dump in \
|
for dump in \
|
||||||
postgresql17-paperless.dump \
|
postgresql17-paperless.dump \
|
||||||
postgresql17-mailarchiver.dump \
|
postgresql17-mailarchiver.dump \
|
||||||
@@ -48,11 +111,24 @@ for dump in \
|
|||||||
age="$(check_file_age_hours "$path")"
|
age="$(check_file_age_hours "$path")"
|
||||||
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
if [ "$age" -gt "$MAX_DUMP_AGE_HOURS" ]; then
|
||||||
critical+=("DUMP_STALE $dump age=${age}h")
|
critical+=("DUMP_STALE $dump age=${age}h")
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
if is_pg_custom_dump "$dump"; then
|
||||||
|
check_pg_header "$dump" "$path" "$age"
|
||||||
else
|
else
|
||||||
info+=("DUMP_OK $dump age=${age}h")
|
info+=("DUMP_OK $dump age=${age}h")
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
optional_dump="postgresql17-authelia.dump"
|
||||||
|
optional_path="$DUMP_ROOT/$optional_dump"
|
||||||
|
optional_age=0
|
||||||
|
if [ -f "$optional_path" ]; then
|
||||||
|
optional_age="$(check_file_age_hours "$optional_path")"
|
||||||
|
fi
|
||||||
|
check_pg_header "$optional_dump" "$optional_path" "$optional_age" optional
|
||||||
|
|
||||||
for service in vaultwarden gitea paperless; do
|
for service in vaultwarden gitea paperless; do
|
||||||
if [ ! -d "$REPORT_ROOT" ]; then
|
if [ ! -d "$REPORT_ROOT" ]; then
|
||||||
warnings+=("REPORT_ROOT_MISSING $REPORT_ROOT")
|
warnings+=("REPORT_ROOT_MISSING $REPORT_ROOT")
|
||||||
|
|||||||
@@ -20,7 +20,28 @@ require_path() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
require_borg_container() {
|
||||||
|
docker inspect "$BORG_CONTAINER" >/dev/null 2>&1 || {
|
||||||
|
echo "Missing Borg container: $BORG_CONTAINER" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
[ "$(docker inspect -f '{{.State.Running}}' "$BORG_CONTAINER" 2>/dev/null)" = "true" ] || {
|
||||||
|
echo "Borg container is not running: $BORG_CONTAINER" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
docker exec "$BORG_CONTAINER" test -r /data/borg.db >/dev/null 2>&1 || {
|
||||||
|
echo "Missing borg-ui database in container: $BORG_CONTAINER:/data/borg.db" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
docker exec "$BORG_CONTAINER" test -r /local/secrets/borg_repo_passphrase.txt >/dev/null 2>&1 || {
|
||||||
|
echo "Missing Borg passphrase in container: $BORG_CONTAINER:/local/secrets/borg_repo_passphrase.txt" >&2
|
||||||
|
echo "Host path exists, but borg-ui must mount it as /local/secrets/borg_repo_passphrase.txt." >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
latest_archive_name() {
|
latest_archive_name() {
|
||||||
|
require_borg_container
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect('/data/borg.db')
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
@@ -34,6 +55,7 @@ PY
|
|||||||
}
|
}
|
||||||
|
|
||||||
borg_repo_url() {
|
borg_repo_url() {
|
||||||
|
require_borg_container
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - <<'PY'
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect('/data/borg.db')
|
conn = sqlite3.connect('/data/borg.db')
|
||||||
@@ -50,6 +72,7 @@ borg_extract() {
|
|||||||
local extract_dir="$1"
|
local extract_dir="$1"
|
||||||
shift
|
shift
|
||||||
local paths=("$@")
|
local paths=("$@")
|
||||||
|
require_borg_container
|
||||||
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
docker exec -i "$BORG_CONTAINER" python3 - "$extract_dir" "${paths[@]}" <<'PY'
|
||||||
import os, sys, subprocess
|
import os, sys, subprocess
|
||||||
extract_dir = sys.argv[1]
|
extract_dir = sys.argv[1]
|
||||||
@@ -88,3 +111,22 @@ cleanup_compose() {
|
|||||||
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
docker compose -f "$compose_file" down >/dev/null 2>&1 || true
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Hilfsfunktion: bei Fehler-Exit Restore-Lab-Pfad nicht loeschen, sondern in
|
||||||
|
# einen `_failed/<service>-<date>-<pid>`-Pfad umbenennen, damit Post-Mortem
|
||||||
|
# moeglich bleibt. Aufrufer setzt vor Erfolg `RESTORE_SUCCESS=1`.
|
||||||
|
RESTORE_FAILED_ROOT="${RESTORE_FAILED_ROOT:-/mnt/user/backups/restore-lab/_failed}"
|
||||||
|
preserve_on_failure() {
|
||||||
|
local service="$1"
|
||||||
|
local path="$2"
|
||||||
|
if [ ! -e "$path" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
mkdir -p "$RESTORE_FAILED_ROOT"
|
||||||
|
local target="$RESTORE_FAILED_ROOT/${service}-$(date +%F)-$$"
|
||||||
|
if mv "$path" "$target" 2>/dev/null; then
|
||||||
|
echo "preserved failed restore data: $target" >&2
|
||||||
|
else
|
||||||
|
echo "failed to preserve restore data: $path -> $target" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,8 +37,14 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "gitea" "$RESTORE_ROOT"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$DATA_DIR"
|
rm -rf "$DATA_DIR"
|
||||||
fi
|
fi
|
||||||
@@ -61,9 +67,9 @@ sleep 8
|
|||||||
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
|
status="$(curl -s -o /tmp/gitea-body.html -w '%{http_code}' http://127.0.0.1:13000)"
|
||||||
grep -qi "Gitea" /tmp/gitea-body.html
|
grep -qi "Gitea" /tmp/gitea-body.html
|
||||||
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
|
if timeout 5 bash -lc '</dev/tcp/127.0.0.1/12222' >/dev/null 2>&1; then
|
||||||
ssh_state="open"
|
ssh_state="tcp-open"
|
||||||
else
|
else
|
||||||
echo "Gitea SSH port not reachable" >&2
|
echo "Gitea SSH port not reachable (TCP connect failed)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -85,7 +91,7 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Borg extract into isolated restore-lab: \`ok\`
|
- Borg extract into isolated restore-lab: \`ok\`
|
||||||
- HTTP status: \`$status\`
|
- HTTP status: \`$status\`
|
||||||
- HTML content: \`Gitea\`
|
- HTML content: \`Gitea\`
|
||||||
- SSH port: \`$ssh_state\`
|
- SSH TCP port: \`$ssh_state\` (TCP connect only, not a full SSH handshake)
|
||||||
- Repository sample: \`$repo_sample\`
|
- Repository sample: \`$repo_sample\`
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
@@ -94,4 +100,5 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Gitea restore test ok -> $REPORT_FILE"
|
echo "Gitea restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Wenn das Archiv den Pfad anders ablegt, zuerst mit `borg list "$BORG_REPO" "::AR
|
|||||||
3. Testcontainer starten
|
3. Testcontainer starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml up -d
|
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/gitea-compose.test.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Smoke-Test
|
4. Smoke-Test
|
||||||
@@ -83,7 +83,7 @@ Minimal erfolgreich:
|
|||||||
5. Testcontainer wieder stoppen
|
5. Testcontainer wieder stoppen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab/ops/restore-tests/gitea-compose.test.yml down
|
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/gitea-compose.test.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Report schreiben
|
6. Report schreiben
|
||||||
|
|||||||
@@ -64,8 +64,14 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "immich" "$RESTORE_ROOT"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
fi
|
||||||
@@ -244,4 +250,5 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
|
- Restore-Quelle Dump: \`local/borg-dumps/latest/immich.dump\` aus aktuellem Borg-Archiv.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Immich restore test ok -> $REPORT_FILE"
|
echo "Immich restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ services:
|
|||||||
# Schreibt in den Restore-Lab-Pfad, NICHT in das produktive
|
# Schreibt in den Restore-Lab-Pfad, NICHT in das produktive
|
||||||
# /mnt/user/appdata/komodo/mongo-Volume.
|
# /mnt/user/appdata/komodo/mongo-Volume.
|
||||||
restoretest-komodo-mongo:
|
restoretest-komodo-mongo:
|
||||||
image: mongo:7.0.32@sha256:32979a1189dfdc44da3f5ed40d910495f5ad8f6f7f77556646f890a30b2d3f56
|
image: mongo:8.0.23@sha256:44aa79ae28ff80b56fe58681b66cda9336706df408a5175a6c04988aa54610d3
|
||||||
container_name: restoretest-komodo-mongo
|
container_name: restoretest-komodo-mongo
|
||||||
restart: "no"
|
restart: "no"
|
||||||
command: --quiet
|
command: --quiet
|
||||||
|
|||||||
@@ -53,8 +53,13 @@ fi
|
|||||||
require_cmd docker
|
require_cmd docker
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
|
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "komodo-bootstrap" "$RESTORE_ROOT"
|
||||||
|
return
|
||||||
|
fi
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
fi
|
||||||
@@ -132,4 +137,5 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Komodo bootstrap trockenlauf ok -> $REPORT_FILE"
|
echo "Komodo bootstrap trockenlauf ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
restoretest-komodo-mongorestore:
|
||||||
|
image: mongo:8.0.23@sha256:44aa79ae28ff80b56fe58681b66cda9336706df408a5175a6c04988aa54610d3
|
||||||
|
container_name: restoretest-komodo-mongorestore
|
||||||
|
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-restore/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
|
||||||
@@ -0,0 +1,163 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Komodo Mongo Daten-Restore Test
|
||||||
|
#
|
||||||
|
# Baut auf dem bestehenden Komodo-Bootstrap-Test auf und fuegt hinzu:
|
||||||
|
# - mongorestore von komodo-mongo.archive.gz in die Test-Mongo
|
||||||
|
# - Liest danach die stack-Collection, um zu pruefen, dass Komodo-
|
||||||
|
# Stack-Definitionen wiederhergestellt sind
|
||||||
|
#
|
||||||
|
# Das ist der Test, der im DR-Fall beweist, dass die KOMODO_*-Stack-
|
||||||
|
# ENV-Werte aus dem Mongo-Dump rekonstruiert werden koennen (die
|
||||||
|
# kanonische Quelle gemaess docs/DISASTER_RECOVERY.md 6.2.1).
|
||||||
|
#
|
||||||
|
# Produktive Komodo-Container und produktive Mongo-Datadir 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-mongo-restore"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/komodo-mongo-restore-compose.test.yml"
|
||||||
|
PROJECT_NAME="restoretest-komodo-mongorestore"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/komodo-mongo-restore-$(date +%F).md"
|
||||||
|
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/komodo-mongo.archive.gz"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Komodo Mongo Daten-Restore Test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
ReportRoot: $REPORT_ROOT
|
||||||
|
DumpPath: $DUMP_HOST_PATH
|
||||||
|
Planned steps:
|
||||||
|
1. Frische Test-Mongo hochfahren (gleiche Compose wie Bootstrap-Test)
|
||||||
|
2. mongorestore --archive --gzip aus $DUMP_HOST_PATH
|
||||||
|
3. Stack-Collection auslesen (Beweis: Stack-Definitionen sind da)
|
||||||
|
4. Report schreiben
|
||||||
|
5. Cleanup
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
require_path "$DUMP_HOST_PATH"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" down -v >/dev/null 2>&1 || true
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "komodo-mongo-restore" "$RESTORE_ROOT"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT/mongo" "$RESTORE_ROOT/core" "$RESTORE_ROOT/keys" "$RESTORE_ROOT/periphery"
|
||||||
|
|
||||||
|
# Stufe 1: Nur Test-Mongo starten (kein Core/Periphery noetig fuer Dump-Restore)
|
||||||
|
docker compose -f "$COMPOSE_FILE" -p "$PROJECT_NAME" up -d \
|
||||||
|
restoretest-komodo-mongorestore >/dev/null
|
||||||
|
|
||||||
|
mongo_ok=0
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
s="$(docker inspect restoretest-komodo-mongorestore --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-mongorestore >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 2: mongorestore aus dem Host-Dump
|
||||||
|
# --drop loescht existierende Collections vor dem Restore (frische DB, also harmlos).
|
||||||
|
# --gzip weil der Dump als .archive.gz erzeugt wurde.
|
||||||
|
# Auth mit den Wegwerf-Credentials aus dem Test-Compose.
|
||||||
|
restore_status="ok"
|
||||||
|
# --noIndexRestore: der Smoke prueft nur, dass Daten lesbar sind, nicht dass
|
||||||
|
# alle Indexe sauber aufgebaut werden. mongorestore scheitert sonst am
|
||||||
|
# Index-Rebuild weil der Test-User keine dbAdmin-Rolle hat. Fuer den
|
||||||
|
# DR-Nachweis (Stack-ENV-Werte lesbar) reicht das.
|
||||||
|
if ! docker exec -i restoretest-komodo-mongorestore \
|
||||||
|
mongorestore --archive --gzip --noIndexRestore \
|
||||||
|
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
|
||||||
|
--drop \
|
||||||
|
< "$DUMP_HOST_PATH" 2>/tmp/komodo-mongorestore.err; then
|
||||||
|
restore_status="failed"
|
||||||
|
cat /tmp/komodo-mongorestore.err >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 3: Stack-Collection auslesen
|
||||||
|
# Komodo speichert Stack-Definitionen in der DB "komodo", Collection "Stack"
|
||||||
|
# (oder "stack" je nach Version). Wir zaehlen Dokumente als Beweis.
|
||||||
|
stack_count="n/a"
|
||||||
|
for coll in Stack stack; do
|
||||||
|
count="$(docker exec restoretest-komodo-mongorestore mongosh --quiet \
|
||||||
|
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
|
||||||
|
--eval "db.getSiblingDB('komodo').getCollection('$coll').countDocuments({})" \
|
||||||
|
2>/dev/null | tr -d '[:space:]' || true)"
|
||||||
|
if [ -n "$count" ] && [ "$count" != "0" ] && [ "$count" != "n/a" ]; then
|
||||||
|
stack_count="$count (collection: $coll)"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Alle DBs + Collections auflisten als zusaetzlicher Nachweis
|
||||||
|
db_list="$(docker exec restoretest-komodo-mongorestore mongosh --quiet \
|
||||||
|
-u komodo -p restoretest-komodo-mongo-pwd --authenticationDatabase admin \
|
||||||
|
--eval "db.adminCommand({listDatabases:1}).databases.map(d=>d.name).join(', ')" \
|
||||||
|
2>/dev/null | tr -d '\r' || echo "n/a")"
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Komodo Mongo Daten-Restore Test - $(date +%F)
|
||||||
|
|
||||||
|
- Dump: \`$DUMP_HOST_PATH\`
|
||||||
|
- Dump size: \`$(ls -lh "$DUMP_HOST_PATH" | awk '{print $5}')\`
|
||||||
|
- Project: \`$PROJECT_NAME\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Test-Mongo healthy: \`ok\`
|
||||||
|
- mongorestore --archive --gzip: \`$restore_status\`
|
||||||
|
- Databases after restore: \`$db_list\`
|
||||||
|
- Stack documents: \`$stack_count\`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Dieser Test beweist, dass \`komodo-mongo.archive.gz\` in eine frische
|
||||||
|
Mongo-Instanz eingespielt werden kann und die Stack-Definitionen danach
|
||||||
|
lesbar sind. Im DR-Fall ist das die kanonische Quelle fuer
|
||||||
|
\`KOMODO_*\`-Stack-ENV-Werte (docs/DISASTER_RECOVERY.md 6.2.1).
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Produktive Komodo-Container und produktive Mongo-Datadir wurden nicht beruehrt.
|
||||||
|
- Test-Mongo nutzt Wegwerf-Credentials (restoretest-komodo-mongo-pwd).
|
||||||
|
- Kein Komodo-Core gestartet (nicht noetig fuer Dump-Restore-Nachweis).
|
||||||
|
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "Komodo Mongo restore test ok -> $REPORT_FILE"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
services:
|
||||||
|
restoretest-mailarchiver-postgres:
|
||||||
|
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
||||||
|
container_name: restoretest-mailarchiver-postgres
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_USER: mailarchiver
|
||||||
|
POSTGRES_DB: mailarchiver
|
||||||
|
POSTGRES_PASSWORD: restoretest-mailarchiver-db
|
||||||
|
PGDATA: /var/lib/postgresql/18/docker
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/mailarchiver/postgres:/var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U mailarchiver -d mailarchiver"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-mailarchiver:
|
||||||
|
image: s1t5/mailarchiver@sha256:ea7fd8c2e3e0ef0941e8dd9e726e35a8de33296f5c7b9ed811df5168ae6a9714
|
||||||
|
container_name: restoretest-mailarchiver
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
restoretest-mailarchiver-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
# Wegwerf-Connection-String fuer isolierten Test.
|
||||||
|
# Produktiver MAILARCHIVER_DB_CONNECTION ist Stack-ENV-only und wird
|
||||||
|
# hier bewusst NICHT verwendet.
|
||||||
|
ConnectionStrings__DefaultConnection: "Host=restoretest-mailarchiver-postgres;Database=mailarchiver;Username=mailarchiver;Password=restoretest-mailarchiver-db"
|
||||||
|
Authentication__Password: restoretest-mailarchiver-auth
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:15000:5000"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/mailarchiver/data-protection-keys:/app/DataProtection-Keys
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Mail-Archiver Restore Smoke Test
|
||||||
|
#
|
||||||
|
# Borg-Extract der Data-Protection-Keys + pg_restore des mailarchiver-Dumps
|
||||||
|
# in isoliertes Test-Postgres + Container-Boot + HTTP-Smoke.
|
||||||
|
#
|
||||||
|
# In Produktion nutzt Mail-Archiver die Shared PostgreSQL 18 — im Test
|
||||||
|
# bekommt er ein eigenes isoliertes Test-Postgres mit Wegwerf-Credentials.
|
||||||
|
# Authelia-ForwardAuth wird im Smoke nicht geprueft (kein Traefik, kein
|
||||||
|
# Auth-Middleware).
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
. "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
WHATIF=0
|
||||||
|
KEEP_DATA=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--what-if) WHATIF=1 ;;
|
||||||
|
--keep-data) KEEP_DATA=1 ;;
|
||||||
|
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
RESTORE_ROOT="/mnt/user/backups/restore-lab/mailarchiver"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/mailarchiver-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/mailarchiver-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/mailarchiver-$(date +%F).md"
|
||||||
|
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/postgresql17-mailarchiver.dump"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Mail-Archiver restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
Borg source: local/appdata/mailarchiver/data-protection-keys
|
||||||
|
Host dump: $DUMP_HOST_PATH (645M)
|
||||||
|
Test endpoint: 127.0.0.1:15000
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
require_path "$DUMP_HOST_PATH"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "mailarchiver" "$RESTORE_ROOT"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT/data-protection-keys" "$RESTORE_ROOT/postgres"
|
||||||
|
|
||||||
|
archive="$(latest_archive_name)"
|
||||||
|
repo="$(borg_repo_url)"
|
||||||
|
|
||||||
|
if [ -z "$archive" ] || [ -z "$repo" ]; then
|
||||||
|
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 1: Data-Protection-Keys aus Borg
|
||||||
|
borg_extract "/restore/mailarchiver-extract" "local/appdata/mailarchiver/data-protection-keys"
|
||||||
|
if [ ! -d "$EXTRACT_DIR/local/appdata/mailarchiver/data-protection-keys" ]; then
|
||||||
|
echo "Mailarchiver data-protection-keys path missing in Borg archive" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp -a "$EXTRACT_DIR/local/appdata/mailarchiver/data-protection-keys/." "$RESTORE_ROOT/data-protection-keys/"
|
||||||
|
chmod -R a+rwX "$RESTORE_ROOT/data-protection-keys"
|
||||||
|
|
||||||
|
# Stufe 2: Test-Postgres + Dump
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-mailarchiver-postgres >/dev/null
|
||||||
|
until docker exec restoretest-mailarchiver-postgres pg_isready -U mailarchiver -d mailarchiver >/dev/null 2>&1; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
restore_ok=0
|
||||||
|
for attempt in $(seq 1 12); do
|
||||||
|
if docker exec -i restoretest-mailarchiver-postgres \
|
||||||
|
pg_restore -U mailarchiver -d mailarchiver --clean --if-exists --no-owner --no-privileges \
|
||||||
|
< "$DUMP_HOST_PATH" 2>/tmp/mailarchiver-pg-restore.err; then
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if grep -qiE "starting up|shutting down|connection refused" /tmp/mailarchiver-pg-restore.err; then
|
||||||
|
sleep 5
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if grep -qiE "FATAL|PANIC" /tmp/mailarchiver-pg-restore.err; then
|
||||||
|
cat /tmp/mailarchiver-pg-restore.err >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
done
|
||||||
|
if [ "$restore_ok" -ne 1 ]; then
|
||||||
|
cat /tmp/mailarchiver-pg-restore.err >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 3: Container starten
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-mailarchiver >/dev/null
|
||||||
|
|
||||||
|
# Mailarchiver ist ein .NET-App, braucht ein paar Sekunden fuer DB-Migration.
|
||||||
|
# Smoke gegen den Root-Endpunkt — bei Authelia-geschuetztem Dienst liefert
|
||||||
|
# der Container selbst trotzdem einen HTTP-Response (302 oder 200).
|
||||||
|
http_status=""
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
http_status="$(curl -s -o /tmp/mailarchiver-body.html -w '%{http_code}' \
|
||||||
|
-L http://127.0.0.1:15000/ || true)"
|
||||||
|
if [ "$http_status" = "200" ] || [ "$http_status" = "302" ] || [ "$http_status" = "401" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$http_status" != "200" ] && [ "$http_status" != "302" ] && [ "$http_status" != "401" ]; then
|
||||||
|
echo "Mailarchiver HTTP smoke failed: status=$http_status" >&2
|
||||||
|
docker logs --tail 80 restoretest-mailarchiver >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tabellen-Count als Sanity
|
||||||
|
table_count="$(docker exec restoretest-mailarchiver-postgres \
|
||||||
|
psql -U mailarchiver -d mailarchiver -tAc \
|
||||||
|
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
|
||||||
|
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Mail-Archiver Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`mail-archiver\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Test containers: \`restoretest-mailarchiver\`, \`restoretest-mailarchiver-postgres\`
|
||||||
|
- Test endpoint: \`http://127.0.0.1:15000/\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract of data-protection-keys: \`ok\`
|
||||||
|
- Host dump copy (645M): \`ok\`
|
||||||
|
- Dump import into isolated Postgres: \`ok\`
|
||||||
|
- HTTP status: \`$http_status\`
|
||||||
|
- Public table count in test DB: \`$table_count\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Productive secrets (MAILARCHIVER_DB_CONNECTION, MAILARCHIVER_AUTH_PASSWORD) NOT used.
|
||||||
|
- Authelia ForwardAuth NOT tested (no Traefik in smoke).
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "Mailarchiver restore test ok -> $REPORT_FILE"
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
services:
|
||||||
|
restoretest-mealie-postgres:
|
||||||
|
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
||||||
|
container_name: restoretest-mealie-postgres
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_USER: mealie
|
||||||
|
POSTGRES_DB: mealie
|
||||||
|
POSTGRES_PASSWORD: restoretest-mealie-db
|
||||||
|
PGDATA: /var/lib/postgresql/18/docker
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/mealie/postgres:/var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U mealie -d mealie"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-mealie:
|
||||||
|
image: ghcr.io/mealie-recipes/mealie:v3.19.2@sha256:f68e959bf66f4f458893ea58facac71690fe6f2ac7a31466b5cecb41b4e99c02
|
||||||
|
container_name: restoretest-mealie
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
restoretest-mealie-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
ALLOW_SIGNUP: "false"
|
||||||
|
PUID: "99"
|
||||||
|
PGID: "100"
|
||||||
|
DB_ENGINE: postgres
|
||||||
|
POSTGRES_SERVER: restoretest-mealie-postgres
|
||||||
|
POSTGRES_DB: mealie
|
||||||
|
POSTGRES_USER: mealie
|
||||||
|
POSTGRES_PASSWORD: restoretest-mealie-db
|
||||||
|
BASE_URL: http://127.0.0.1:19925
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:19925:9000"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/mealie/data:/app/data
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Mealie Restore Smoke Test
|
||||||
|
#
|
||||||
|
# Borg-Extract der App-Daten + pg_restore des mealie.dump in isoliertes
|
||||||
|
# Test-Postgres + Mealie-Boot + HTTP-Smoke.
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
. "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
WHATIF=0
|
||||||
|
KEEP_DATA=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--what-if) WHATIF=1 ;;
|
||||||
|
--keep-data) KEEP_DATA=1 ;;
|
||||||
|
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
RESTORE_ROOT="/mnt/user/backups/restore-lab/mealie"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/mealie-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/mealie-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/mealie-$(date +%F).md"
|
||||||
|
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/mealie.dump"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Mealie restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
Borg source: local/appdata/mealie/data
|
||||||
|
Host dump: $DUMP_HOST_PATH
|
||||||
|
Test endpoint: 127.0.0.1:19925
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
require_path "$DUMP_HOST_PATH"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "mealie" "$RESTORE_ROOT"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT/data" "$RESTORE_ROOT/postgres"
|
||||||
|
|
||||||
|
archive="$(latest_archive_name)"
|
||||||
|
repo="$(borg_repo_url)"
|
||||||
|
|
||||||
|
if [ -z "$archive" ] || [ -z "$repo" ]; then
|
||||||
|
echo "Could not resolve Borg repo/archive from borg-ui database" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 1: App-Daten aus Borg
|
||||||
|
borg_extract "/restore/mealie-extract" "local/appdata/mealie/data"
|
||||||
|
if [ ! -d "$EXTRACT_DIR/local/appdata/mealie/data" ]; then
|
||||||
|
echo "Mealie data path missing in Borg archive" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
cp -a "$EXTRACT_DIR/local/appdata/mealie/data/." "$RESTORE_ROOT/data/"
|
||||||
|
chmod -R a+rwX "$RESTORE_ROOT/data"
|
||||||
|
|
||||||
|
# Stufe 2: Test-Postgres hochfahren + Dump einspielen
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-mealie-postgres >/dev/null
|
||||||
|
until docker exec restoretest-mealie-postgres pg_isready -U mealie -d mealie >/dev/null 2>&1; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
restore_ok=0
|
||||||
|
for attempt in $(seq 1 12); do
|
||||||
|
if docker exec -i restoretest-mealie-postgres \
|
||||||
|
pg_restore -U mealie -d mealie --clean --if-exists --no-owner --no-privileges \
|
||||||
|
< "$DUMP_HOST_PATH" 2>/tmp/mealie-pg-restore.err; then
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if grep -qiE "starting up|shutting down|connection refused" /tmp/mealie-pg-restore.err; then
|
||||||
|
sleep 5
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if grep -qiE "FATAL|PANIC" /tmp/mealie-pg-restore.err; then
|
||||||
|
cat /tmp/mealie-pg-restore.err >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
done
|
||||||
|
if [ "$restore_ok" -ne 1 ]; then
|
||||||
|
cat /tmp/mealie-pg-restore.err >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 3: Mealie starten
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-mealie >/dev/null
|
||||||
|
|
||||||
|
http_status=""
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
http_status="$(curl -s -o /tmp/mealie-body.html -w '%{http_code}' \
|
||||||
|
-L http://127.0.0.1:19925/api/app/about || true)"
|
||||||
|
if [ "$http_status" = "200" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$http_status" != "200" ]; then
|
||||||
|
echo "Mealie HTTP smoke failed: status=$http_status" >&2
|
||||||
|
docker logs --tail 80 restoretest-mealie >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rezept-Count als Sanity-Check
|
||||||
|
recipe_count="$(docker exec restoretest-mealie-postgres \
|
||||||
|
psql -U mealie -d mealie -tAc \
|
||||||
|
"SELECT count(*) FROM recipes;" 2>/dev/null | tr -d '[:space:]' || echo "n/a")"
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Mealie Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`mealie\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Test containers: \`restoretest-mealie\`, \`restoretest-mealie-postgres\`
|
||||||
|
- Test endpoint: \`http://127.0.0.1:19925/api/app/about\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract of data: \`ok\`
|
||||||
|
- Host dump copy: \`ok\`
|
||||||
|
- Dump import into isolated Postgres: \`ok\`
|
||||||
|
- HTTP status from /api/app/about: \`$http_status\`
|
||||||
|
- Recipe count in test DB: \`$recipe_count\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Productive Mealie secrets were NOT mounted; test uses throwaway DB password.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "Mealie restore test ok -> $REPORT_FILE"
|
||||||
@@ -3,7 +3,7 @@ set -euo pipefail
|
|||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
TOPIC="${TOPIC:-homelab-info}"
|
TOPIC="${TOPIC:-homelab-info}"
|
||||||
TESTS="${TESTS:-vaultwarden gitea paperless}"
|
TESTS="${TESTS:-vaultwarden gitea paperless authelia}"
|
||||||
|
|
||||||
pick_random() {
|
pick_random() {
|
||||||
printf '%s\n' $TESTS | awk 'BEGIN { srand() } { items[++count] = $0 } END { print items[int(rand() * count) + 1] }'
|
printf '%s\n' $TESTS | awk 'BEGIN { srand() } { items[++count] = $0 } END { print items[int(rand() * count) + 1] }'
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
LAB_ROOT="${LAB_ROOT:-/mnt/user/backups/restore-lab/freshness-negative}"
|
||||||
|
REPORT_ROOT="${REPORT_ROOT:-/mnt/user/backups/restore-reports}"
|
||||||
|
ALERT_TOPIC="${ALERT_TOPIC:-homelab-alerts}"
|
||||||
|
INFO_TOPIC="${INFO_TOPIC:-homelab-info}"
|
||||||
|
SEND_NTFY="${SEND_NTFY:-1}"
|
||||||
|
|
||||||
|
stamp="$(date +%F-%H%M%S)"
|
||||||
|
test_root="$LAB_ROOT/$stamp"
|
||||||
|
dump_root="$test_root/dumps"
|
||||||
|
test_report_root="$test_root/reports"
|
||||||
|
report_file="$REPORT_ROOT/freshness-negative-$stamp.md"
|
||||||
|
raw_log="$test_root/check-output.md"
|
||||||
|
|
||||||
|
mkdir -p "$dump_root" "$test_report_root" "$REPORT_ROOT"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$test_root"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
set +e
|
||||||
|
DUMP_ROOT="$dump_root" \
|
||||||
|
REPORT_ROOT="$test_report_root" \
|
||||||
|
MAX_DUMP_AGE_HOURS=26 \
|
||||||
|
MAX_REPORT_AGE_DAYS=45 \
|
||||||
|
"$SCRIPT_DIR/check-restore-freshness.sh" >"$raw_log" 2>&1
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
critical_count="$(awk -F': ' '/^Critical:/ {print $2; exit}' "$raw_log" | tr -d '[:space:]')"
|
||||||
|
critical_count="${critical_count:-0}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Restore Freshness Negative Alert Test"
|
||||||
|
echo
|
||||||
|
echo "Timestamp: $(date '+%F %T')"
|
||||||
|
echo "Result: $([ "$rc" -ne 0 ] && [ "$critical_count" -gt 0 ] && echo "ok" || echo "failed")"
|
||||||
|
echo "Check exit code: $rc"
|
||||||
|
echo "Critical count: $critical_count"
|
||||||
|
echo "Synthetic dump root: $dump_root"
|
||||||
|
echo "Synthetic report root: $test_report_root"
|
||||||
|
echo "Production dump root touched: no"
|
||||||
|
echo
|
||||||
|
echo "## Check Output"
|
||||||
|
echo
|
||||||
|
cat "$raw_log"
|
||||||
|
} >"$report_file"
|
||||||
|
|
||||||
|
if [ "$rc" -ne 0 ] && [ "$critical_count" -gt 0 ]; then
|
||||||
|
if [ "$SEND_NTFY" = "1" ]; then
|
||||||
|
bash "$SCRIPT_DIR/send-ntfy.sh" \
|
||||||
|
"$ALERT_TOPIC" \
|
||||||
|
"TEST: Restore freshness alert path ok" \
|
||||||
|
"Negativtest erfolgreich: check-restore-freshness.sh meldete ${critical_count} Criticals gegen synthetischen leeren Testpfad. Produktive Dumps wurden nicht veraendert. Report: $report_file" \
|
||||||
|
high
|
||||||
|
fi
|
||||||
|
echo "Negative freshness alert test ok. Report: $report_file"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SEND_NTFY" = "1" ]; then
|
||||||
|
bash "$SCRIPT_DIR/send-ntfy.sh" \
|
||||||
|
"$ALERT_TOPIC" \
|
||||||
|
"TEST FAILED: Restore freshness alert path" \
|
||||||
|
"Negativtest fehlgeschlagen: erwarteter Critical-Zustand wurde nicht erkannt. Report: $report_file" \
|
||||||
|
high || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Negative freshness alert test failed. Report: $report_file" >&2
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
services:
|
||||||
|
restoretest-nextcloud-postgres:
|
||||||
|
# Gleiche Major-Version wie apps/nextcloud/docker-compose.yml in Produktion.
|
||||||
|
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
||||||
|
container_name: restoretest-nextcloud-postgres
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_DB: nextcloud
|
||||||
|
POSTGRES_USER: nextcloud
|
||||||
|
POSTGRES_PASSWORD: restoretest-nextcloud-db
|
||||||
|
PGDATA: /var/lib/postgresql/18/docker
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/nextcloud/postgres:/var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U nextcloud -d nextcloud"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-nextcloud-redis:
|
||||||
|
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
|
||||||
|
container_name: restoretest-nextcloud-redis
|
||||||
|
restart: "no"
|
||||||
|
command: redis-server --save "" --appendonly no
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
restoretest-nextcloud:
|
||||||
|
# Gleicher Image-Digest wie apps/nextcloud/docker-compose.yml.
|
||||||
|
image: nextcloud:33.0.4-apache@sha256:caa40b8beaf0057ac213d8dfc515c36ce64f7a8f0825b6a287e6f7cf2f4a095d
|
||||||
|
container_name: restoretest-nextcloud
|
||||||
|
restart: "no"
|
||||||
|
depends_on:
|
||||||
|
restoretest-nextcloud-postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
restoretest-nextcloud-redis:
|
||||||
|
condition: service_started
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_HOST: restoretest-nextcloud-postgres
|
||||||
|
POSTGRES_DB: nextcloud
|
||||||
|
POSTGRES_USER: nextcloud
|
||||||
|
POSTGRES_PASSWORD: restoretest-nextcloud-db
|
||||||
|
REDIS_HOST: restoretest-nextcloud-redis
|
||||||
|
NEXTCLOUD_ADMIN_USER: restoretest-admin
|
||||||
|
NEXTCLOUD_ADMIN_PASSWORD: restoretest-nextcloud-admin-pass
|
||||||
|
NEXTCLOUD_DATA_DIR: /var/www/html/data
|
||||||
|
# Bewusst keine Trusted-Domain/Proxy-Konfiguration: Smoke prueft
|
||||||
|
# nur localhost-HTTP, keine Traefik-Route.
|
||||||
|
ports:
|
||||||
|
# nur 127.0.0.1, keine Public-Route, keine Traefik-Labels
|
||||||
|
- "127.0.0.1:18180:80"
|
||||||
|
volumes:
|
||||||
|
# Restore-Lab-Pfade: alles isoliert, keine produktiven Mounts.
|
||||||
|
- /mnt/user/backups/restore-lab/nextcloud/html:/var/www/html
|
||||||
|
- /mnt/user/backups/restore-lab/nextcloud/data:/var/www/html/data
|
||||||
|
# KEIN no-new-privileges fuer den Smoke-Test-Container.
|
||||||
|
# Der Nextcloud-Entrypoint fuehrt intern chown/chmod auf /var/www/html
|
||||||
|
# und /var/www/html/data aus. Auf Unraid (FUSE/shfs) ignoriert das
|
||||||
|
# Host-Dateisystem chown-Aufrufe von aussen, deshalb muss der
|
||||||
|
# Container-Entrypoint die Rechte selbst setzen koennen. Im isolierten
|
||||||
|
# Smoke-Kontext (127.0.0.1, kein Traefik, Wegwerf-Daten) ist das
|
||||||
|
# vertretbar. Test-Postgres und Test-Redis behalten no-new-privileges.
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Nextcloud Restore Smoke Test
|
||||||
|
#
|
||||||
|
# Nicht-destruktiver Restore-Smoke-Test fuer Nextcloud.
|
||||||
|
#
|
||||||
|
# Was dieser Smoke nachweist:
|
||||||
|
# - Nextcloud-HTML und -Datenpfade koennen aus dem Borg-Archiv extrahiert werden
|
||||||
|
# - nextcloud.dump kann in eine isolierte Test-Postgres importiert werden
|
||||||
|
# - Nextcloud startet gegen die restaurierten Daten + Test-Redis und antwortet
|
||||||
|
# auf HTTP
|
||||||
|
# - occ status zeigt maintenance:mode = false
|
||||||
|
#
|
||||||
|
# Besonderheiten gegenueber den anderen Restore-Tests:
|
||||||
|
# - Nextcloud hat eine eigene Postgres (nicht shared), mit eigener DB-Rolle
|
||||||
|
# - Nextcloud nutzt eine eigene Redis-Instanz (Snapshot-Persistenz, kein Passwort)
|
||||||
|
# - occ maintenance:mode und die Rolle oc_admin sind im DR-Fall relevant;
|
||||||
|
# im Smoke pruefen wir occ status nach dem Boot
|
||||||
|
# - Produktive Secrets (admin_user, admin_password, postgres_password) werden
|
||||||
|
# durch Wegwerf-Werte im Test-Compose ersetzt
|
||||||
|
#
|
||||||
|
# Produktive Nextcloud-Container, produktive Postgres-DB, produktive Secrets,
|
||||||
|
# produktive Nutzdaten unter /mnt/user/documents/nextcloud-data und
|
||||||
|
# produktiver Traefik-Eintrag 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/nextcloud"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
EXTRACT_DIR="$BORG_RESTORE_HOST_ROOT/nextcloud-extract"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/nextcloud-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/nextcloud-$(date +%F).md"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Nextcloud restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
ReportRoot: $REPORT_ROOT
|
||||||
|
Expected Borg source paths:
|
||||||
|
- local/appdata/nextcloud/html (aus Borg-Archiv)
|
||||||
|
Host source paths:
|
||||||
|
- /mnt/user/backups/borg/dumps/latest/nextcloud.dump (vom Host, taeglich frisch)
|
||||||
|
Planned isolation:
|
||||||
|
- Test-Postgres: postgres:18.4 mit Wegwerf-Credentials
|
||||||
|
- Test-Redis: redis:8.8.0-alpine (rebuildbar, kein Restore)
|
||||||
|
- Test-Nextcloud: nextcloud:33.0.4-apache (Image-Pin wie Produktion)
|
||||||
|
- Wegwerf-Admin-Credentials im Test-Compose
|
||||||
|
- Produktive Secrets und Nutzdaten werden NICHT gemountet
|
||||||
|
- Test endpoint: 127.0.0.1:18180 (no Traefik, no public domain)
|
||||||
|
Smoke-Test:
|
||||||
|
- pg_restore -> nextcloud.dump
|
||||||
|
- HTTP 200/302/3xx von 127.0.0.1:18180
|
||||||
|
- occ status: maintenance=false
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_cmd curl
|
||||||
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "nextcloud" "$RESTORE_ROOT"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$EXTRACT_DIR" "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT/html" "$RESTORE_ROOT/data" "$RESTORE_ROOT/postgres" "$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
|
||||||
|
|
||||||
|
# Stufe 1: Nextcloud-App-Pfade aus Borg, Dump vom Host.
|
||||||
|
# HTML (App-Code + config) kommt aus dem Borg-Archiv.
|
||||||
|
# Der Dump liegt frisch auf dem Host unter /mnt/user/backups/borg/dumps/latest/
|
||||||
|
# (wird taeglich von pre-backup-dumps.sh erzeugt und dann in Borg gesichert).
|
||||||
|
# Der Borg-Extract des Dumps wuerde dieselbe Datei liefern, braucht aber eine
|
||||||
|
# eigene Remote-Roundtrip-Zeit; wir nutzen die Host-Kopie direkt.
|
||||||
|
DUMP_HOST_PATH="/mnt/user/backups/borg/dumps/latest/nextcloud.dump"
|
||||||
|
|
||||||
|
borg_extract "/restore/nextcloud-extract" \
|
||||||
|
"local/appdata/nextcloud/html"
|
||||||
|
|
||||||
|
if [ ! -d "$EXTRACT_DIR/local/appdata/nextcloud/html" ]; then
|
||||||
|
echo "Nextcloud html path missing in Borg archive" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ ! -f "$DUMP_HOST_PATH" ]; then
|
||||||
|
echo "nextcloud.dump missing on host at $DUMP_HOST_PATH" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# App-Code + Config ins Restore-Lab verschieben
|
||||||
|
cp -a "$EXTRACT_DIR/local/appdata/nextcloud/html/." "$RESTORE_ROOT/html/"
|
||||||
|
cp "$DUMP_HOST_PATH" "$RESTORE_ROOT/dumps/latest/nextcloud.dump"
|
||||||
|
|
||||||
|
# Nextcloud braucht einen beschreibbaren data-Pfad, auch wenn er leer ist.
|
||||||
|
# Im Restore-Lab ist das /mnt/user/backups/restore-lab/nextcloud/data.
|
||||||
|
mkdir -p "$RESTORE_ROOT/data"
|
||||||
|
|
||||||
|
# Nextcloud prueft auf einen Marker `.ncdata` mit dem festen Inhalt
|
||||||
|
# "# Nextcloud data directory" und blockt sonst mit "Your data directory
|
||||||
|
# is invalid" (HTTP 503). Produktiv liegt der Marker unter
|
||||||
|
# /mnt/user/documents/nextcloud-data/.ncdata; der Smoke mountet diesen
|
||||||
|
# Pfad bewusst nicht, also legen wir den Marker hier an. Das ist die
|
||||||
|
# in der Nextcloud-Doku vorgesehene Form.
|
||||||
|
echo "# Nextcloud data directory" > "$RESTORE_ROOT/data/.ncdata"
|
||||||
|
|
||||||
|
# Unraid (FUSE/shfs) ignoriert chown auf User-Shares. Stattdessen setzen
|
||||||
|
# wir die Dateien auf world-writable, damit der Nextcloud-Entrypoint
|
||||||
|
# (der als root startet und intern auf www-data wechselt) die Dateien
|
||||||
|
# lesen und beschreiben kann. Im isolierten Smoke-Kontext vertretbar.
|
||||||
|
chmod -R a+rwX "$RESTORE_ROOT/html" "$RESTORE_ROOT/data"
|
||||||
|
|
||||||
|
# Falls config.php einen anderen dbuser als das Test-Compose hat, patchen
|
||||||
|
# wir die DB-Zugangsdaten in der restaurierten config.php fuer den Test.
|
||||||
|
CONFIG_PHP="$RESTORE_ROOT/html/config/config.php"
|
||||||
|
if [ -f "$CONFIG_PHP" ]; then
|
||||||
|
# Backup der Originalkonfig fuer Diagnose
|
||||||
|
cp "$CONFIG_PHP" "$RESTORE_ROOT/html/config/config.php.original"
|
||||||
|
|
||||||
|
# DB-Credentials auf die Test-Werte umbiegen. Nextcloud config.php
|
||||||
|
# ist PHP; wir patchen die relevanten Zeilen per sed.
|
||||||
|
sed -i \
|
||||||
|
-e "s|'dbhost'.*|'dbhost' => 'restoretest-nextcloud-postgres',|" \
|
||||||
|
-e "s|'dbuser'.*|'dbuser' => 'nextcloud',|" \
|
||||||
|
-e "s|'dbpassword'.*|'dbpassword' => 'restoretest-nextcloud-db',|" \
|
||||||
|
-e "s|'dbname'.*|'dbname' => 'nextcloud',|" \
|
||||||
|
-e "s|'dbport'.*|'dbport' => '',|" \
|
||||||
|
"$CONFIG_PHP"
|
||||||
|
|
||||||
|
# Redis-Host patchen. Die config.php hat ein verschachteltes Array:
|
||||||
|
# 'redis' => array( 'host' => 'nextcloud-redis', ... )
|
||||||
|
# Wir ersetzen nur den Host-Wert innerhalb des redis-Blocks.
|
||||||
|
sed -i "s|'host' => 'nextcloud-redis'|'host' => 'restoretest-nextcloud-redis'|g" "$CONFIG_PHP"
|
||||||
|
|
||||||
|
# Zwei Patches in der config.php, beides per PHP-Code-Injection am Ende:
|
||||||
|
#
|
||||||
|
# 1. trusted_domains: 127.0.0.1 hinzufuegen, damit der Smoke-Endpunkt
|
||||||
|
# akzeptiert wird. Nextcloud prueft trusted_domains und blockt sonst
|
||||||
|
# mit "Access through untrusted domain" (503).
|
||||||
|
#
|
||||||
|
# 2. check_data_directory_permissions: false. Hintergrund: Nextcloud
|
||||||
|
# (OC_Util::checkDataDirectoryPermissions) prueft beim HTTP-Request, ob
|
||||||
|
# die data-Dir-Permissions in der letzten Stelle 0 sind. Falls nicht,
|
||||||
|
# versucht es als www-data ein chmod(0770). Auf Unraid (shfs/FUSE)
|
||||||
|
# lehnt das Filesystem chmod von Non-Root ab, also kann der Container
|
||||||
|
# das nie korrigieren -> Nextcloud meldet "data directory readable by
|
||||||
|
# other people" -> HTTP 503. Im isolierten Smoke-Kontext (Wegwerf-
|
||||||
|
# Daten, kein Public, kein Traefik) ist das Aushebeln dieses Checks
|
||||||
|
# sauber dokumentiert vorgesehen. Produktiv bleibt der Check an.
|
||||||
|
php -r "
|
||||||
|
\$f = '$CONFIG_PHP';
|
||||||
|
\$c = file_get_contents(\$f);
|
||||||
|
if (strpos(\$c, \"'127.0.0.1'\") === false || strpos(\$c, 'check_data_directory_permissions') === false) {
|
||||||
|
include \$f;
|
||||||
|
if (!in_array('127.0.0.1', \$CONFIG['trusted_domains'])) {
|
||||||
|
\$CONFIG['trusted_domains'][] = '127.0.0.1';
|
||||||
|
}
|
||||||
|
\$CONFIG['check_data_directory_permissions'] = false;
|
||||||
|
\$out = '<?php' . PHP_EOL . '\$CONFIG = ' . var_export(\$CONFIG, true) . ';' . PHP_EOL;
|
||||||
|
file_put_contents(\$f, \$out);
|
||||||
|
}
|
||||||
|
" 2>/dev/null || {
|
||||||
|
# Fallback: wenn php nicht auf dem Host ist, per sed versuchen
|
||||||
|
if ! grep -q "127.0.0.1" "$CONFIG_PHP"; then
|
||||||
|
sed -i "/'trusted_domains'/,/^ )/s|^ )| 99 => '127.0.0.1',\n )|" "$CONFIG_PHP" || true
|
||||||
|
fi
|
||||||
|
if ! grep -q "check_data_directory_permissions" "$CONFIG_PHP"; then
|
||||||
|
sed -i "s|^);| 'check_data_directory_permissions' => false,\n);|" "$CONFIG_PHP" || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
config_patched="ok"
|
||||||
|
else
|
||||||
|
config_patched="no config.php found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 2: Test-Postgres + Test-Redis hochfahren
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-nextcloud-postgres restoretest-nextcloud-redis >/dev/null
|
||||||
|
until docker exec restoretest-nextcloud-postgres pg_isready -U nextcloud -d nextcloud >/dev/null 2>&1; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Stufe 3: Dump einspielen (mit Retry wie bei Paperless/Immich)
|
||||||
|
restore_ok=0
|
||||||
|
for attempt in $(seq 1 12); do
|
||||||
|
if docker exec -i restoretest-nextcloud-postgres \
|
||||||
|
pg_restore -U nextcloud -d nextcloud --clean --if-exists --no-owner --no-privileges \
|
||||||
|
< "$RESTORE_ROOT/dumps/latest/nextcloud.dump" 2>/tmp/nextcloud-pg-restore.err; then
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/nextcloud-pg-restore.err; then
|
||||||
|
sleep 5
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# pg_restore mit --clean erzeugt "does not exist"-Warnungen fuer nicht vorhandene
|
||||||
|
# Objekte beim ersten Import. Diese sind erwartbar und kein echter Fehler.
|
||||||
|
# Wir pruefen auf harte Fehler.
|
||||||
|
if grep -qiE "FATAL|PANIC" /tmp/nextcloud-pg-restore.err; then
|
||||||
|
cat /tmp/nextcloud-pg-restore.err >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$restore_ok" -ne 1 ]; then
|
||||||
|
cat /tmp/nextcloud-pg-restore.err >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 4: Nextcloud starten
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-nextcloud >/dev/null
|
||||||
|
|
||||||
|
# Nextcloud braucht beim ersten Start mit existierender config.php einige
|
||||||
|
# Sekunden fuer DB-Migrations-Checks. Wir geben bis zu 180s.
|
||||||
|
http_status=""
|
||||||
|
for _ in $(seq 1 90); do
|
||||||
|
http_status="$(curl -s -o /tmp/nextcloud-body.html -w '%{http_code}' \
|
||||||
|
-L http://127.0.0.1:18180/status.php || true)"
|
||||||
|
if [ "$http_status" = "200" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$http_status" != "200" ]; then
|
||||||
|
echo "Nextcloud HTTP smoke failed: status=$http_status" >&2
|
||||||
|
docker logs --tail 120 restoretest-nextcloud >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 5: occ status pruefen (maintenance mode)
|
||||||
|
occ_output="$(docker exec -u www-data restoretest-nextcloud php occ status --output=json 2>/dev/null || echo '{}')"
|
||||||
|
maintenance="$(echo "$occ_output" | grep -o '"maintenance":[a-z]*' | head -1 | cut -d: -f2)"
|
||||||
|
if [ -z "$maintenance" ]; then
|
||||||
|
maintenance="unknown"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# DB-Tabellen-Count als fachlicher Sanity-Check
|
||||||
|
table_count="$(docker exec restoretest-nextcloud-postgres \
|
||||||
|
psql -U nextcloud -d nextcloud -tAc \
|
||||||
|
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
|
||||||
|
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Nextcloud Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`nextcloud\`
|
||||||
|
- Source repo: \`$repo\`
|
||||||
|
- Archive: \`$archive\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Test containers:
|
||||||
|
- \`restoretest-nextcloud\`
|
||||||
|
- \`restoretest-nextcloud-postgres\`
|
||||||
|
- \`restoretest-nextcloud-redis\`
|
||||||
|
- Test endpoint: \`http://127.0.0.1:18180/status.php\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Borg extract of html: \`ok\`
|
||||||
|
- Host dump copy: \`ok\`
|
||||||
|
- config.php patched for test DB: \`$config_patched\`
|
||||||
|
- Dump import into isolated Postgres: \`ok\`
|
||||||
|
- HTTP status from /status.php: \`$http_status\`
|
||||||
|
- occ status maintenance: \`$maintenance\`
|
||||||
|
- Public table count in test DB: \`$table_count\`
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Dieser Smoke prueft: Borg-Restore von App-Code + Config + DB-Dump,
|
||||||
|
Dump-Import in isoliertes Test-Postgres, Nextcloud-Boot mit restaurierter
|
||||||
|
config.php (DB-Credentials auf Test-Werte gepatcht), HTTP-Status und
|
||||||
|
occ-Maintenance-Status.
|
||||||
|
|
||||||
|
Bewusst NICHT Teil des Smokes:
|
||||||
|
- Voller Restore der Nutzdaten unter /mnt/user/documents/nextcloud-data
|
||||||
|
(zu gross fuer regelmaessigen Smoke; Pfad-Existenz im Archiv kann
|
||||||
|
separat geprueft werden)
|
||||||
|
- Produktive Secrets (admin_user/password, postgres_password)
|
||||||
|
- Traefik-Route und produktive Domain cloud.kaleschke.info
|
||||||
|
- occ maintenance:mode Toggle (der Test-Restore braucht keinen
|
||||||
|
vorhergehenden maintenance:mode --on, weil er gegen einen Dump laeuft)
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Test ran without Traefik and without the productive domain.
|
||||||
|
- Productive Nextcloud secrets were NOT mounted; test uses throwaway credentials.
|
||||||
|
- Productive user data under /mnt/user/documents/nextcloud-data was NOT mounted.
|
||||||
|
- config.php.original preserved for diagnosis.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "Nextcloud restore test ok -> $REPORT_FILE"
|
||||||
@@ -41,8 +41,14 @@ require_cmd curl
|
|||||||
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
require_path "$BORG_PASSPHRASE_FILE_DEFAULT"
|
||||||
require_path "$COMPOSE_FILE"
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
cleanup() {
|
cleanup() {
|
||||||
cleanup_compose "$COMPOSE_FILE"
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "paperless" "$RESTORE_ROOT"
|
||||||
|
rm -rf "$EXTRACT_DIR"
|
||||||
|
return
|
||||||
|
fi
|
||||||
if [ "$KEEP_DATA" -ne 1 ]; then
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
rm -rf "$RESTORE_ROOT"
|
rm -rf "$RESTORE_ROOT"
|
||||||
fi
|
fi
|
||||||
@@ -70,7 +76,30 @@ mv "$EXTRACT_DIR/local/borg-dumps/latest/postgresql17-paperless.dump" "$RESTORE_
|
|||||||
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless-postgres restoretest-paperless-redis >/dev/null
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless-postgres restoretest-paperless-redis >/dev/null
|
||||||
until docker exec restoretest-paperless-postgres pg_isready -U paperless -d paperless >/dev/null 2>&1; do sleep 2; done
|
until docker exec restoretest-paperless-postgres pg_isready -U paperless -d paperless >/dev/null 2>&1; do sleep 2; done
|
||||||
cat "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump" | docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperless --clean --if-exists --no-owner --no-privileges
|
|
||||||
|
# Postgres-Entrypoint kann kurz nach "ready" noch vom Init- auf den finalen
|
||||||
|
# Server wechseln. pg_restore toleriert transiente Start-/Shutdown-Fehler und
|
||||||
|
# retried; harte Fehler (z. B. Dump-Korruption) brechen wie bisher ab.
|
||||||
|
restore_ok=0
|
||||||
|
for attempt in $(seq 1 12); do
|
||||||
|
if docker exec -i restoretest-paperless-postgres \
|
||||||
|
pg_restore -U paperless -d paperless --clean --if-exists --no-owner --no-privileges \
|
||||||
|
< "$RESTORE_ROOT/dumps/latest/postgresql17-paperless.dump" 2>/tmp/paperless-pg-restore.err; then
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if grep -qiE "starting up|shutting down|connection refused|database .* does not exist" /tmp/paperless-pg-restore.err; then
|
||||||
|
sleep 5
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
cat /tmp/paperless-pg-restore.err >&2
|
||||||
|
exit 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$restore_ok" -ne 1 ]; then
|
||||||
|
cat /tmp/paperless-pg-restore.err >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless >/dev/null
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-paperless >/dev/null
|
||||||
sleep 12
|
sleep 12
|
||||||
@@ -110,4 +139,5 @@ write_report "$REPORT_FILE" <<EOF
|
|||||||
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
echo "Paperless restore test ok -> $REPORT_FILE"
|
echo "Paperless restore test ok -> $REPORT_FILE"
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ mv /mnt/user/backups/restore-lab/paperless/local/paperless/consume /mnt/user/bac
|
|||||||
3. Test-Postgres und Test-Redis starten
|
3. Test-Postgres und Test-Redis starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless-postgres restoretest-paperless-redis
|
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless-postgres restoretest-paperless-redis
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Dump in Test-Postgres importieren
|
4. Dump in Test-Postgres importieren
|
||||||
@@ -78,7 +78,7 @@ docker exec -i restoretest-paperless-postgres pg_restore -U paperless -d paperle
|
|||||||
5. Testinstanz starten
|
5. Testinstanz starten
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless
|
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml up -d restoretest-paperless
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Smoke-Test
|
6. Smoke-Test
|
||||||
@@ -98,7 +98,7 @@ Minimal erfolgreich:
|
|||||||
7. Testcontainer wieder stoppen
|
7. Testcontainer wieder stoppen
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose -f /mnt/user/services/homelab/ops/restore-tests/paperless-compose.test.yml down
|
docker compose -f /mnt/user/services/homelab-infra/ops/restore-tests/paperless-compose.test.yml down
|
||||||
```
|
```
|
||||||
|
|
||||||
8. Testdaten nach erfolgreichem Lauf bereinigen
|
8. Testdaten nach erfolgreichem Lauf bereinigen
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
services:
|
||||||
|
restoretest-redis:
|
||||||
|
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1
|
||||||
|
container_name: restoretest-redis
|
||||||
|
restart: "no"
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- exec redis-server --appendonly yes --requirepass "$$(cat /run/secrets/redis_password)"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:16379:6379/tcp"
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/redis/data:/data
|
||||||
|
- /mnt/user/backups/restore-lab/redis/secrets/redis_password.txt:/run/secrets/redis_password:ro
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
Executable
+152
@@ -0,0 +1,152 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Redis 8 Restore Smoke Test
|
||||||
|
#
|
||||||
|
# Scope:
|
||||||
|
# - Restore aus dem dokumentierten shared-redis-pre-redis8-Artefakt
|
||||||
|
# - Start einer isolierten Redis-8-Testinstanz auf localhost:16379
|
||||||
|
# - PING, INFO server und DBSIZE ohne Ausgabe des Passworts
|
||||||
|
|
||||||
|
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/redis"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
PRE_CUTOVER_ROOT="/mnt/user/backups/borg/dumps/latest"
|
||||||
|
SECRET_FILE="/mnt/user/appdata/secrets/redis_password.txt"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/redis-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/redis-$(date +%F).md"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Redis 8 restore test
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
Restore source: newest $PRE_CUTOVER_ROOT/shared-redis-pre-redis8-*
|
||||||
|
Secret source: $SECRET_FILE
|
||||||
|
Test endpoint: 127.0.0.1:16379
|
||||||
|
Scope: Data restore + isolated Redis boot + PING/INFO/DBSIZE smoke
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_path "$PRE_CUTOVER_ROOT"
|
||||||
|
require_path "$SECRET_FILE"
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
cleanup_compose "$COMPOSE_FILE"
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "redis" "$RESTORE_ROOT"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT/data" "$RESTORE_ROOT/secrets"
|
||||||
|
|
||||||
|
restore_source="$(find "$PRE_CUTOVER_ROOT" -maxdepth 1 -type d -name 'shared-redis-pre-redis8-*' | sort | tail -1)"
|
||||||
|
if [ -z "$restore_source" ]; then
|
||||||
|
echo "No shared-redis-pre-redis8-* restore source found under $PRE_CUTOVER_ROOT" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$restore_source" ]; then
|
||||||
|
echo "Redis restore source is not a directory: $restore_source" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cp -a "$restore_source/." "$RESTORE_ROOT/data/"
|
||||||
|
cp "$SECRET_FILE" "$RESTORE_ROOT/secrets/redis_password.txt"
|
||||||
|
chmod -R a+rwX "$RESTORE_ROOT/data"
|
||||||
|
chmod a+r "$RESTORE_ROOT/secrets/redis_password.txt"
|
||||||
|
|
||||||
|
data_files="$(find "$RESTORE_ROOT/data" -type f | wc -l | tr -d ' ')"
|
||||||
|
data_bytes="$(du -sb "$RESTORE_ROOT/data" | awk '{print $1}')"
|
||||||
|
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-redis >/dev/null
|
||||||
|
|
||||||
|
ping_result=""
|
||||||
|
for _ in $(seq 1 60); do
|
||||||
|
ping_result="$(docker exec restoretest-redis sh -lc \
|
||||||
|
'p=$(cat /run/secrets/redis_password); redis-cli -a "$p" --no-auth-warning PING' 2>/dev/null || true)"
|
||||||
|
if [ "$ping_result" = "PONG" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$ping_result" != "PONG" ]; then
|
||||||
|
echo "Redis PING smoke failed: $ping_result" >&2
|
||||||
|
docker logs --tail 80 restoretest-redis >&2 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
redis_version="$(docker exec restoretest-redis sh -lc \
|
||||||
|
'p=$(cat /run/secrets/redis_password); redis-cli -a "$p" --no-auth-warning INFO server | awk -F: "/^redis_version:/ {gsub(/\r/, \"\", \$2); print \$2}"')"
|
||||||
|
dbsize="$(docker exec restoretest-redis sh -lc \
|
||||||
|
'p=$(cat /run/secrets/redis_password); redis-cli -a "$p" --no-auth-warning DBSIZE' | tr -d '\r')"
|
||||||
|
aof_enabled="$(docker exec restoretest-redis sh -lc \
|
||||||
|
'p=$(cat /run/secrets/redis_password); redis-cli -a "$p" --no-auth-warning INFO persistence | awk -F: "/^aof_enabled:/ {gsub(/\r/, \"\", \$2); print \$2}"')"
|
||||||
|
|
||||||
|
case "$redis_version" in
|
||||||
|
8.*) ;;
|
||||||
|
*)
|
||||||
|
echo "Unexpected Redis version: $redis_version" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "${dbsize:-0}" -lt 1 ]; then
|
||||||
|
echo "Unexpected Redis DBSIZE: $dbsize" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
# Redis 8 Restore Test Report - $(date +%F)
|
||||||
|
|
||||||
|
- Service: \`redis\`
|
||||||
|
- Restore source: \`$restore_source\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Test container: \`restoretest-redis\`
|
||||||
|
- Test endpoint: \`127.0.0.1:16379\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Data restore from pre-Redis8 artifact: \`ok\`
|
||||||
|
- Secret file mounted from host secret path: \`ok\`
|
||||||
|
- Restored data files: \`$data_files\`
|
||||||
|
- Restored data bytes: \`$data_bytes\`
|
||||||
|
- PING: \`$ping_result\`
|
||||||
|
- Redis version: \`$redis_version\`
|
||||||
|
- AOF enabled: \`$aof_enabled\`
|
||||||
|
- DBSIZE: \`$dbsize\`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Productive Redis port 6379 and productive data path were NOT used.
|
||||||
|
- Test port was bound to localhost only: \`127.0.0.1:16379\`.
|
||||||
|
- Redis password value was used from the restored secret file and was not printed.
|
||||||
|
- Test data was cleaned after success: \`$([ "$KEEP_DATA" -eq 1 ] && echo no || echo yes)\`
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "Redis restore test ok -> $REPORT_FILE"
|
||||||
@@ -10,6 +10,9 @@ case "$MODE" in
|
|||||||
freshness)
|
freshness)
|
||||||
exec "$SCRIPT_DIR/check-restore-freshness.sh"
|
exec "$SCRIPT_DIR/check-restore-freshness.sh"
|
||||||
;;
|
;;
|
||||||
|
freshness-negative)
|
||||||
|
exec "$SCRIPT_DIR/negative-freshness-alert-test.sh"
|
||||||
|
;;
|
||||||
vaultwarden)
|
vaultwarden)
|
||||||
if [ "$WHATIF" = "--what-if" ]; then
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
exec "$SCRIPT_DIR/vaultwarden-restore-test.sh" --what-if
|
exec "$SCRIPT_DIR/vaultwarden-restore-test.sh" --what-if
|
||||||
@@ -34,8 +37,68 @@ case "$MODE" in
|
|||||||
fi
|
fi
|
||||||
exec "$SCRIPT_DIR/immich-restore-test.sh"
|
exec "$SCRIPT_DIR/immich-restore-test.sh"
|
||||||
;;
|
;;
|
||||||
|
authelia)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/authelia-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/authelia-restore-test.sh"
|
||||||
|
;;
|
||||||
|
adguard)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/adguard-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/adguard-restore-test.sh"
|
||||||
|
;;
|
||||||
|
redis)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/redis-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/redis-restore-test.sh"
|
||||||
|
;;
|
||||||
|
nextcloud)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/nextcloud-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/nextcloud-restore-test.sh"
|
||||||
|
;;
|
||||||
|
komodo-bootstrap)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/komodo-bootstrap-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/komodo-bootstrap-test.sh"
|
||||||
|
;;
|
||||||
|
komodo-mongo-restore)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/komodo-mongo-restore-test.sh"
|
||||||
|
;;
|
||||||
|
traefik)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/traefik-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/traefik-restore-test.sh"
|
||||||
|
;;
|
||||||
|
mailarchiver)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/mailarchiver-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/mailarchiver-restore-test.sh"
|
||||||
|
;;
|
||||||
|
mealie)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/mealie-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/mealie-restore-test.sh"
|
||||||
|
;;
|
||||||
|
shared-pg-cluster)
|
||||||
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
|
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh" --what-if
|
||||||
|
fi
|
||||||
|
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich} [--what-if]" >&2
|
echo "Usage: $0 {freshness|freshness-negative|vaultwarden|gitea|paperless|immich|authelia|adguard|redis|nextcloud|komodo-bootstrap|komodo-mongo-restore|traefik|mailarchiver|mealie|shared-pg-cluster} [--what-if]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -7,24 +7,29 @@ SUCCESS_TOPIC="${2:-${RESTORE_SUCCESS_TOPIC:-homelab-info}}"
|
|||||||
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
|
FAILURE_TOPIC="${RESTORE_FAILURE_TOPIC:-homelab-alerts}"
|
||||||
|
|
||||||
if [ -z "$MODE" ]; then
|
if [ -z "$MODE" ]; then
|
||||||
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless|immich> [success_topic]" >&2
|
echo "Usage: $0 <freshness|vaultwarden|gitea|paperless|immich|authelia|nextcloud|komodo-bootstrap> [success_topic]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
|
REPORT_FILE="$REPORT_ROOT/${MODE}-$(date +%F).md"
|
||||||
|
WRAPPER_LOG="$REPORT_ROOT/_wrapper-${MODE}-$(date +%F).log"
|
||||||
|
|
||||||
mkdir -p "$REPORT_ROOT"
|
mkdir -p "$REPORT_ROOT"
|
||||||
|
|
||||||
echo "Running restore job: $MODE"
|
echo "Running restore job: $MODE"
|
||||||
echo "Report target: $REPORT_FILE"
|
echo "Inner report (written by restore script): $REPORT_FILE"
|
||||||
|
echo "Wrapper log (stdout/stderr of dispatcher): $WRAPPER_LOG"
|
||||||
|
|
||||||
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" > "$REPORT_FILE"; then
|
# Der Restore-Job schreibt seinen Markdown-Report selbst nach $REPORT_FILE.
|
||||||
|
# Wir leiten stdout/stderr in eine separate Wrapper-Log-Datei, damit hier
|
||||||
|
# kein zweiter Schreiber denselben Pfad ueberschreibt.
|
||||||
|
if "$SCRIPT_DIR/run-restore-checks.sh" "$MODE" >"$WRAPPER_LOG" 2>&1; then
|
||||||
echo "Restore job succeeded, sending ntfy..."
|
echo "Restore job succeeded, sending ntfy..."
|
||||||
"$SCRIPT_DIR/send-ntfy.sh" "$SUCCESS_TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
|
"$SCRIPT_DIR/send-ntfy.sh" "$SUCCESS_TOPIC" "Restore job ok: $MODE" "Restore job succeeded. Report: $REPORT_FILE" default || true
|
||||||
echo "Done"
|
echo "Done"
|
||||||
else
|
else
|
||||||
echo "Restore job failed, sending ntfy..."
|
echo "Restore job failed, sending ntfy..."
|
||||||
"$SCRIPT_DIR/send-ntfy.sh" "$FAILURE_TOPIC" "Restore job failed: $MODE" "Restore job failed. Report: $REPORT_FILE" high || true
|
"$SCRIPT_DIR/send-ntfy.sh" "$FAILURE_TOPIC" "Restore job failed: $MODE" "Restore job failed. Wrapper log: $WRAPPER_LOG (Report if written: $REPORT_FILE)" high || true
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ Quartalsweise:
|
|||||||
|
|
||||||
- Restore-/DR-Sanity-Check
|
- Restore-/DR-Sanity-Check
|
||||||
- `immich` Restore-Smoke-Test (DB + UI, ohne produktive Foto-Mounts; Erstlauf 2026-05-27 erfolgreich)
|
- `immich` Restore-Smoke-Test (DB + UI, ohne produktive Foto-Mounts; Erstlauf 2026-05-27 erfolgreich)
|
||||||
|
- `adguard` Restore-Smoke-Test (Config + HTTP/DNS, nach DNS-Aenderungen auch ausserhalb des Quartals)
|
||||||
|
- `redis` Restore-Smoke-Test (Pre-Cutover-Artefakt + Redis 8, vor/nach Major-Aenderungen auch ausserhalb des Quartals)
|
||||||
- pruefen:
|
- pruefen:
|
||||||
- Restore-Lab-Struktur
|
- Restore-Lab-Struktur
|
||||||
- Reports
|
- Reports
|
||||||
@@ -44,7 +46,10 @@ Quartals-Belegung:
|
|||||||
| Q4 | `vaultwarden` oder `gitea` | Externe Abhaengigkeiten, Hetzner, GitHub-Mirror |
|
| Q4 | `vaultwarden` oder `gitea` | Externe Abhaengigkeiten, Hetzner, GitHub-Mirror |
|
||||||
|
|
||||||
Bestaetigte Mini-Restores: Vaultwarden, Gitea und Paperless am 2026-05-07;
|
Bestaetigte Mini-Restores: Vaultwarden, Gitea und Paperless am 2026-05-07;
|
||||||
Immich am 2026-05-27; Paperless erneut am 2026-05-31.
|
Immich am 2026-05-27; Paperless erneut am 2026-05-31; Authelia am
|
||||||
|
2026-06-03 (Config-Smoke ohne produktiven Dump-Restore); AdGuard Home am
|
||||||
|
2026-06-06 (Config + HTTP/DNS-Smoke); Redis 8 am 2026-06-06
|
||||||
|
(Pre-Cutover-Artefakt + PING/INFO/DBSIZE-Smoke).
|
||||||
|
|
||||||
## Konkreter Kalender
|
## Konkreter Kalender
|
||||||
|
|
||||||
@@ -56,6 +61,8 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31.
|
|||||||
- `gitea`
|
- `gitea`
|
||||||
- Jeden 2. Samstag in ungeraden Monaten, 08:00:
|
- Jeden 2. Samstag in ungeraden Monaten, 08:00:
|
||||||
- `paperless`
|
- `paperless`
|
||||||
|
- Jeden 2. Samstag in geraden Monaten, 07:30:
|
||||||
|
- `authelia`
|
||||||
- Jeden 1. des Monats, 09:00:
|
- Jeden 1. des Monats, 09:00:
|
||||||
- `monthly-random-restore.sh`
|
- `monthly-random-restore.sh`
|
||||||
- Quartalsweise am 1. Werktag des Quartals:
|
- Quartalsweise am 1. Werktag des Quartals:
|
||||||
@@ -65,24 +72,29 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31.
|
|||||||
|
|
||||||
## Unraid User Scripts Cron
|
## Unraid User Scripts Cron
|
||||||
|
|
||||||
| Script | Cron | Bedeutung |
|
Vixie-Cron (Unraid) verknuepft `day-of-month` und `day-of-week` mit **OR**, sobald beide gesetzt sind. "n-ter Samstag im Monat" laesst sich deshalb nicht direkt im Cron-Ausdruck ausdruecken. Wir triggern stattdessen an **jedem** Samstag/Sonntag und filtern den Monatstag im User-Script per Shell-Guard.
|
||||||
|---|---|---|
|
|
||||||
| `restore-freshness-weekly` | `30 6 * * 1` | jeden Montag 06:30 |
|
| Script | Cron | Shell-Guard (zusaetzlich) | Bedeutung |
|
||||||
| `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-freshness-weekly` | `30 6 * * 1` | - | jeden Montag 06:30 |
|
||||||
| `restore-paperless-bimonthly` | `0 8 8-14 1,3,5,7,9,11 *` | zweiter Samstag in ungeraden Monaten 08:00 |
|
| `restore-vaultwarden-monthly` | `0 7 * * 6` | `[ "$(date +%-d)" -le 7 ]` | erster Samstag im Monat 07:00 |
|
||||||
| `restore-immich-quarterly` | `30 8 8-14 2,5,8,11 0` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
|
| `restore-gitea-monthly` | `15 7 * * 6` | `d=$(date +%-d); [ "$d" -ge 15 ] && [ "$d" -le 21 ]` | dritter Samstag im Monat 07:15 |
|
||||||
| `monthly-random-restore` | `0 9 1 * *` | erster Kalendertag im Monat 09:00 |
|
| `restore-paperless-bimonthly` | `0 8 * * 6` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 1\|3\|5\|7\|9\|11) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Samstag in ungeraden Monaten 08:00 |
|
||||||
|
| `restore-authelia-bimonthly` | `30 7 * * 6` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|4\|6\|8\|10\|12) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Samstag in geraden Monaten 07:30 |
|
||||||
|
| `restore-immich-quarterly` | `30 8 * * 0` | `m=$(date +%-m); d=$(date +%-d); case "$m" in 2\|5\|8\|11) [ "$d" -ge 8 ] && [ "$d" -le 14 ];; *) false;; esac` | zweiter Sonntag in Feb/Mai/Aug/Nov 08:30 |
|
||||||
|
| `monthly-random-restore` | `0 9 1 * *` | - | erster Kalendertag im Monat 09:00 |
|
||||||
|
|
||||||
|
**Warum so**: ein frueheres Schema wie `0 7 1-7 * 6` haette in Vixie-Cron die OR-Semantik ausgeloest und an jedem Tag 1-7 zusaetzlich zu jedem Samstag gefeuert (~11 Laeufe statt 1 pro Monat). Die obige Trennung Cron-Trigger + Shell-Guard ist die einzige robuste Loesung in Standard-Cron.
|
||||||
|
|
||||||
## Betriebsmodus
|
## Betriebsmodus
|
||||||
|
|
||||||
- V1:
|
- V1:
|
||||||
- Bash-Jobs laufen hostseitig manuell oder per User Script
|
- Bash-Jobs laufen hostseitig manuell oder per User Script
|
||||||
- `ntfy` ist optional und folgt nach stabiler Basis
|
- `ntfy`-Wrapper ist vorhanden; Erfolg geht nach `homelab-info`, Fehler nach `homelab-alerts`
|
||||||
- Hermes wertet spaeter nur Reports aus
|
- Hermes wertet spaeter optional Reports aus
|
||||||
- V2:
|
- V2:
|
||||||
- fester Host-Schedule
|
- fester Host-Schedule
|
||||||
- `ntfy` bei Erfolg/Fehler
|
- `ntfy` bei Erfolg/Fehler ueber `run-restore-job-with-ntfy.sh`
|
||||||
- Hermes erzeugt Zusammenfassungen und Overviews
|
- Hermes erzeugt Zusammenfassungen und Overviews
|
||||||
|
|
||||||
## Automatisierung
|
## Automatisierung
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
services:
|
||||||
|
restoretest-shared-pg:
|
||||||
|
image: postgres:18.4@sha256:8ff36f3c66371cba71d20ceedccfc3de9669a68737607888c4ef0af93abe8e39
|
||||||
|
container_name: restoretest-shared-pg
|
||||||
|
restart: "no"
|
||||||
|
environment:
|
||||||
|
TZ: Europe/Berlin
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: restoretest-shared-pg-superuser
|
||||||
|
PGDATA: /var/lib/postgresql/18/docker
|
||||||
|
volumes:
|
||||||
|
- /mnt/user/backups/restore-lab/shared-pg-cluster/data:/var/lib/postgresql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
security_opt:
|
||||||
|
- no-new-privileges:true
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Shared PostgreSQL 18 Cluster Restore Drill
|
||||||
|
#
|
||||||
|
# Beweist, dass der komplette Shared-Postgres-Cluster aus den Dump-Artefakten
|
||||||
|
# wiederhergestellt werden kann:
|
||||||
|
# 1. Globals (Rollen) aus pg_dumpall --globals-only
|
||||||
|
# 2. Per-DB Custom-Format-Dumps: paperless, mailarchiver, authelia,
|
||||||
|
# nextcloud, mealie
|
||||||
|
#
|
||||||
|
# Bekannter Sonderfall (docs/RESTORE_MATRIX.md):
|
||||||
|
# - CREATE ROLE mailarchiver scheitert, weil der User gleichzeitig der
|
||||||
|
# Dump-Admin-User ist. Das ALTER ROLE danach muss trotzdem durchlaufen.
|
||||||
|
# Der Test toleriert diesen spezifischen Fehler.
|
||||||
|
#
|
||||||
|
# Produktive PostgreSQL-Container und -Datenpfade werden NICHT angefasst.
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
. "$SCRIPT_DIR/common.sh"
|
||||||
|
|
||||||
|
WHATIF=0
|
||||||
|
KEEP_DATA=0
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--what-if) WHATIF=1 ;;
|
||||||
|
--keep-data) KEEP_DATA=1 ;;
|
||||||
|
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
RESTORE_ROOT="/mnt/user/backups/restore-lab/shared-pg-cluster"
|
||||||
|
REPORT_ROOT="/mnt/user/backups/restore-reports"
|
||||||
|
COMPOSE_FILE="$SCRIPT_DIR/shared-pg-cluster-compose.test.yml"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/shared-pg-cluster-$(date +%F).md"
|
||||||
|
DUMP_ROOT="/mnt/user/backups/borg/dumps/latest"
|
||||||
|
|
||||||
|
# Alle erwarteten Dumps
|
||||||
|
GLOBALS_DUMP="$DUMP_ROOT/postgresql17-globals.sql"
|
||||||
|
PAPERLESS_DUMP="$DUMP_ROOT/postgresql17-paperless.dump"
|
||||||
|
MAILARCHIVER_DUMP="$DUMP_ROOT/postgresql17-mailarchiver.dump"
|
||||||
|
AUTHELIA_DUMP="$DUMP_ROOT/postgresql17-authelia.dump"
|
||||||
|
NEXTCLOUD_DUMP="$DUMP_ROOT/nextcloud.dump"
|
||||||
|
MEALIE_DUMP="$DUMP_ROOT/mealie.dump"
|
||||||
|
|
||||||
|
if [ "$WHATIF" -eq 1 ]; then
|
||||||
|
cat <<EOF
|
||||||
|
Shared PostgreSQL 18 Cluster Restore Drill
|
||||||
|
Mode: WhatIf
|
||||||
|
RestoreRoot: $RESTORE_ROOT
|
||||||
|
Dumps from: $DUMP_ROOT
|
||||||
|
Steps:
|
||||||
|
1. Frisches postgres:18.4 mit Superuser hochfahren
|
||||||
|
2. Globals einspielen (pg_dumpall --globals-only)
|
||||||
|
-> bekannter mailarchiver-Rollenkonflikt wird toleriert
|
||||||
|
3. DBs anlegen: paperless, mailarchiver, authelia, nextcloud, mealie
|
||||||
|
4. Per-DB pg_restore fuer jede DB
|
||||||
|
5. Tabellen-Count pro DB als Sanity-Check
|
||||||
|
6. Report schreiben
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
require_cmd docker
|
||||||
|
require_path "$COMPOSE_FILE"
|
||||||
|
require_path "$GLOBALS_DUMP"
|
||||||
|
require_path "$PAPERLESS_DUMP"
|
||||||
|
require_path "$MAILARCHIVER_DUMP"
|
||||||
|
|
||||||
|
# Authelia/Nextcloud/Mealie-Dumps sind optional (koennen fehlen)
|
||||||
|
OPTIONAL_DUMPS=""
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=0
|
||||||
|
cleanup() {
|
||||||
|
docker compose -f "$COMPOSE_FILE" down -v >/dev/null 2>&1 || true
|
||||||
|
if [ "$RESTORE_SUCCESS" -ne 1 ]; then
|
||||||
|
preserve_on_failure "shared-pg-cluster" "$RESTORE_ROOT"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
if [ "$KEEP_DATA" -ne 1 ]; then
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
rm -rf "$RESTORE_ROOT"
|
||||||
|
mkdir -p "$RESTORE_ROOT/data"
|
||||||
|
|
||||||
|
# Stufe 1: Test-Postgres hochfahren
|
||||||
|
docker compose -f "$COMPOSE_FILE" up -d restoretest-shared-pg >/dev/null
|
||||||
|
until docker exec restoretest-shared-pg pg_isready -U postgres >/dev/null 2>&1; do
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
# Extra Wartezeit fuer Entrypoint-Init
|
||||||
|
sleep 3
|
||||||
|
|
||||||
|
# Stufe 2: Globals einspielen
|
||||||
|
# Der Globals-Dump enthaelt CREATE ROLE fuer alle DB-User. Der bekannte
|
||||||
|
# Konflikt ist, dass CREATE ROLE mailarchiver scheitern kann wenn dieser
|
||||||
|
# User auch der Dump-Admin ist. Wir tolerieren das und pruefen nur auf
|
||||||
|
# FATAL/PANIC.
|
||||||
|
globals_status="ok"
|
||||||
|
docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
|
||||||
|
psql -U postgres -f - < "$GLOBALS_DUMP" >/tmp/shared-pg-globals.log 2>&1 || true
|
||||||
|
if grep -qiE "FATAL|PANIC" /tmp/shared-pg-globals.log; then
|
||||||
|
globals_status="failed (FATAL/PANIC)"
|
||||||
|
cat /tmp/shared-pg-globals.log >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stufe 3: DBs anlegen und Dumps einspielen
|
||||||
|
declare -A DB_STATUS
|
||||||
|
declare -A TABLE_COUNTS
|
||||||
|
|
||||||
|
restore_db() {
|
||||||
|
local dbname="$1"
|
||||||
|
local dbuser="$2"
|
||||||
|
local dump_path="$3"
|
||||||
|
local optional="${4:-no}"
|
||||||
|
|
||||||
|
if [ ! -f "$dump_path" ]; then
|
||||||
|
if [ "$optional" = "yes" ]; then
|
||||||
|
DB_STATUS[$dbname]="skipped (dump missing)"
|
||||||
|
TABLE_COUNTS[$dbname]="n/a"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
DB_STATUS[$dbname]="failed (dump missing)"
|
||||||
|
TABLE_COUNTS[$dbname]="n/a"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Rolle anlegen falls nicht durch Globals erzeugt (idempotent)
|
||||||
|
docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
|
||||||
|
psql -U postgres -c "DO \$\$ BEGIN CREATE ROLE $dbuser WITH LOGIN PASSWORD 'restoretest-$dbuser'; EXCEPTION WHEN duplicate_object THEN NULL; END \$\$;" >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# DB anlegen
|
||||||
|
docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
|
||||||
|
psql -U postgres -c "SELECT 1 FROM pg_database WHERE datname='$dbname'" 2>/dev/null | grep -q 1 || \
|
||||||
|
docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
|
||||||
|
createdb -U postgres -O "$dbuser" "$dbname" 2>/dev/null || true
|
||||||
|
|
||||||
|
# pg_restore mit Retry
|
||||||
|
local restore_ok=0
|
||||||
|
for attempt in $(seq 1 5); do
|
||||||
|
if docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
|
||||||
|
pg_restore -U postgres -d "$dbname" --clean --if-exists --no-owner --no-privileges \
|
||||||
|
< "$dump_path" 2>/tmp/shared-pg-restore-${dbname}.err; then
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if grep -qiE "starting up|shutting down|connection refused" /tmp/shared-pg-restore-${dbname}.err; then
|
||||||
|
sleep 5
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
# --clean erzeugt "does not exist" Warnungen beim ersten Import -> ignorieren
|
||||||
|
if grep -qiE "FATAL|PANIC" /tmp/shared-pg-restore-${dbname}.err; then
|
||||||
|
DB_STATUS[$dbname]="failed"
|
||||||
|
TABLE_COUNTS[$dbname]="n/a"
|
||||||
|
cat /tmp/shared-pg-restore-${dbname}.err >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
restore_ok=1
|
||||||
|
break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$restore_ok" -ne 1 ]; then
|
||||||
|
DB_STATUS[$dbname]="failed (timeout)"
|
||||||
|
TABLE_COUNTS[$dbname]="n/a"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
DB_STATUS[$dbname]="ok"
|
||||||
|
|
||||||
|
# Tabellen zaehlen
|
||||||
|
TABLE_COUNTS[$dbname]="$(docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
|
||||||
|
psql -U postgres -d "$dbname" -tAc \
|
||||||
|
"SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" \
|
||||||
|
2>/dev/null | tr -d '[:space:]' || echo "n/a")"
|
||||||
|
}
|
||||||
|
|
||||||
|
restore_db "paperless" "paperless" "$PAPERLESS_DUMP"
|
||||||
|
restore_db "mailarchiver" "mailarchiver" "$MAILARCHIVER_DUMP"
|
||||||
|
restore_db "authelia" "authelia" "$AUTHELIA_DUMP" "yes"
|
||||||
|
restore_db "nextcloud" "nextcloud" "$NEXTCLOUD_DUMP" "yes"
|
||||||
|
restore_db "mealie" "mealie" "$MEALIE_DUMP" "yes"
|
||||||
|
|
||||||
|
# Stufe 4: data_checksums pruefen
|
||||||
|
checksums="$(docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
|
||||||
|
psql -U postgres -tAc "SHOW data_checksums;" 2>/dev/null | tr -d '[:space:]' || echo "n/a")"
|
||||||
|
|
||||||
|
# Stufe 5: DB-Liste
|
||||||
|
db_list="$(docker exec -i -e PGPASSWORD=restoretest-shared-pg-superuser restoretest-shared-pg \
|
||||||
|
psql -U postgres -tAc "SELECT datname FROM pg_database WHERE NOT datistemplate ORDER BY datname;" \
|
||||||
|
2>/dev/null | tr '\n' ', ' | sed 's/,$//' || echo "n/a")"
|
||||||
|
|
||||||
|
# Report bauen
|
||||||
|
report_body="# Shared PostgreSQL 18 Cluster Restore Drill - $(date +%F)
|
||||||
|
|
||||||
|
- Dump source: \`$DUMP_ROOT\`
|
||||||
|
- Restore root: \`$RESTORE_ROOT\`
|
||||||
|
- Result: \`SUCCESS\`
|
||||||
|
|
||||||
|
## Checks
|
||||||
|
|
||||||
|
- Test-Postgres healthy: \`ok\`
|
||||||
|
- Globals import: \`$globals_status\`
|
||||||
|
- data_checksums: \`$checksums\`
|
||||||
|
- Databases: \`$db_list\`
|
||||||
|
|
||||||
|
## Per-DB Restore
|
||||||
|
|
||||||
|
| Database | Restore | Tables |
|
||||||
|
|---|---|---|
|
||||||
|
| paperless | \`${DB_STATUS[paperless]}\` | \`${TABLE_COUNTS[paperless]}\` |
|
||||||
|
| mailarchiver | \`${DB_STATUS[mailarchiver]}\` | \`${TABLE_COUNTS[mailarchiver]}\` |
|
||||||
|
| authelia | \`${DB_STATUS[authelia]}\` | \`${TABLE_COUNTS[authelia]}\` |
|
||||||
|
| nextcloud | \`${DB_STATUS[nextcloud]}\` | \`${TABLE_COUNTS[nextcloud]}\` |
|
||||||
|
| mealie | \`${DB_STATUS[mealie]}\` | \`${TABLE_COUNTS[mealie]}\` |
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Dieser Drill beweist, dass der gesamte Shared-PostgreSQL-18-Cluster aus
|
||||||
|
den taeglichen Dump-Artefakten wiederhergestellt werden kann: Globals
|
||||||
|
(Rollen) + per-DB Custom-Format-Dumps. Der bekannte mailarchiver-
|
||||||
|
Bootstrap-Rollenkonflikt wird toleriert.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Produktive PostgreSQL-Container und -Datenpfade wurden nicht beruehrt.
|
||||||
|
- Test-Postgres nutzt Wegwerf-Superuser-Passwort.
|
||||||
|
- Test-Daten wurden \`$([ "$KEEP_DATA" -eq 1 ] && echo behalten || echo bereinigt)\`.
|
||||||
|
"
|
||||||
|
|
||||||
|
write_report "$REPORT_FILE" <<EOF
|
||||||
|
$report_body
|
||||||
|
EOF
|
||||||
|
|
||||||
|
RESTORE_SUCCESS=1
|
||||||
|
echo "Shared PG cluster restore drill ok -> $REPORT_FILE"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user