1 Commits

Author SHA1 Message Date
renovate a074392848 chore(deps): update redis:8.8.0-alpine docker digest to 9eb6a7b 2026-06-23 04:22:06 +00:00
16 changed files with 9 additions and 407 deletions
+1 -1
View File
@@ -36,7 +36,7 @@ services:
- no-new-privileges:true - no-new-privileges:true
dawarich_redis: dawarich_redis:
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1 image: redis:8.8.0-alpine@sha256:9eb6a7ba3d344e1958c7e1589fa3dee90373a934e8159c634562a91d622759a0
container_name: dawarich_redis container_name: dawarich_redis
restart: unless-stopped restart: unless-stopped
command: command:
+1 -1
View File
@@ -65,7 +65,7 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1 image: redis:8.8.0-alpine@sha256:9eb6a7ba3d344e1958c7e1589fa3dee90373a934e8159c634562a91d622759a0
restart: unless-stopped restart: unless-stopped
networks: networks:
- immich_default - immich_default
+1 -1
View File
@@ -64,7 +64,7 @@ services:
- no-new-privileges:true - no-new-privileges:true
nextcloud-redis: nextcloud-redis:
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1 image: redis:8.8.0-alpine@sha256:9eb6a7ba3d344e1958c7e1589fa3dee90373a934e8159c634562a91d622759a0
container_name: nextcloud-redis container_name: nextcloud-redis
restart: unless-stopped restart: unless-stopped
command: redis-server --save 60 1 --loglevel warning command: redis-server --save 60 1 --loglevel warning
-69
View File
@@ -1,69 +0,0 @@
# Auth-Matrix
Typ: Inventar/Referenz · Stand: 2026-06-23 · Status: aktiv
Konsolidierte Review-Sicht auf die **effektive Zugriffskontrolle je oeffentlicher
UI/Domain**. Bisher lag das verstreut: Authelia-ACL nur in der Live-Config, die
Bypass-/Ausnahmefaelle in `HOMELAB_ARCHITECTURE_MASTER_V2.md` (Prosa + Service-
und Ausnahmen-Tabellen) und die Begruendungen in `DECISIONS.md`. Diese Datei
buendelt das an **einem** Ort und **verlinkt** auf die Quellen statt zu kopieren.
## Quelle der Wahrheit
- **Authelia-ACL** (bypass / two_factor / Catch-all): `security/authelia/configuration.yml`
(`access_control`). Repo-zu-Host-Drift prueft `services/authelia-diff.sh`.
- **Dienste ohne ForwardAuth** (bewusste Ausnahmen): Ausnahmen-Tabelle in
`HOMELAB_ARCHITECTURE_MASTER_V2.md` + die jeweiligen `DECISIONS.md`-Eintraege.
- **Nicht-Authelia-Schutzschichten** (Tailscale-Bind, Traefik-IP-Allowlist): die
jeweilige `docker-compose.yml` per Label bzw. Host-Bind.
> Diese Matrix ist die kommentierte Lese-/Review-Sicht. Bei Widerspruch gewinnen
> die oben genannten Quellen; dann diese Datei nachziehen.
## Policy-Klassen
- **Bypass** — Authelia laesst durch, **kein 2FA**; Schutz liegt allein in der
App-eigenen Anmeldung. Bewusst fuer Public-Apps mit eigenen Clients.
- **two_factor** — Authelia-ForwardAuth mit 2FA (Operator-TOTP), `authelia@file`.
- **Keine Authelia (native)** — bewusste Ausnahme ohne ForwardAuth, App-Auth bleibt.
- **Tailscale-only / LAN-only** — gar nicht oeffentlich, kein Traefik bzw. an
Tailscale-/LAN-IP gebunden.
- **IP-Allowlist** — oeffentlich geroutet, aber per Traefik-Middleware auf
vertrauenswuerdige Quell-Netze begrenzt (sonst 403).
## Matrix
| UI / Domain | Effektive Policy | Mechanismus | Quelle / Begruendung |
|---|---|---|---|
| `auth.kaleschke.info` | Bypass (Authelia selbst) | — | muss immer erreichbar sein |
| `immich`, `paperless`, `mealie`, `vault`, `ntfy`, `git` `.kaleschke.info` | **Bypass** → native App-Auth, kein 2FA | Authelia laesst durch | `configuration.yml`; Public-Apps mit eigener Auth/Clients |
| `vault.kaleschke.info/admin` | **IP-Allowlist** (Tailnet `100.64.0.0/10` + LAN `192.168.178.0/24`), sonst 403 | Traefik `ipallowlist` (Label) | DECISIONS 2026-06-23 (Audit-P1) |
| `files.kaleschke.info`, `scrutiny.kaleschke.info` | **two_factor** (explizit) | `authelia@file` | `configuration.yml`; scrutiny zusaetzlich privileged |
| uebrige `*.kaleschke.info` mit Middleware (monitoring/grafana, glances, glance, speedtest, bentopdf, mail-archiver, paperless-gpt, hermes, super-productivity, borg-ui, code-server) | **two_factor** (Catch-all) | `authelia@file` Catch-all | `configuration.yml`; Haertung 2026-06-06 |
| `komodo.kaleschke.info` | **IP-Allowlist** (Tailnet + LAN), sonst 403; native (keine Authelia) | Traefik `ipallowlist` (Label) + native Komodo-Auth | DECISIONS 2026-06-23; Webhooks/Periphery laufen intern, nicht ueber Traefik |
| `nextcloud` | Keine Authelia (native) | WebDAV/CardDAV/Client-Flows | DECISIONS 2026-04 / Ausnahmen-Tabelle |
| `n8n.kaleschke.info` | Keine pauschale Authelia (native) | Webhook-Endpunkte `/webhook/*` | Ausnahmen-Tabelle; ⚠ Middleware-Abweichung lt. policy-check |
| `plex.kaleschke.info` | Keine Authelia (native Plex) | File-Provider-Route; WAN-Port 32400 + Remote Access aus | DECISIONS 2026-05-28 |
| `home.kaleschke.info` (homeassistant) | Keine Authelia (native HA) | Traefik + `smarthome_net`; LAN-Port 8123 | Ausnahmen-Tabelle; ⚠ Middleware-Abweichung lt. policy-check |
| AdGuard-Admin | **Tailscale-only**, nicht oeffentlich | Host-Bind `100.80.98.33:8082`, kein Traefik | DECISIONS 2026-05-26 |
| `influxdb3-core` :8181 | **LAN-only** Writer (HA) | Host-Port, kein Traefik, nicht in `frontend_net` | dokumentierte Ausnahme |
## Review-Gaps (Audit 2026-06-23)
- **Komodo**: beschlossen IP-Allowlist (Tailnet + LAN) statt public (DECISIONS
2026-06-23). Self-Stack ist inline in Komodo verwaltet → Labels via Komodo-UI
setzen (Task #6), dann verifizieren (#7).
- **Bypass-Liste bewusst ohne 2FA**: Bei App-CVE oder Account-Kompromiss greift
davor kein Authelia. Akzeptiert fuer Public-Apps mit eigenen Clients; Review-
Trigger = neue sensible App in der Liste oder veraendertes Risikoprofil.
- **Middleware-Abweichungen** (policy-check `TRAEFIK002`): `n8n` und
`homeassistant` sind erwartbar (native Ausnahmen). `grafana` steht **nicht** in
der Ausnahmen-Tabelle und ist als Catch-all-`two_factor` gefuehrt — die
abweichende Middleware ist live zu bestaetigen (offen).
## Pflege
Diese Matrix bei jeder Aenderung an Authelia-ACL, vorgeschalteter Middleware,
Tailscale-Bind oder IP-Allowlist mitziehen. Zugehoerige Drift-Erkennung:
`services/authelia-diff.sh` (ACL), `ops/policy-checks/check_repo.ps1` (Middleware-
Standard `TRAEFIK002`).
-65
View File
@@ -11,71 +11,6 @@ in `HOMELAB_ARCHITECTURE_MASTER_V2.md` §13, `docs/MASTER_TODO.md` (Geparkt),
--- ---
## 2026-06-23 - Komodo nur aus vertrauenswuerdigen Netzen (IP-Allowlist statt public)
**Entscheidung:** Der Komodo-Router (`komodo.kaleschke.info`) bekommt eine
Label-definierte `ipallowlist`-Middleware auf Tailnet `100.64.0.0/10` + LAN
`192.168.178.0/24`; public-Zugriff liefert kuenftig `403`. KEINE ForwardAuth
(die bewusste Komodo-Ausnahme bleibt), `KOMODO_HOST` bleibt
`https://komodo.kaleschke.info`. Der GANZE Router wird begrenzt, kein
pfadbasierter Public-Bypass.
**Kontext:** Audit 2026-06-23 (P1): Komodo war public mit `200` erreichbar und
koppelt ueber den RW-Docker-Socket der Periphery an Host-root-aehnliche Macht
(Core -> Periphery -> `docker.sock` -> jeder Container/Datenpfad). Read-only
gemessen: Gitea→Komodo-Webhooks (`/listener`) und Periphery (`/ws/periphery`)
laufen INTERN ueber `komodo-core:9120`, NICHT ueber Traefik. Der public Router
hat damit keine legitimen externen Consumer; eine Allowlist auf dem ganzen Router
schliesst die Public-Flaeche, ohne Automation zu brechen.
**Umsetzung / Ausnahme:** Der Komodo-Self-Stack ist inline in Komodo verwaltet
(`repo=""`, `files_on_host=false`, `webhook_enabled=false`, vgl. 2026-05-04),
KEIN GitOps-Push-Stack. Die Labels werden in der Komodo-UI am Inline-Compose
gesetzt; `ops/komodo/docker-compose.yml` ist nur Spiegel/Doku und wird zur
Paritaet nachgezogen.
**Alternativen:** Reines Tailscale-only (Route + public DNS-Record raus,
`KOMODO_HOST` auf Tailscale-Host) — strenger (kein 403-Endpunkt, keine
Hostname-Disclosure), aber mehr Aufwand und geaenderter Operator-Zugriff;
verworfen zugunsten des bewaehrten, minimalen Allowlist-Musters (analog Vault
/admin). **Review-Trigger:** Wunsch nach vollstaendiger Unsichtbarkeit von aussen
oder Aenderung am Komodo-Zugriffspfad.
---
## 2026-06-23 - Vaultwarden /admin nur aus vertrauenswuerdigen Netzen (IP-Allowlist)
**Entscheidung:** Das Vaultwarden-Admin-Panel `/admin` bekommt einen zweiten,
hoeher priorisierten Traefik-Router `vaultwarden-admin` (Regel Host +
PathPrefix `/admin`, `priority=100`) mit einer Label-definierten
`ipallowlist`-Middleware auf Tailnet `100.64.0.0/10` + LAN `192.168.178.0/24`.
Der Hauptrouter bleibt unveraendert nativ (Browser-Extension, Mobile-Clients,
WebSocket `/notifications/hub`), damit normale Vault-Nutzung von ueberall
funktioniert. Public-Zugriff auf `/admin` liefert kuenftig `403`.
**Kontext:** Empirischer Audit 2026-06-23 (P1): `/admin` antwortete public mit
`200`, obwohl `SIGNUPS_ALLOWED=false`, `INVITATIONS_ALLOWED=false` und
`ADMIN_TOKEN_FILE` gesetzt sind. Der Admin-Token bleibt damit oeffentlich brute-
und CVE-exponiert. Gleiche Logik wie AdGuard-Admin (Entscheidung 2026-05-26,
Tailscale-only), hier aber pfadbasiert ueber Traefik statt Host-Port-Bind, weil
Vaultwarden nur einen Container-Port hat. Definition als Docker-Label (nicht
File-Provider), damit Komodo die Middleware mitdeployed.
**Alternativen:** (a) Authelia `two_factor` auf `/admin` — verworfen als
Primaerloesung, weil der Endpunkt dann public erreichbar bliebe; bleibt Fallback,
falls die Quelle-IP ueber den Operator-Zugriffspfad nicht zuverlaessig im
Allowlist-Bereich landet. (b) Reines Tailscale-only ohne LAN — strenger, aber
LAN bewusst als Break-glass behalten (im Bedrohungsmodell vertrauenswuerdig),
um Self-Lockout zu vermeiden.
**Abhaengigkeit / Review-Trigger:** Wirkt nur, wenn `/admin`-Zugriff mit einer
Quelle aus `100.64.0.0/10` oder `192.168.178.0/24` an Traefik ankommt — vor
finaler Abnahme per Traefik-Access-Log und `curl` aus public + Tailscale/LAN
verifizieren. Review bei Aenderung an Vault-Routing, Tailnet-CIDR oder Umstieg
auf reines Tailscale-only.
---
## 2026-06-16 - Immich ML bekommt dediziertes Egress-Netz (Modell-Download) ## 2026-06-16 - Immich ML bekommt dediziertes Egress-Netz (Modell-Download)
**Entscheidung:** `immich_machine_learning` haengt zusaetzlich zu `immich_default` **Entscheidung:** `immich_machine_learning` haengt zusaetzlich zu `immich_default`
+2 -3
View File
@@ -1,6 +1,6 @@
# Master To-do - KalliLab CORE # Master To-do - KalliLab CORE
Typ: Status/To-do · Stand: 2026-06-23 · Status: aktiv Typ: Status/To-do · Stand: 2026-06-21 · Status: aktiv
Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im Diese Liste ist die **einzige** Arbeitsliste fuer offene operative Punkte im
Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit Homelab. Detailablaeufe stehen in den verlinkten Runbooks; Entscheidungen mit
@@ -27,7 +27,6 @@ Host-Reports (`/mnt/user/backups/restore-reports/`) und in der Git-Historie.
| Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` | | Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` |
| Nearline-Pull Dead-Man's-Switch | Operator | **S4U-Root-Cause 2026-06-21 behoben + verifiziert:** Task `KalliLab H Drive Nearline Pull` von S4U auf LogonType `Interactive` ("Nur wenn Benutzer angemeldet") umgestellt (kein Passwort noetig, da `michi` Dauer-Konsolen-User) -> per Planer mit `0x0` bestaetigt. Spiegel frisch, Exit-Code-Leak gefixt, Heartbeat-Pings gepusht. **Verbleibt (optional, niedrige Dringlichkeit):** je einen Healthchecks-Check anlegen + Capability-URL hinterlegen (baerchen ENV `HEALTHCHECKS_NEARLINE_URL`/Datei; Unraid `/mnt/user/appdata/secrets/healthchecks_borg_url`) | `ops/h-drive-nearline/README.md` | | Nearline-Pull Dead-Man's-Switch | Operator | **S4U-Root-Cause 2026-06-21 behoben + verifiziert:** Task `KalliLab H Drive Nearline Pull` von S4U auf LogonType `Interactive` ("Nur wenn Benutzer angemeldet") umgestellt (kein Passwort noetig, da `michi` Dauer-Konsolen-User) -> per Planer mit `0x0` bestaetigt. Spiegel frisch, Exit-Code-Leak gefixt, Heartbeat-Pings gepusht. **Verbleibt (optional, niedrige Dringlichkeit):** je einen Healthchecks-Check anlegen + Capability-URL hinterlegen (baerchen ENV `HEALTHCHECKS_NEARLINE_URL`/Datei; Unraid `/mnt/user/appdata/secrets/healthchecks_borg_url`) | `ops/h-drive-nearline/README.md` |
| Monitoring Single-File-Bind-Mount Hardening | Operator/Claude | alertmanager/blackbox/loki/promtail + alertmanager-ntfy-bridge lokal auf Directory-Mounts umgestellt (grafana-provisioning war bereits Directory-Mount); `docker compose config` gruen. **Verbleibt:** Push + Komodo-Redeploy des monitoring-Stacks mit `--force-recreate` (Mount-Pfade aendern sich), danach Reload-/Alert-Smoke | `monitoring/docker-compose.yml` | | Monitoring Single-File-Bind-Mount Hardening | Operator/Claude | alertmanager/blackbox/loki/promtail + alertmanager-ntfy-bridge lokal auf Directory-Mounts umgestellt (grafana-provisioning war bereits Directory-Mount); `docker compose config` gruen. **Verbleibt:** Push + Komodo-Redeploy des monitoring-Stacks mit `--force-recreate` (Mount-Pfade aendern sich), danach Reload-/Alert-Smoke | `monitoring/docker-compose.yml` |
| Audit 2026-06-23 Remediation-Reste | Codex/Operator | **P1/P2-Kern erledigt** (Commits `23a6975..3e9c12e`). Verbleibt: #19 Snapshot-Restore-Test live validieren + in `schedule.md` (`run-restore-checks.sh hetzner-snapshot`); #12 `vaultwarden`+`Redis` per force-recreate aus Default-`bridge`; #14 Drift-Redeploy `gitea` 1.26.4 + `dawarich_redis` 8.8.0; #15 Immich-Stackpfad messen + Repo/Doku angleichen | `docs/AUTH_MATRIX.md`, `docs/DECISIONS.md` (2026-06-23), `ops/restore-tests/hetzner-snapshot-runbook.md` |
--- ---
@@ -75,8 +74,8 @@ Bewusst nicht jetzt - Begruendungen in `docs/DECISIONS.md`, hier nur Thema und T
## Zuletzt erledigt (Kurzlog, max. 5 Eintraege) ## Zuletzt erledigt (Kurzlog, max. 5 Eintraege)
- **2026-06-23** Audit-Remediation abgeschlossen: Vault `/admin` + Komodo public zu (403, IP-Allowlist), Off-site-Snapshots bewiesen + monatlicher Test, Live-Drift bereinigt, `backend_net` auf `internal:true` gesetzt (Egress live BLOCKED, 12/12 Member ok). Doku: `AUTH_MATRIX.md`, DECISIONS, Snapshot-Runbook. Commits `23a6975..` ff.
- **2026-06-17** Offene TODOs gegen Live-Stand abgeglichen: Paperless-OIDC-Secret verdrahtet und Service-Smoke gruen; alter Tailscale-Docker-State nach `_archive/tailscale-removed-2026-06-06/` verschoben; Tailnet-Restpunkt geschlossen. - **2026-06-17** Offene TODOs gegen Live-Stand abgeglichen: Paperless-OIDC-Secret verdrahtet und Service-Smoke gruen; alter Tailscale-Docker-State nach `_archive/tailscale-removed-2026-06-06/` verschoben; Tailnet-Restpunkt geschlossen.
- **2026-06-17** Repo-Hygiene abgeschlossen: Glance-Widget-Tokens sind in Runtime gesetzt, Audit-PDF liegt extern unter `H:\kallilab-recovery\audits`, Worktree clean.
- **2026-06-17** Komodo/Gitea-Webhooks normalisiert: aktive Komodo-Hooks fuer `Micha/homelab-infra` nutzen Branch-Filter `master`; DB-Backup vor Host-Hotfix erstellt. Workflow-Regel nachgezogen. - **2026-06-17** Komodo/Gitea-Webhooks normalisiert: aktive Komodo-Hooks fuer `Micha/homelab-infra` nutzen Branch-Filter `master`; DB-Backup vor Host-Hotfix erstellt. Workflow-Regel nachgezogen.
- **2026-06-19** Backup-Hardening live verifiziert: Borg-Scope-Drift 0 (alle 33 Quellen konfiguriert), Dumps frisch (11/11 present), neue Dump-Alerts aktiv (25 Regeln, 0 feuern). Prometheus-`alerts.yml`-Stale-Handle (FUSE-Einzeldatei-Mount) per `--force-recreate` behoben und anschliessend dauerhaft auf Directory-Mount umgestellt (recreated, 25 Regeln aktiv). - **2026-06-19** Backup-Hardening live verifiziert: Borg-Scope-Drift 0 (alle 33 Quellen konfiguriert), Dumps frisch (11/11 present), neue Dump-Alerts aktiv (25 Regeln, 0 feuern). Prometheus-`alerts.yml`-Stale-Handle (FUSE-Einzeldatei-Mount) per `--force-recreate` behoben und anschliessend dauerhaft auf Directory-Mount umgestellt (recreated, 25 Regeln aktiv).
- **2026-06-18** Backup-Audit-Hardening: Dump-Frische-Metriken + Alerts `HomelabBorgDumpMissing/Stale`, Freshness-Checks + Nearline-Pull um `n8n`/`globals` ergaenzt, 4 Tier-2-Container in Critical-Watch, Scope-Doku fuer `projekte`/Hermes praezisiert. H:-Nearline (still seit 2026-06-04) nachgeholt + Task neu registriert. - **2026-06-18** Backup-Audit-Hardening: Dump-Frische-Metriken + Alerts `HomelabBorgDumpMissing/Stale`, Freshness-Checks + Nearline-Pull um `n8n`/`globals` ergaenzt, 4 Tier-2-Container in Critical-Watch, Scope-Doku fuer `projekte`/Hermes praezisiert. H:-Nearline (still seit 2026-06-04) nachgeholt + Task neu registriert.
-2
View File
@@ -287,8 +287,6 @@ 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'
``` ```
> Stand 2026-06-23: `backend_net` live als `internal: true` bestaetigt (Egress-Test aus `postgresql17` nach `1.1.1.1:443` = BLOCKED, 12/12 Member attached); zuvor Drift `internal:false`, per Audit-Remediation behoben.
## SSH-Konfiguration Host ## SSH-Konfiguration Host
Geprueft 2026-06-06 (read-only), **gehaertet 2026-06-07** via `ssh root@192.168.178.58`. Geprueft 2026-06-06 (read-only), **gehaertet 2026-06-07** via `ssh root@192.168.178.58`.
-1
View File
@@ -40,7 +40,6 @@ geloescht (Git-Historie ist das Archiv). Verbindliche Doku-Regeln:
|---|---| |---|---|
| `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 |
| `AUTH_MATRIX.md` | konsolidierte Auth-Matrix: effektive Policy je Domain (bypass/2FA/native/Tailscale/IP-Allowlist) |
| `AUTHELIA_OIDC_PLAN.md` | Plan & Runbook fuer app-uebergreifendes SSO via Authelia OIDC | | `AUTHELIA_OIDC_PLAN.md` | Plan & Runbook fuer app-uebergreifendes SSO via Authelia OIDC |
| `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART- und Power-Baseline | | `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART- und Power-Baseline |
| `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen | | `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen |
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
redis: redis:
image: redis:8.8.0-alpine@sha256:09160599abd229764c0fb44cb6be640294e1d360a54b19985ab4843dcf2d90f1 image: redis:8.8.0-alpine@sha256:9eb6a7ba3d344e1958c7e1589fa3dee90373a934e8159c634562a91d622759a0
container_name: Redis container_name: Redis
restart: unless-stopped restart: unless-stopped
command: command:
+1 -8
View File
@@ -74,14 +74,7 @@ services:
- traefik.http.routers.komodo.tls=true - traefik.http.routers.komodo.tls=true
- traefik.http.routers.komodo.tls.certresolver=le - traefik.http.routers.komodo.tls.certresolver=le
- traefik.http.services.komodo.loadbalancer.server.port=9120 - traefik.http.services.komodo.loadbalancer.server.port=9120
# Audit 2026-06-23 (P1): Komodo war public mit 200 erreichbar + RW-Docker-Socket-Kette.
# IP-Allowlist begrenzt den GANZEN Router auf Tailnet + LAN (public -> 403). KEINE ForwardAuth
# (Webhooks/Periphery laufen intern ueber komodo-core:9120, nicht ueber Traefik).
# ACHTUNG: Self-Stack ist inline in Komodo verwaltet -> diese Labels muessen in der Komodo-UI
# am Inline-Compose gesetzt werden; diese Datei ist nur Spiegel.
- traefik.http.routers.komodo.middlewares=komodo-allowlist@docker
- traefik.http.middlewares.komodo-allowlist.ipallowlist.sourcerange=100.64.0.0/10,192.168.178.0/24
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
+1 -2
View File
@@ -59,7 +59,7 @@ Stand 2026-06-11 ist der Betrieb auf V1+ (validierte Bash-Host-Jobs mit ntfy):
# Frische-Check # Frische-Check
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh freshness
# Dienst-Restore-Check (vaultwarden|gitea|paperless|immich|authelia|adguard|redis|homeassistant|komodo-bootstrap|nextcloud|hetzner-snapshot) # Dienst-Restore-Check (vaultwarden|gitea|paperless|immich|authelia|adguard|redis|homeassistant|komodo-bootstrap|nextcloud)
bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh <dienst> bash /mnt/user/services/homelab-infra/ops/restore-tests/run-restore-checks.sh <dienst>
# Negativtest des Alarmwegs (quartalsweise) # Negativtest des Alarmwegs (quartalsweise)
@@ -79,7 +79,6 @@ Einziger Status-Ort ist die **Reifegrad-Tabelle** in `docs/RESTORE_MATRIX.md`
- **Immich:** Foto-Dateien-Restore ist bewusst nicht Teil des Smokes (separater DR-Drill); Test-Postgres nutzt das produktive VectorChord-Image. - **Immich:** Foto-Dateien-Restore ist bewusst nicht Teil des Smokes (separater DR-Drill); Test-Postgres nutzt das produktive VectorChord-Image.
- **Home Assistant:** nutzt das neueste HA-native Backup-Artefakt und eine Kopie der Mosquitto-Appdata; Testcontainer laufen nur auf localhost-Ports, ohne Traefik/Public Route. - **Home Assistant:** nutzt das neueste HA-native Backup-Artefakt und eine Kopie der Mosquitto-Appdata; Testcontainer laufen nur auf localhost-Ports, ohne Traefik/Public Route.
- **Unraid-Flash / Tailscale:** noch ohne vollstaendigen Erstlauf - `unraid-flash-runbook.md`, `tailscale-runbook.md`; offene Schritte in `docs/MASTER_TODO.md`. - **Unraid-Flash / Tailscale:** noch ohne vollstaendigen Erstlauf - `unraid-flash-runbook.md`, `tailscale-runbook.md`; offene Schritte in `docs/MASTER_TODO.md`.
- **Hetzner-Snapshot:** Infrastruktur-Test (kein Service-Restore): prueft `.zfs/snapshot` der Storage Box (Existenz, Retention, Einzeldatei-Restore) und belegt den snapshot-basierten Off-site-Schutz. Dispatcher `hetzner-snapshot`, Runbook `hetzner-snapshot-runbook.md`. Live validiert 2026-06-23 (7 Snapshots, Einzeldatei-Restore ok); monatlich im `schedule.md`.
## Naechste Ausbaustufen ## Naechste Ausbaustufen
@@ -1,187 +0,0 @@
#!/bin/bash
set -euo pipefail
# Hetzner Storage Box Snapshot Restore Test
#
# Belegt, dass der Off-site-Schutz wirklich greift. Append-only ist bewusst NICHT
# umgesetzt (DECISIONS 2026-06-01); der Schutz ist snapshot-basiert (DECISIONS
# 2026-06-11): operative Borg-Creds koennen weiter prune/compact, die ZFS-
# Snapshots der Storage Box sind hostseitig aber nicht loeschbar. Dieser Test
# macht den am 2026-06-23 manuell gefuehrten Beweis wiederholbar und ueberwachbar.
#
# Scope (READ-ONLY gegen die Storage Box, ueber den borg-ui-Container):
# 1. .zfs/snapshot/ listen -> Anzahl + neuesten Snapshot bestimmen (Retention)
# 2. Alter des neuesten Snapshots aus dem Namen pruefen (Automatic-<ISO>)
# 3. eine kleine Datei (Borg-Repo `README`) aus dem neuesten Snapshot per SFTP
# in den Container nach /tmp holen, Groesse + SHA256 pruefen, danach loeschen
# 4. Report nach /mnt/user/backups/restore-reports/
#
# KEIN Schreibzugriff auf die Box, kein borg prune/compact, keine produktiven Pfade.
#
# Verbindung wird aus der in borg-ui konfigurierten Borg-Repo-URL abgeleitet
# (kein Secret im Skript). SSH-Key + known_hosts liegen bereits im borg-ui-
# Container und werden via BORG_RSH-Konvention genutzt.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
. "$SCRIPT_DIR/common.sh"
WHATIF=0
for arg in "$@"; do
case "$arg" in
--what-if) WHATIF=1 ;;
*) echo "Unknown argument: $arg" >&2; exit 1 ;;
esac
done
REPORT_ROOT="/mnt/user/backups/restore-reports"
REPORT_FILE="$REPORT_ROOT/hetzner-snapshot-$(date +%F).md"
# Erwartungswerte = Zielbild lt. DECISIONS 2026-06-11 (taeglich, Retention 7 Tage).
MIN_SNAPSHOTS="${MIN_SNAPSHOTS:-5}"
MAX_SNAPSHOT_AGE_HOURS="${MAX_SNAPSHOT_AGE_HOURS:-48}"
SNAPSHOT_DIR="${SNAPSHOT_DIR:-.zfs/snapshot}"
PROBE_FILE="${PROBE_FILE:-README}" # jede Borg-Repo-Wurzel hat README + config
SNAPSHOT_NAME_GLOB="${SNAPSHOT_NAME_GLOB:-Automatic-}"
# Zusaetzliche ssh/sftp-Optionen; Default nutzt borg-uis known_hosts wie BORG_RSH.
SNAPSHOT_SSH_OPTS="${SNAPSHOT_SSH_OPTS:--o UserKnownHostsFile=/data/known_hosts -o StrictHostKeyChecking=yes -o BatchMode=yes}"
PROBE_TMP_DIR="${PROBE_TMP_DIR:-/tmp/hetzner-snapshot-probe}"
if [ "$WHATIF" -eq 1 ]; then
cat <<EOF
Hetzner Storage Box snapshot restore test
Mode: WhatIf
Container: $BORG_CONTAINER
Snapshot dir (rel. login home): $SNAPSHOT_DIR
Probe file: <repo>/$PROBE_FILE
Min snapshots: $MIN_SNAPSHOTS
Max age (h): $MAX_SNAPSHOT_AGE_HOURS
Scope: list snapshots + SFTP get one small file from newest snapshot + sha256
Note: connection derived from borg-ui repo URL; no productive write.
EOF
exit 0
fi
require_cmd docker
require_cmd date
require_borg_container
# --- Borg-Repo-URL aus borg-ui-DB; daraus user/host/port + Repo-Verzeichnis ----
repo="$(borg_repo_url)"
if [ -z "$repo" ]; then
echo "Could not resolve Borg repo URL from borg-ui database" >&2
exit 1
fi
# Unterstuetzte Formen: ssh://user@host:port/./reldir | user@host:reldir
proto_stripped="${repo#ssh://}"
if [ "$proto_stripped" != "$repo" ]; then
userhostport="${proto_stripped%%/*}"
repo_path="/${proto_stripped#*/}"
else
userhostport="${proto_stripped%%:*}"
repo_path="${proto_stripped#*:}"
fi
ssh_user="${userhostport%%@*}"
hostport="${userhostport#*@}"
ssh_host="${hostport%%:*}"
if [ "$hostport" = "$ssh_host" ]; then ssh_port=22; else ssh_port="${hostport##*:}"; fi
# Repo-Verzeichnis relativ zum Login-Home: fuehrende /, ./ und /./ entfernen
repo_dir="$repo_path"
repo_dir="${repo_dir#/}"; repo_dir="${repo_dir#./}"; repo_dir="${repo_dir#/}"
if [ -z "$ssh_user" ] || [ -z "$ssh_host" ] || [ -z "$repo_dir" ]; then
echo "Could not parse user/host/repo-dir from repo URL: $repo" >&2
exit 1
fi
run_sftp() { # liest Batch-Kommandos von stdin
# shellcheck disable=SC2086
docker exec -i "$BORG_CONTAINER" sftp -q -P "$ssh_port" $SNAPSHOT_SSH_OPTS -b - "$ssh_user@$ssh_host"
}
# --- 1) Snapshots listen --------------------------------------------------------
snap_list="$(printf 'ls -1 %s\nbye\n' "$SNAPSHOT_DIR" | run_sftp 2>/dev/null \
| tr -d '\r' | grep -F "$SNAPSHOT_NAME_GLOB" | sed 's#.*/##' | sort -u || true)"
if [ -z "$snap_list" ]; then
echo "No snapshots found in $SNAPSHOT_DIR on $ssh_host (glob: $SNAPSHOT_NAME_GLOB)" >&2
exit 1
fi
snap_count="$(printf '%s\n' "$snap_list" | grep -c . || true)"
newest="$(printf '%s\n' "$snap_list" | sort | tail -n 1)"
# --- 2) Alter des neuesten Snapshots aus dem Namen ableiten ---------------------
# Format: Automatic-YYYY-MM-DDTHH-MM-SS
age_hours="unknown"
ts="${newest#${SNAPSHOT_NAME_GLOB}}"
date_part="${ts%%T*}"
time_part="${ts#*T}"
time_colons="$(printf '%s' "$time_part" | tr '-' ':')"
if snap_epoch="$(date -d "$date_part $time_colons" +%s 2>/dev/null)"; then
now_epoch="$(date +%s)"
age_hours="$(( (now_epoch - snap_epoch) / 3600 ))"
fi
# --- 3) Einzeldatei aus dem neuesten Snapshot holen + pruefen -------------------
remote_probe="$SNAPSHOT_DIR/$newest/$repo_dir/$PROBE_FILE"
docker exec -i "$BORG_CONTAINER" sh -c "rm -rf '$PROBE_TMP_DIR' && mkdir -p '$PROBE_TMP_DIR'"
probe_ok="no"
probe_size=0
probe_sha256="n/a"
if printf 'get %s %s/%s\nbye\n' "$remote_probe" "$PROBE_TMP_DIR" "$PROBE_FILE" | run_sftp 2>/dev/null; then
if docker exec -i "$BORG_CONTAINER" test -s "$PROBE_TMP_DIR/$PROBE_FILE"; then
probe_ok="yes"
probe_size="$(docker exec -i "$BORG_CONTAINER" stat -c '%s' "$PROBE_TMP_DIR/$PROBE_FILE" 2>/dev/null || echo 0)"
probe_sha256="$(docker exec -i "$BORG_CONTAINER" sha256sum "$PROBE_TMP_DIR/$PROBE_FILE" 2>/dev/null | awk '{print $1}' || echo n/a)"
fi
fi
# Temp im Container wieder loeschen (kein Datenrest)
docker exec -i "$BORG_CONTAINER" rm -rf "$PROBE_TMP_DIR" >/dev/null 2>&1 || true
# --- Bewertung ------------------------------------------------------------------
result="SUCCESS"
fail_reason=""
if [ "$probe_ok" != "yes" ]; then
result="FAILED"; fail_reason="Einzeldatei-Restore aus Snapshot fehlgeschlagen ($remote_probe)"
elif [ "$snap_count" -lt "$MIN_SNAPSHOTS" ]; then
result="FAILED"; fail_reason="Zu wenige Snapshots: $snap_count < $MIN_SNAPSHOTS"
elif [ "$age_hours" != "unknown" ] && [ "$age_hours" -gt "$MAX_SNAPSHOT_AGE_HOURS" ]; then
result="FAILED"; fail_reason="Neuester Snapshot zu alt: ${age_hours}h > ${MAX_SNAPSHOT_AGE_HOURS}h"
fi
write_report "$REPORT_FILE" <<EOF
# Hetzner Storage Box Snapshot Restore Test - $(date +%F)
- Scope: \`Off-site Snapshot-Schutz (nicht append-only)\`
- Storage Box host: \`$ssh_host\`
- Borg repo dir: \`$repo_dir\`
- Snapshot dir: \`$SNAPSHOT_DIR\`
- Result: \`$result\`
## Checks
- Snapshots gefunden: \`$snap_count\` (min \`$MIN_SNAPSHOTS\`)
- Neuester Snapshot: \`$newest\`
- Alter neuester Snapshot: \`${age_hours}h\` (max \`${MAX_SNAPSHOT_AGE_HOURS}h\`)
- Probe-Datei: \`$repo_dir/$PROBE_FILE\`
- Einzeldatei-Restore aus Snapshot: \`$probe_ok\`
- Probe-Groesse: \`${probe_size} B\`
- Probe-SHA256: \`$probe_sha256\`
$( [ -n "$fail_reason" ] && echo "- Fehlergrund: \`$fail_reason\`" )
## Notes
- READ-ONLY: nur \`ls\` + \`get\` einer kleinen Datei via SFTP; kein Schreibzugriff,
kein borg prune/compact, keine produktiven Pfade.
- Verbindung aus der borg-ui-Repo-URL abgeleitet; Secrets/known_hosts bleiben im Container.
- Schutzmodell ist snapshot-basiert, append-only bewusst nicht (DECISIONS 2026-06-01/-11).
EOF
if [ "$result" != "SUCCESS" ]; then
echo "Hetzner snapshot restore test FAILED: $fail_reason -> $REPORT_FILE" >&2
exit 1
fi
echo "Hetzner snapshot restore test ok ($snap_count snapshots, newest $newest, probe ${probe_size}B) -> $REPORT_FILE"
@@ -1,44 +0,0 @@
# Hetzner Storage Box Snapshot Restore - Runbook
Typ: Runbook · Stand: 2026-06-23 · Status: aktiv (live validiert 2026-06-23)
Belegt den Off-site-Schutz der Hetzner Storage Box. Append-only ist bewusst NICHT
umgesetzt (DECISIONS 2026-06-01); der Schutz ist snapshot-basiert (DECISIONS
2026-06-11): operative Borg-Creds koennen weiter `prune`/`compact`, die ZFS-
Snapshots der Box sind hostseitig nicht loeschbar (taeglich 05:30 UTC, Retention 7 Tage).
## Was der Test tut
`ops/restore-tests/hetzner-snapshot-restore-test.sh` (Dispatcher: `hetzner-snapshot`):
1. listet `.zfs/snapshot/` auf der Box (READ-ONLY, via SFTP aus dem `borg-ui`-Container)
2. zaehlt Snapshots (Retention) + prueft das Alter des neuesten aus dessen Namen
3. holt eine kleine Datei (`<repo>/README`) aus dem neuesten Snapshot, prueft Groesse + SHA256
4. loescht die Temp-Datei und schreibt einen Report nach `/mnt/user/backups/restore-reports/`
Verbindung (user/host/port/Repo-Verzeichnis) wird aus der in `borg-ui` konfigurierten
Borg-Repo-URL abgeleitet; SSH-Key + `known_hosts` liegen bereits im Container. Kein
Secret im Skript, kein Schreibzugriff, kein `prune`/`compact`.
## Manuell belegter Referenzlauf (2026-06-23, Codex)
- Box `u565255.your-storagebox.de`, `.zfs/snapshot` sichtbar
- Snapshots `2026-06-17`..`2026-06-23`, je `05:30` -> 7 Tage Retention
- neuester: `Automatic-2026-06-23T05-30-24`
- Probe `hetzner_borg_appdata_critical/README`, 73 B, SHA256 erzeugt, Temp geloescht
## Live-Validierung (2026-06-23, Codex): erfolgreich
Erstlauf ohne ENV-Anpassung gruen (`SUCCESS`), Defaults passten direkt:
- Report: `/mnt/user/backups/restore-reports/hetzner-snapshot-2026-06-23.md`
- 7 Snapshots gefunden, neuester `Automatic-2026-06-23T05-30-24`
- Einzeldatei-Restore aus `.zfs/snapshot` erfolgreich
ENV-Override (`SNAPSHOT_DIR`, `PROBE_FILE`, `SNAPSHOT_NAME_GLOB`,
`SNAPSHOT_SSH_OPTS`, `MIN_SNAPSHOTS`, `MAX_SNAPSHOT_AGE_HOURS`) bleibt fuer
kuenftige Abweichungen verfuegbar.
**Verbleibender Host-Schritt (Codex/Du):** Unraid-User-Script
`restore-hetzner-snapshot-monthly` anlegen (Cron `0 6 15 * *`), Vorlage in
`unraid-user-scripts.md`.
+1 -7
View File
@@ -103,14 +103,8 @@ case "$MODE" in
fi fi
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh" exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
;; ;;
hetzner-snapshot)
if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/hetzner-snapshot-restore-test.sh" --what-if
fi
exec "$SCRIPT_DIR/hetzner-snapshot-restore-test.sh"
;;
*) *)
echo "Usage: $0 {freshness|freshness-negative|vaultwarden|gitea|paperless|immich|authelia|adguard|redis|homeassistant|nextcloud|komodo-bootstrap|komodo-mongo-restore|traefik|mailarchiver|mealie|shared-pg-cluster|hetzner-snapshot} [--what-if]" >&2 echo "Usage: $0 {freshness|freshness-negative|vaultwarden|gitea|paperless|immich|authelia|adguard|redis|homeassistant|nextcloud|komodo-bootstrap|komodo-mongo-restore|traefik|mailarchiver|mealie|shared-pg-cluster} [--what-if]" >&2
exit 1 exit 1
;; ;;
esac esac
-4
View File
@@ -19,7 +19,6 @@ Monatlich:
- `vaultwarden` Mini-Restore - `vaultwarden` Mini-Restore
- `gitea` Mini-Restore, versetzt zum Vaultwarden-Lauf - `gitea` Mini-Restore, versetzt zum Vaultwarden-Lauf
- `hetzner-snapshot` Storage-Box Snapshot-Restore-Test (read-only `.zfs/snapshot`; Erstlauf 2026-06-23 erfolgreich)
Alle 2 Monate: Alle 2 Monate:
@@ -70,8 +69,6 @@ Immich am 2026-05-27; Paperless erneut am 2026-05-31; Authelia am
- DR-/Restore-Sanity-Check - DR-/Restore-Sanity-Check
- Quartalsweise am 2. Sonntag im zweiten Quartalsmonat, 08:30: - Quartalsweise am 2. Sonntag im zweiten Quartalsmonat, 08:30:
- `immich` - `immich`
- Jeden 15. des Monats, 06:00:
- `hetzner-snapshot`
## Unraid User Scripts Cron ## Unraid User Scripts Cron
@@ -86,7 +83,6 @@ Vixie-Cron (Unraid) verknuepft `day-of-month` und `day-of-week` mit **OR**, soba
| `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-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 | | `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 | | `monthly-random-restore` | `0 9 1 * *` | - | erster Kalendertag im Monat 09:00 |
| `restore-hetzner-snapshot-monthly` | `0 6 15 * *` | - | 15. des Monats 06:00 (read-only Snapshot-Test) |
**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. **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.
-11
View File
@@ -52,17 +52,6 @@ services:
- traefik.http.routers.vaultwarden.tls=true - traefik.http.routers.vaultwarden.tls=true
- traefik.http.routers.vaultwarden.tls.certresolver=le - traefik.http.routers.vaultwarden.tls.certresolver=le
- traefik.http.services.vaultwarden.loadbalancer.server.port=80 - traefik.http.services.vaultwarden.loadbalancer.server.port=80
# Audit 2026-06-23 (P1): /admin war public mit 200 erreichbar. Zweiter, hoeher
# priorisierter Router scoped auf /admin und laesst nur Tailnet + LAN durch (sonst 403).
# Hauptrouter oben bleibt nativ, damit Browser-/Mobile-Clients von ueberall funktionieren.
- traefik.http.routers.vaultwarden-admin.rule=Host(`vault.kaleschke.info`) && PathPrefix(`/admin`)
- traefik.http.routers.vaultwarden-admin.entrypoints=websecure
- traefik.http.routers.vaultwarden-admin.tls=true
- traefik.http.routers.vaultwarden-admin.tls.certresolver=le
- traefik.http.routers.vaultwarden-admin.service=vaultwarden
- traefik.http.routers.vaultwarden-admin.priority=100
- traefik.http.routers.vaultwarden-admin.middlewares=vaultwarden-admin-allowlist@docker
- traefik.http.middlewares.vaultwarden-admin-allowlist.ipallowlist.sourcerange=100.64.0.0/10,192.168.178.0/24
networks: networks:
frontend_net: frontend_net: