60 Commits

Author SHA1 Message Date
renovate af4b7015ee chore(deps): update traefik:v3.7 docker digest to d685879 2026-06-10 22:20:18 +00:00
Micha d48d473942 docs: add homelab optimization assessment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 20:40:05 +02:00
Micha e80e5dd49f renovate: komodo-Stack (inline-managed) aus Tracking nehmen
Der komodo-Stack wird in Komodo inline (file_contents) verwaltet, nicht aus dem Repo deployed. Renovate-PRs darauf wirken zur Laufzeit nicht und erzeugen Git-Komodo-Scheinsicherheit. Daher: ops/komodo/** in ignorePaths, mongo-Digest auf den real laufenden Stand zurueckgesetzt, Inline-Ausnahme in docs/RENOVATE.md und im Compose-Header dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 20:34:52 +02:00
Micha 3c339474a7 Merge pull request 'chore(deps): update mongo:8.0.23 docker digest to 73ee318' (#13) from renovate/mongo-8.0.23 into master
Reviewed-on: #13
2026-06-10 18:27:13 +00:00
Micha c79afdfab0 Merge pull request 'chore(deps): update docker.n8n.io/n8nio/n8n docker tag to v2.26.2' (#17) from renovate/docker.n8n.io-n8nio-n8n-2.x into master
Reviewed-on: #17
2026-06-10 18:24:23 +00:00
Micha 8172793c68 Merge pull request 'chore(deps): update nextcloud docker tag to v33.0.5' (#16) from renovate/nextcloud-33.x into master
Reviewed-on: #16
2026-06-10 18:19:56 +00:00
Micha 8e46440944 Merge pull request 'chore(deps): update shaanmajid/unbound:1.25.1 docker digest to f140db0' (#14) from renovate/shaanmajid-unbound-1.25.1 into master
Reviewed-on: #14
2026-06-10 18:13:58 +00:00
Micha dfe1dc1c99 Merge pull request 'chore(deps): update traefik:v3.7 docker digest to fcdef59' (#15) from renovate/traefik-v3.7 into master
Reviewed-on: #15
2026-06-10 18:06:09 +00:00
Micha 4007da3302 docs: Runbook fuer Komodo-Bulk-Deploy-DNS-Ausfall
Bulk-Renovate-Merge loest parallele Komodo-Deploys aus; Image-Pulls scheitern mit DNS connection refused, weil AdGuard (einziger Host-Resolver) im selben Batch recreated wird. Runbook haelt Symptom, Ursache, Sofortmassnahme (Unraid DNS2) und Betriebsregel fest. Verweis in REPO_MAP ergaenzt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 19:52:34 +02:00
Micha 9836ea3c4f Merge pull request 'chore(deps): update minor-and-patch-updates' (#12) from renovate/minor-patch-updates into master
Reviewed-on: #12
2026-06-10 14:41:25 +00:00
renovate 803f84b3af chore(deps): update docker.n8n.io/n8nio/n8n docker tag to v2.26.2 2026-06-10 14:32:41 +00:00
renovate d05ca63545 chore(deps): update nextcloud docker tag to v33.0.5 2026-06-10 14:32:09 +00:00
renovate 9847baf327 chore(deps): update minor-and-patch-updates 2026-06-10 14:32:08 +00:00
renovate 8ec5bc55d9 chore(deps): update traefik:v3.7 docker digest to fcdef59 2026-06-10 14:31:35 +00:00
renovate 9c844074e0 chore(deps): update shaanmajid/unbound:1.25.1 docker digest to f140db0 2026-06-10 14:31:33 +00:00
Micha c126b71852 renovate: Kritische Kerninfra aus minor-patch-Sammel-PR ausgliedern
Traefik (Public-Entrypoint), Unbound (DNS), n8n und Nextcloud bekommen eigene PRs statt im gruppierten minor-and-patch-updates-PR zu landen. Erzwingt kontrollierten, einzeln reviewbaren Merge pro kritischem Dienst (WORKFLOW.md: keine mehreren kritischen Dienste gleichzeitig migrieren).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 16:22:12 +02:00
Micha e89b88a513 report: Noise-Eskalations-Befreiung fuer dauerhaft-laute Benign-Patterns
Bekannte Noise-Patterns, die strukturell sehr laut sind (Unraid-mdadm-Spam
~5760/Tag, Fritz!Box-SOA ~1170/Tag), hielten den Report-Status dauerhaft
auf >= WARNUNG ueber noise_threshold_exceeded und entwerteten damit die
Ampel - das System konnte nie OK erreichen, egal wie gesund.

Neue Datei noise-escalation-exempt.patterns listet solche Patterns. Sie
zaehlen weiterhin als Noise und erscheinen in der Breakdown-Tabelle (jetzt
mit Hinweis-Spalte "eskalations-befreit"), zaehlen aber nicht mehr zu
noise_threshold_exceeded. Neue/unerwartete laute Patterns eskalieren
weiterhin zur WARNUNG. Jede Befreiung traegt Begruendung + Recheck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 15:15:10 +02:00
Micha 8bb250220b immich-ml: --no-control-socket gegen gunicorn-25.1.0 Worker-Hang
Ersetzt den wirkungslosen LD_PRELOAD-Versuch durch den dokumentierten
Root-Cause-Fix. immich_machine_learning blieb dauerhaft unhealthy: der
gunicorn-Worker haengt nach "Control socket listening" in futex und
erreicht nie "Application startup complete" (/ping -> Timeout). Ursache ist
der in gunicorn 25.1.0 neu eingefuehrte, fehlerhafte Control-Socket
(bestaetigt: gunicorn#3510, immich#27228, Regression seit Immich 2.6).

GUNICORN_CMD_ARGS=--no-control-socket deaktiviert das Feature. immich-ml
startet gunicorn als Subprozess (python -m gunicorn), der GUNICORN_CMD_ARGS
aus der Env liest und anhaengt; das Flag --no-control-socket
(config: control_socket_disable) ist in diesem gunicorn-Build als gueltig
verifiziert. Reversibel; bei gefixter Immich-Release wieder entfernen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 12:36:01 +02:00
Micha 2f64aee109 report: unhealthy-Container namentlich + Image-Age-Allowlist
Zwei Verbesserungen am Daily Operations Report, ausgeloest durch den
versteckten immich_machine_learning-Ausfall (lief 2,3 Tage unhealthy,
weil der Report nur "unhealthy=1" zaehlte, ohne Name/Grund):

1. collect_container_state: neue Sektion "Unhealthy Container" listet jeden
   unhealthy Container mit FailingStreak und letztem Healthcheck-Output.
   So ist sofort sichtbar WELCHER Container und WARUM.

2. collect_image_freshness: neue Image-Age-Allowlist
   (image-age-allow.patterns). Bewusst gepinnte, aber aktuelle/empfohlene
   Images (immich_postgres = exakt Immichs Pin; blackbox-exporter v0.28.0 =
   latest) werden mit Recheck-Datum von der Ueberalterungs-Warnung
   ausgenommen. Nach Ablauf des Recheck-Datums greift die Ausnahme nicht
   mehr -> erzwingt Neubewertung statt stillen Alterns. Top-10-Tabelle hat
   jetzt eine Hinweis-Spalte (ueberaltert / bewusst gepinnt).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:08:44 +02:00
Micha ed55b88ec1 immich-ml: LD_PRELOAD leeren gegen gunicorn-25.1.0-Fork-Deadlock
immich_machine_learning haengt seit dem 7.6. unhealthy: der gunicorn-Worker
bleibt nach "Control socket listening" in futex_do_wait stehen und erreicht
nie "Application startup complete" (/ping -> ConnectTimeout/ReadTimeout).
Kein OOM (22 GB frei), kein Disk-I/O-Wait, laeuft als root, Socket wird
erstellt - klassischer Fork-Deadlock von mimalloc (LD_PRELOAD) im geforkten
Worker unter gunicorn 25.1.0.

mimalloc per LD_PRELOAD="" deaktiviert. Reine Allocator-Optimierung,
funktional unkritisch, reversibel. Bekannte Upstream-Regression seit
Immich 2.6 (immich#27228, #22317) ohne offiziellen Fix; Restart und
force-recreate sind dort als wirkungslos dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 11:02:27 +02:00
Micha ce747f687f ops-report: cert-dedup, blackbox-DNS auf AdGuard, neue Noise-Patterns
Behebt drei Befunde aus dem Operations-Report 2026-06-10:

- daily-status-report.sh: Zertifikate werden vor der Auswertung pro
  Domain-Set dedupliziert; nur das laengstlaufende Cert zaehlt. Traefik
  haelt waehrend der Erneuerung altes + neues Cert in acme.json, was
  bisher eine falsche KRITISCH-Warnung (traefik.kaleschke.info 5 Tage)
  ausloeste, obwohl das neue Cert 65 Tage Restlaufzeit hat.

- monitoring/blackbox-exporter: DNS von 1.1.1.1/8.8.8.8 auf AdGuard
  (172.23.0.3 via dns_net) umgestellt. Externe Resolver lieferten die
  WAN-IP, was Hairpin-NAT-Timeouts (9,5s) bei Probes von cloud/glances
  verursachte (662 Fehler/Tag).

- log-noise.patterns: Fritz!Box-SOA-Fehler (AdGuard, RFC-1035-Verstoss)
  und fehlendes grafana-amazonprometheus-datasource-Plugin als bekanntes
  Rauschen klassifiziert (~1800 Zeilen/Tag).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 10:06:52 +02:00
renovate cf11b4d75b chore(deps): update mongo:8.0.23 docker digest to 73ee318 2026-06-10 04:21:10 +00:00
Micha 796901ec6b docs(network): Post-Upgrade Posture-Recheck Unraid 7.3.1 + AdGuard/libvirt-:53-Vorfall
Nach Major-Upgrade 7.2.4 -> 7.3.1 read-only Host-Listener gegen dokumentierte
Annahmen geprueft: alle Ausnahmen intakt (InfluxDB 127.0.0.1:8181, AdGuard nur
Tailscale, Gitea-SSH 222 LAN/TS, Traefik einziger 80/443-Owner, libvirt :53 weg).
Docker-Socket-Lage festgehalten (nur komodo-periphery RW; Traefik C-3 ro, kein Regress).
AdGuard-Boot-Race (libvirt-Default-Netz belegte :53 vor AdGuard) + Fix dokumentiert;
Dauerfix-Empfehlung VM-Manager aus. SSH-Haertung nach Upgrade verifiziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 21:26:59 +02:00
Micha de7b714b4d docs(network): SSH-Host-Haertung dokumentieren (key-only root, upgrade-sichere Persistenz)
Host gehaertet 2026-06-07: PermitRootLogin prohibit-password,
PasswordAuthentication no, KbdInteractiveAuthentication no; PubkeyAuthentication yes.
Persistenz upgrade-sicher via idempotentem /boot/config/ssh-harden.sh aus
/boot/config/go (sshd -t vor HUP-Reload, Syslog-Selbst-Verifikation).
Manueller Post-Upgrade-Check und Rollback dokumentiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 11:02:07 +02:00
Micha 8045e22873 authelia-oidc: Immich+Nextcloud bis Onboarding geparkt; aktive Phase abgeschlossen
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:18:54 +02:00
Micha 52f8c2adcb posture-check: Tailscale-Docker aus CRITICAL_CONTAINERS entfernen (Container abgebaut)
Verhindert Dauer-ntfy-Alarm fuer den entfernten userspace-Docker-Tailscale. Natives Tailscale-Plugin ist kein Container und wird hier bewusst nicht geprueft.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 19:15:07 +02:00
Micha 0ddae675a8 plex: add web redirect for public route 2026-06-06 13:45:47 +02:00
Micha 7ce8e948cd plex: route host network service via traefik file 2026-06-06 13:44:22 +02:00
Micha 2a87220862 plex: expose via traefik domain 2026-06-06 13:41:39 +02:00
Micha f2d4cad566 paperless: Authelia OIDC SSO additiv (allauth, extra_hosts)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:41:16 +02:00
Micha e7370e4820 authelia-oidc: Mealie erledigt + extra_hosts-Gotcha dokumentieren
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:37:34 +02:00
Micha dc26eb313c mealie: extra_hosts auth.kaleschke.info -> Host-IP fuer OIDC-Erreichbarkeit
Mealie-Container konnte auth.kaleschke.info nicht aufloesen/erreichen (httpx.ConnectTimeout beim OIDC-Discovery). extra_hosts-Muster wie Komodo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:34:09 +02:00
Micha dc7cbfa6cd mealie: Authelia OIDC SSO additiv (lokaler Login bleibt)
OIDC_AUTH_ENABLED + Authelia-Provider, Secret via ${MEALIE_OIDC_CLIENT_SECRET} (Stack-ENV). Kein Auto-Redirect, Self-Signup an. Authelia-Client mealie (one_factor) host-seitig angelegt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:29:01 +02:00
Micha cf9ca59eb1 docs: close baerchen veeam recovery test 2026-06-06 13:27:31 +02:00
Micha d2a9c3b8cb docs: record baerchen veeam recovery usb boot 2026-06-06 13:25:53 +02:00
Micha 0177350e64 docs: close guest iot network setup 2026-06-06 13:23:35 +02:00
Micha 2f3a029098 authelia-oidc: Grafana-Proof als erledigt dokumentieren + Secret eintragen
- SECRETS_MAP: grafana_oidc_client_secret (Datei + __FILE, Hash in Authelia-Host-Config)
- AUTHELIA_OIDC_PLAN: Stufe 1 (Grafana) als erledigt markiert
- MASTER_TODO: OIDC-Proof verifiziert, naechster Schritt Familien-Apps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:17:29 +02:00
Micha a4c79d9d81 docs: record guest iot preflight 2026-06-06 13:14:07 +02:00
Micha 18a90fbb4b ops: add guest iot network preflight 2026-06-06 13:13:01 +02:00
Micha 30f076c85a monitoring/grafana: OIDC-SSO via Authelia (Stufe-1-Proof)
- generic_oauth gegen Authelia (client_id grafana, PKCE, client_secret via __FILE aus /mnt/user/appdata/secrets/grafana_oidc_client_secret)
- Traefik-Middleware authelia@file entfernt -> OIDC ist jetzt die Auth; lokaler Grafana-Admin bleibt Fallback
- Authelia-Client wurde host-seitig angelegt (Secret nur als Host-Datei + Hash in Authelia-Config)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 13:11:00 +02:00
Micha 6e65f81503 docs: record restore freshness negative alert test 2026-06-06 13:04:42 +02:00
Micha 6123584a02 ops: make freshness negative ntfy call robust 2026-06-06 13:03:05 +02:00
Micha c33e29016b ops: add restore freshness negative alert test 2026-06-06 13:02:14 +02:00
Micha 2628a0c795 authelia-oidc: Plan + Runbook fuer app-uebergreifendes SSO
- docs/AUTHELIA_OIDC_PLAN.md: v4.39-Client-Schema, Endpoints, Secret-Erzeugung, Rollout-Reihenfolge (Grafana-Proof zuerst, dann Familien-Apps), Grafana-Schritt-fuer-Schritt
- MASTER_TODO: OIDC-Punkt auf Plan verweisen, naechster Schritt Grafana-Proof
- README: Doku-Index ergaenzt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:58:38 +02:00
Micha c7eed6bdad todo: Authelia Rest-2FA als komplett erledigt markieren (Host-Merge + 2FA-Login verifiziert)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:55:23 +02:00
Micha 6c61ad3860 authelia: Repo-Baseline an Host-2FA-Endzustand angleichen
Expliziten 2FA-Block auf files/scrutiny reduziert (borg/code sind via
Catch-all *.kaleschke.info=two_factor weiterhin 2FA). Damit matcht die
Repo-access_control-Sektion den Host-Stand -> authelia-diff.sh wird clean,
sobald der Host-Repo-Mirror auf diesen Commit gezogen ist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:53:20 +02:00
Micha 2d1b541847 todo: offene Operator-Entscheidungen abschliessen; Authelia alle UIs auf 2FA
- BitLocker baerchen: bewusst deaktiviert
- Veeam Storage Encryption: bewusst unverschluesselt
- Stromverbrauch: bewusst ohne Messung (geschlossen)
- Nextcloud 2FA: geparkt bis OIDC die App-Login-Ebene erreicht
- Authelia: Catch-all *.kaleschke.info one_factor -> two_factor (Repo-Baseline; Host-Merge + restart + authelia-diff.sh als aktiver Schritt offen)
- Authelia OIDC und Gast-/IoT-Netz als aktive Bloecke aufgenommen
- MASTER_TODO: Operator-Entscheidung-Sektion ohne offene Punkte

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 12:32:52 +02:00
Micha c3491eb382 tailscale: auf natives Plugin konsolidieren, redundanten Docker-Stack entfernen, ACL-Haertung dokumentieren
- host-services/tailscale/ (userspace-only Docker-Stack) entfernt; Komodo stop/destroy durch Operator, danach git rm
- Glance-Widget Tailscale-Docker entfernt
- HOMELAB_ARCHITECTURE/SERVICE_CATALOG/DISASTER_RECOVERY/CLAUDE/RESTORE_MATRIX: tailscale als natives Unraid-Plugin dokumentiert; Restore-State-Pfad korrigiert auf /boot/config/plugins/tailscale/state (Flash-Backup)
- NETWORK_INVENTORY: restriktive tag-basierte grants-ACL (2026-06-06; tag:server/tag:operator, tag:family vorbereitet) und Subnet-Router-Befund dokumentiert

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-06 10:58:59 +02:00
Micha 023ee63687 docs: close dr workstation kit 2026-06-06 10:11:17 +02:00
Micha 3a263a4846 docs: update dr workstation readiness 2026-06-06 09:17:23 +02:00
Micha 68d3ace598 ops: add dr workstation readiness check 2026-06-06 08:40:31 +02:00
Micha 0ef98a23e1 docs: close baerchen app license checks 2026-06-06 08:31:17 +02:00
Micha 6353da47c5 ops: add baerchen app license readiness check 2026-06-06 08:27:10 +02:00
Micha 207f49f001 docs: retire home assistant influx todo 2026-06-06 08:22:27 +02:00
Micha a687d9b73e docs: record redis restore test 2026-06-06 08:11:03 +02:00
Micha e3459c76d0 fix: use redis pre-cutover restore artifact 2026-06-06 08:08:52 +02:00
Micha 254eb81496 ops: add redis restore test 2026-06-06 08:07:11 +02:00
Micha 9a6d7123ce docs: record adguard restore test 2026-06-06 08:03:53 +02:00
Micha 151d253aff ops: add adguard restore test 2026-06-06 08:01:27 +02:00
Micha dda6021116 docs: record tailscale acl plan and watcher activation 2026-06-05 23:27:40 +02:00
67 changed files with 2571 additions and 181 deletions
+1 -1
View File
@@ -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
+7 -4
View File
@@ -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**:
@@ -240,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
@@ -273,7 +274,7 @@ 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 | | `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 | | `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 |
@@ -308,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
@@ -407,6 +408,7 @@ Für den laufenden Betrieb gilt stattdessen:
| `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) | | `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. |
--- ---
@@ -495,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:
+11
View File
@@ -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 -1
View File
@@ -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:
+16
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
n8n: n8n:
image: docker.n8n.io/n8nio/n8n:2.22.6@sha256:07138bb60aee990651e9c2090d7dde330cba3a5bd84fcc5cba63b2997243bc45 image: docker.n8n.io/n8nio/n8n:2.26.2@sha256:61ba01bc5e39304bbc928c9dbecd938c3a5cc1331b68affba6a34d0f654c43d9
container_name: n8n container_name: n8n
restart: unless-stopped restart: unless-stopped
+1 -1
View File
@@ -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 -1
View File
@@ -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:
+8
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
super-productivity: super-productivity:
image: johannesjo/super-productivity:v18.8.0@sha256:c739caca8e0c5e83ea4a6289884079ac49e0c3c87c7f95598b5a9fb10cc2d9c4 image: johannesjo/super-productivity:v18.9.1@sha256:773760107344e739f4c29409f7842db66a1b167d50eb2c40248cb5b5b328652e
container_name: super-productivity container_name: super-productivity
restart: unless-stopped restart: unless-stopped
+1 -1
View File
@@ -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:
+1 -1
View File
@@ -11,7 +11,6 @@ in `docs/RESTORE_MATRIX.md` ergaenzt.
| Prioritaet | Punkt | Naechster Schritt | | Prioritaet | Punkt | Naechster Schritt |
|---|---|---| |---|---|---|
| P1 | DR-Workstation Bare-Metal-Kit: WSL2 + Borg-Client installieren | Hetzner-DR-SSH-Key ist 2026-06-03 erledigt und offline gesichert. Verbleibend: WSL2 auf dem Gaming-PC einrichten (`wsl --install -d Ubuntu`), `sudo apt install borgbackup` und ein erster Smoke `borg list ssh://u565255@u565255.your-storagebox.de/./hetzner_borg_appdata_critical` mit dem offline gesicherten Key + Passphrase. Bestandteile dokumentiert in `docs/EXTERNAL_DEPENDENCIES.md` Abschnitt "DR-Workstation Bare-Metal-Kit" |
| P2 | Family-Onboarding praktisch starten | Fokus: Vaultwarden als Passwortbasis, Immich-Mobile-Backup auf jedem Handy, Mealie mit erstem Rezept/Einkaufsliste; Ablauf steht in `docs/FAMILY_ONBOARDING.md` | | P2 | Family-Onboarding praktisch starten | Fokus: Vaultwarden als Passwortbasis, Immich-Mobile-Backup auf jedem Handy, Mealie mit erstem Rezept/Einkaufsliste; Ablauf steht in `docs/FAMILY_ONBOARDING.md` |
## Restore-Audit Backlog (Stand 2026-06-03) ## Restore-Audit Backlog (Stand 2026-06-03)
@@ -44,6 +43,7 @@ Ergebnis des Restore-Skills-Audits (Session 2026-06-02/03). Die kritischen Bugfi
## 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`. - 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. - 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. - 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.
+186
View File
@@ -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.
+8 -1
View File
@@ -290,7 +290,14 @@ Erfolgskriterium: `docker network ls` zeigt `frontend_net`, `backend_net`, `moni
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. **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.
+9
View File
@@ -115,6 +115,15 @@ Erwartet: HEAD und mindestens ein `refs/heads/master`-Eintrag.
Damit der "ich pruefe das vierteljaehrlich"-Schritt zur Routine wird, ein kleines Skript ins WSL-Home: 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 ```bash
cat > ~/dr-smoke.sh <<'EOF' cat > ~/dr-smoke.sh <<'EOF'
#!/bin/bash #!/bin/bash
+2 -1
View File
@@ -21,7 +21,7 @@ Dieses Dokument beschreibt externe Anbieter und Konten, von denen Betrieb, Recov
| OpenAI API | Paperless-GPT LLM und Vision-OCR | mittel | Automatische Dokument-Titel, Tags, Korrespondenten und LLM-OCR fallen aus; Paperless selbst laeuft weiter | OpenAI-Projekt/API-Key ausserhalb Repo | Key in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten | | OpenAI API | Paperless-GPT LLM und Vision-OCR | mittel | Automatische Dokument-Titel, Tags, Korrespondenten und LLM-OCR fallen aus; Paperless selbst laeuft weiter | OpenAI-Projekt/API-Key ausserhalb Repo | Key in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten |
| 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") | | 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") |
@@ -107,3 +107,4 @@ Operative Regel: Die DR-Workstation wird nicht als Test-/Spiel-PC betrachtet. WS
| 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 | 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 | 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-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 |
+1 -1
View File
@@ -25,7 +25,7 @@ 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.
+117
View File
@@ -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.
+5 -5
View File
@@ -15,7 +15,7 @@ Entscheidungen festgehalten. Details in den jeweiligen Abschnitten unten.
|---|---|---| |---|---|---|
| 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) | | 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 | | 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 | **Operator-Entscheidung offen: Messgeraet beschaffen.** Aktuell kein Smart-Plug/Messgeraet vorhanden, daher keine Messwerte. | Beschaffung eines USB-/Smart-Plug-Messgeraets (z. B. schaltbare Mess-Steckdose) | | 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
@@ -157,10 +157,10 @@ Bewertung:
## Stromverbrauch ## Stromverbrauch
**Operator-Entscheidung offen: Messgeraet beschaffen.** Stand 2026-06-05 ist kein **Bewusst ohne Messung (Operator-Entscheidung 2026-06-06).** Es wird kein
Smart-Plug/Messgeraet vorhanden, daher liegen keine Messwerte vor. Die Werte Messgeraet beschafft; Idle/Normal/Backup/Last bleiben dauerhaft offen. Kein
bleiben bewusst offen, bis ein messfaehiges Geraet beschafft ist. Erst danach offener Todo. Falls spaeter doch eine Mess-Steckdose angeschafft wird, reicht
werden Idle/Normal/Backup/Last in einem Durchlauf erfasst. ein einziger Messdurchlauf, um die Tabelle zu fuellen.
| Zustand | Verbrauch | Messmethode | Datum | | Zustand | Verbrauch | Messmethode | Datum |
|---|---:|---|---| |---|---:|---|---|
+8 -1
View File
@@ -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.
+25 -22
View File
@@ -1,6 +1,6 @@
# Master To-do - KalliLab CORE # Master To-do - KalliLab CORE
Stand: 2026-06-05 (Wochenend-Sprint, nach Status-Kategorien sortiert) Stand: 2026-06-06 (Wochenend-Sprint, nach Status-Kategorien sortiert)
Diese Liste ist die zentrale Arbeitsliste fuer offene operative Punkte im Diese Liste ist die zentrale Arbeitsliste fuer offene operative Punkte im
Homelab. Detailentscheidungen bleiben in den verlinkten Runbooks; diese Datei Homelab. Detailentscheidungen bleiben in den verlinkten Runbooks; diese Datei
@@ -23,29 +23,17 @@ Host-/Entscheidungsaufgaben beim **Operator**.
| Thema | Owner | Naechster konkreter Schritt | Quelle | | Thema | Owner | Naechster konkreter Schritt | Quelle |
|---|---|---|---| |---|---|---|---|
| DR-Workstation Bare-Metal-Kit | Operator | Auf dem Gaming-PC `wsl --install -d Ubuntu`, dann `sudo apt install borgbackup`, dann Smoke `borg list ssh://u565255@u565255.your-storagebox.de/./hetzner_borg_appdata_critical` mit offline gesichertem Key + Passphrase | `docs/AUDIT_2026-05-25_TODO.md`, `docs/DR_WORKSTATION_SETUP.md`, `docs/EXTERNAL_DEPENDENCIES.md` |
| 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` | | 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` |
| Home Assistant -> InfluxDB Token | Operator | `influxdb3_homeassistant_token` in InfluxDB 3 erzeugen, in HA `secrets.yaml` ablegen, einen Write-Pfad-Test fahren; nur Variablennamen/Pfade dokumentieren, kein Wert ins Repo | `docs/SECRETS_MAP.md`, `docs/HOME_ASSISTANT_INFLUXDB_ECOWITT.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 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 AdGuard Home | Operator | Runbook-Stub abarbeiten: Config nach `/mnt/user/backups/restore-lab/adguard` extrahieren, Testcontainer auf Port `5353`/`3001`, DNS-Smoke | `docs/RESTORE_MATRIX.md` Abschnitt "AdGuard Home" |
| 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" | | 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" |
| Restore-Test Redis 8 | Operator | Runbook-Stub abarbeiten: Pre-Cutover-Backup in isolierte Instanz auf Port `16379`, `PING` + `INFO server` (8.x) + `DBSIZE` pruefen | `docs/RESTORE_MATRIX.md` Abschnitt "Redis 8 (Shared)" | | 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` |
| Manuelle App-/Lizenzchecks `baerchen` | Codex/Operator | Passwortmanager/2FA-Recovery-Codes, Banking4, WISO, Microsoft/M365/OneDrive im laufenden System bestaetigen | `ops/windows-reinstall/docs/windows-neuaufsetzen-masterplan.md` |
--- ---
## Operator-Entscheidung ## Operator-Entscheidung
| Thema | Entscheidungsfrage | Quelle | **Stand 2026-06-06: keine offenen Operator-Entscheidungen.** Alle am 2026-06-06
|---|---|---| entschieden — Ergebnisse in "Aktiv", "Geparkt" bzw. "Entschieden 2026-06-06".
| BitLocker-Entscheidung `baerchen` | C: (und ggf. D:) aktivieren oder bewusst deaktiviert lassen? Bei Ja: Recovery-Key vorher nach `D:\30_Finanzen\...`, Vaultwarden und physisch sichern. (Claude fasst BitLocker bewusst nicht an.) | `docs/SECRETS_MAP.md`, `ops/windows-reinstall/docs/laufwerks-neustruktur-2026-06-04.md` |
| Veeam Storage Encryption `baerchen` | Reicht der erste unverschluesselte Full-Lauf, oder soll Veeam Storage Encryption aktiviert werden? Bei Ja: Passwort in Vaultwarden anlegen, Job umstellen und neues Full-Backup erzeugen | `ops/windows-reinstall/docs/windows-image-backup-baseline.md`, `docs/SECRETS_MAP.md` |
| Stromverbrauch messen | Messgeraet/Smart-Plug beschaffen? Ohne Geraet bleiben Idle/Normal/Backup/Last bewusst offen. **Status 2026-06-05: kein Geraet vorhanden.** | `docs/HARDWARE_INVENTORY.md` Abschnitt "Stromverbrauch" |
| Tailscale ACL-Policy | **Jetzt sicherheitsrelevanter:** Messung 2026-06-05 zeigt, dass der Host das ganze LAN `192.168.178.0/24` als Subnet-Route ins Tailnet bringt — jedes Tailnet-Geraet kann darueber LAN-/Admin-Dienste erreichen. Entscheidung: Default-Allow belassen oder Tag-basierte ACL (`tag:operator`/`tag:family`) einfuehren? Aktuelle Policy read-only aus Admin-Konsole sichten, kein Wert ins Repo | `docs/NETWORK_INVENTORY.md` Abschnitt "Tailscale" / "ACL-Policy" |
| Nextcloud 2FA / Brute-Force-Haertung | Operator-TOTP (`twofactor_totp`) jetzt aktivieren? Familien-/OIDC-weite Policy separat | `docs/AUDIT_2026-05-25_TODO.md` |
| Authelia Rest-2FA | Weitere Admin-UIs (`monitoring`, `glances`, `glance`, `speedtest`, `pdf`, `mail`, `sp` ...) von `one_factor` auf `two_factor` heben oder bewusst belassen? | `docs/AUDIT_2026-05-25_TODO.md` |
| Authelia OIDC fuer Apps | App-uebergreifendes SSO einfuehren - haengt an Familien-/SSO-Grundsatzentscheidung | `docs/AUDIT_2026-05-25_TODO.md` |
| Gast-/IoT-Netz | Bewusst kein Gast-WLAN/IoT-Netz aktivieren, solange nicht gebraucht. Vorbedingung bei spaeterer Aktivierung: LAN-Admin-Ports vorher per FRITZ!Box-Filter gegen das Gastsegment sperren | `docs/NETWORK_INVENTORY.md` |
--- ---
@@ -58,12 +46,13 @@ Bewusst nicht jetzt - mit Review-Trigger.
| 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` | | 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` | | 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` | | 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` |
| Home Assistant InfluxDB Bind | Aktuell `127.0.0.1:8181`, validiert. Nur wenn HA nicht lokal auf den Host schreibt, bewusste Bind-Aenderung planen | `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` |
| Docker Critical Events Watcher | Aktivierung vorbereitet: `services/posture-check/docker-critical-events-supervisor.sh` kapselt Start/Stop/Status/Smoke fuer das Unraid User Script. Optional spaeter in der Unraid-UI `at array start` aktivieren und ntfy-Smoke fahren; kein Pflicht-Pfad | `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` |
| Negativ-Test Backup-Frische | Quartalsweise: bewusst kaputten/fehlenden Dump in Testpfad simulieren, pruefen ob `homelab-alerts` feuert | `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` | | 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` | | 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` | | 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` | | 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` | | 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` | | Filebrowser-Mounts | Bei zukuenftigem Hardening-Sprint Mount-Scope reduzieren | `docs/SERVICE_CATALOG.md` |
@@ -81,9 +70,7 @@ Wartet auf ein externes Ereignis oder eine Abhaengigkeit.
| Thema | Blockiert durch | Naechster Schritt sobald entblockt | Quelle | | Thema | Blockiert durch | Naechster Schritt sobald entblockt | Quelle |
|---|---|---|---| |---|---|---|---|
| `baerchen` Recovery-Test ohne Restore | Haengt am physischen USB-Boot-Test | Von USB `VEEAMRE` booten, SMB-Ziel mounten, Restore Point anzeigen, vor echtem Restore abbrechen. **Owner: Codex/Operator** | `ops/windows-reinstall/docs/windows-image-backup-baseline.md`, `docs/RESTORE_MATRIX.md` |
| 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` | | 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` |
| Stromverbrauch-Messwerte | Kein Messgeraet beschafft | Nach Beschaffung einer schaltbaren Mess-Steckdose einen Messdurchlauf Idle/Normal/Backup/Last fahren | `docs/HARDWARE_INVENTORY.md` |
--- ---
@@ -92,12 +79,28 @@ Wartet auf ein externes Ereignis oder eine Abhaengigkeit.
- 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-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. - 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. - 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 (Operator-Entscheidung offen) von diffusen TBDs auf bewusste Entscheidungen mit Review-Triggern gehoben. - `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.** - `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.** - `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/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. - `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. - `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.
--- ---
+238 -22
View File
@@ -55,7 +55,7 @@ Gemessen am 2026-06-05 per read-only SSH auf den Host (`tailscale status`,
| Tailscale IPv6 | `fd7a:115c:a1e0::2c01:62b2` (gemessen 2026-06-05) | | Tailscale IPv6 | `fd7a:115c:a1e0::2c01:62b2` (gemessen 2026-06-05) |
| 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). | | 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). |
| 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. | | 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 | **Operator-Entscheidung offen** — siehe eigener Block unten; durch den aktiven Subnet-Router ist die ACL-Frage sicherheitsrelevanter als zuvor angenommen. | | 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. |
### Tailnet-Geraete (Snapshot 2026-06-05) ### Tailnet-Geraete (Snapshot 2026-06-05)
@@ -65,13 +65,37 @@ Gemessen am 2026-06-05 per read-only SSH auf den Host (`tailscale status`,
| `100.78.133.37` | baerchen-1 | windows | aktiv (aktuelle Operator-Workstation, direct) | | `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.105.203.21` | baerchen | windows | offline, zuletzt vor ~1 Tag gesehen (Alt-Node) |
| `100.73.83.55` | iphone-14 | iOS | bekannt | | `100.73.83.55` | iphone-14 | iOS | bekannt |
| `100.112.0.90` | kallilab-core | linux | gelistet, kein aktiver Verkehr — **moeglicher Alt-/Dubletten-Node**, separat pruefen | | `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. |
> Hygiene-Hinweis (kein Secret): Es existieren zwei linux-Nodes mit aehnlichem > **Befund 2026-06-06 (read-only auf dem Host ermittelt):** Der Host hat **zwei**
> Namen (`kallilabcore` und `kallilab-core`) sowie zwei `baerchen`-Nodes > `tailscaled`-Prozesse:
> (`baerchen-1` aktiv, `baerchen` offline). Bei Gelegenheit in der >
> Tailscale-Admin-Konsole pruefen, ob die inaktiven Eintraege stillgelegt werden > 1. **Native Unraid-Plugin** = `kallilabcore` (100.80.98.33). Prozess
> koennen. Das ist Aufraeumarbeit, kein akutes Risiko. > `/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 ### Subnet-Router-Konsequenz
@@ -91,22 +115,91 @@ tailscale ip -4
tailscale ip -6 tailscale ip -6
``` ```
### ACL-Policy (Operator-Entscheidung offen) ### ACL-Policy — ANGEWENDET 2026-06-06 (restriktive Tag-basierte grants)
Die Tailnet-ACL ist aktuell nicht im Repo gespiegelt. Sie wird in der **Status: live und verifiziert.** Die restriktive Policy wurde am 2026-06-06
Tailscale-Admin-Konsole unter `Access controls` verwaltet (kein Wert/Secret gemeinsam mit dem Operator in der lockout-sicheren Reihenfolge ausgerollt und
gehoert ins Repo). Offene Entscheidung: 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.
- Default-Allow belassen (jedes Tailnet-Geraet darf alles, inkl. der > **Schema-Hinweis:** Dieses Tailnet nutzt das `grants`-Modell
LAN-Subnet-Route), **oder** > (`{"src","dst","ip"}`), nicht das aeltere `acls`/`action:accept`-Modell.
- restriktivere, Tag-basierte ACL einfuehren: nur Operator-Geraete duerfen die > Normaler SSH-Zugriff (`ssh kallilabcore` ueber OpenSSH Port 22) wird ueber
Subnet-Route `192.168.178.0/24` und die Admin-Ports nutzen, Familien-/Mobil- > `grants` geregelt, nicht ueber den `ssh`-Block; letzterer betrifft nur die
geraete nur die explizit benoetigten Dienste. > Tailscale-SSH-Funktion.
Empfehlung (zur Entscheidung, nicht umgesetzt): Da der Subnet-Router das ganze **Angewendete Policy (live, kein Secret):**
LAN ins Tailnet bringt, ist eine restriktive ACL der wirksamste Hebel. Naechster
Schritt waere, die aktuelle Policy read-only aus der Admin-Konsole zu sichten ```json
und dann eine Tag-Struktur (`tag:operator`, `tag:family`) zu entwerfen. {
"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
@@ -124,6 +217,7 @@ 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
@@ -152,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 |
@@ -168,7 +263,7 @@ docker ps --format "{{.Names}}: {{.Ports}}" | sort
|---|---|---| |---|---|---|
| LAN | 192.168.178.0/24 | Hauptnetz, Host `192.168.178.58`, FRITZ!Box meldet 35 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 |
@@ -193,6 +288,126 @@ 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 |
@@ -201,7 +416,8 @@ docker network inspect backend_net | jq '.[0].Internal'
| 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!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!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. | | 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. |
| 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. | | 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. |
| Gast-/IoT-Zugriff auf Admin-Ports | **Entscheidungspunkt: kein Gast-/IoT-Netz aktivieren, solange nicht gebraucht** | Aktuell entschaerft, weil Gast-WLAN inaktiv ist und kein IoT-VLAN existiert. Risiko entsteht erst bei Aktivierung. Harte Vorbedingung fuer eine spaetere Aktivierung: **vor** dem Einschalten von Gast-WLAN/IoT muessen `192.168.178.58:8082` (AdGuard-Admin, ohnehin Tailscale-gebunden), `192.168.178.58:8181` (InfluxDB, bereits `127.0.0.1`-bound) und alle weiteren LAN-Admin-Ports per FRITZ!Box-Netzwerkfilter/Kindersicherung gegen das Gastsegment gesperrt sein. Bis dahin bewusst kein Gastnetz. | | 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. |
| 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. | | 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. | | 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". |
+3 -1
View File
@@ -31,8 +31,10 @@ 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 | | `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 |
@@ -43,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
+9 -1
View File
@@ -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
+1
View File
@@ -35,6 +35,7 @@ Details gilt immer die betroffene Compose-Datei oder das jeweilige Runbook.
| `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/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
+46 -19
View File
@@ -28,10 +28,10 @@ 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` (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` | | 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; 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` | | 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 |
@@ -44,7 +44,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
| System | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test | | 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 | Von `VEEAMRE` booten, SMB-Ziel oeffnen, Restore Point anzeigen, vor echtem Restore abbrechen; Runbook `ops/windows-reinstall/docs/windows-image-backup-baseline.md` | | `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` |
--- ---
@@ -135,7 +135,7 @@ Die Dump-Erzeugung ist host-seitig ueber `ops/borg-ui/scripts/pre-backup-dumps.s
## Restore-Test-Reifegrad ## Restore-Test-Reifegrad
Stand 2026-06-05. Pro Dienst auf einen Blick: Wurde der Restore schon einmal real getestet? Stand 2026-06-06. Pro Dienst auf einen Blick: Wurde der Restore schon einmal real getestet?
| Dienst | Tier | Letzter Restore-Test | Typ | Naechster Lauf | | Dienst | Tier | Letzter Restore-Test | Typ | Naechster Lauf |
|---|---|---|---|---| |---|---|---|---|---|
@@ -147,10 +147,10 @@ Stand 2026-06-05. Pro Dienst auf einen Blick: Wurde der Restore schon einmal rea
| Immich | 2 | 2026-05-27 | Dump + Container + HTTP + Asset-Count | quartalsweise (2. So Feb/Mai/Aug/Nov) | | Immich | 2 | 2026-05-27 | Dump + Container + HTTP + Asset-Count | quartalsweise (2. So Feb/Mai/Aug/Nov) |
| Unraid OS Flash | 1 | 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 | | 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 | | Traefik | 1 | 2026-06-03 | Config + LE-State + File-Provider + Ping 200 | quartalsweise |
| AdGuard Home | 1 | - | noch kein Test | - | | AdGuard Home | 1 | 2026-06-06 | Config + Container + HTTP 401 + DNS + Filter-Count | quartalsweise oder nach DNS-Aenderungen |
| Tailscale | 1 | - | noch kein Test | - | | Tailscale | 1 | - | noch kein Test | - |
| PostgreSQL 18 Cluster | 1 | 2026-06-03 | globals + 5 per-DB dumps, 290 Tabellen gesamt | quartalsweise | | PostgreSQL 18 Cluster | 1 | 2026-06-03 | globals + 5 per-DB dumps, 290 Tabellen gesamt | quartalsweise |
| Redis 8 | 1 | - | noch kein Test | - | | Redis 8 | 1 | 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 | | 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 | | 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 | | Mealie | 2 | 2026-06-03 | File + Dump + Container + HTTP + Recipe-Count (3) | quartalsweise |
@@ -159,21 +159,19 @@ Stand 2026-06-05. Pro Dienst auf einen Blick: Wurde der Restore schon einmal rea
| ntfy | 2 | - | rebuildbar, kein Test noetig | - | | ntfy | 2 | - | rebuildbar, kein Test noetig | - |
| Borg UI | 3 | - | rebuildbar | - | | Borg UI | 3 | - | rebuildbar | - |
| Filebrowser | 3 | - | rebuildbar | - | | Filebrowser | 3 | - | rebuildbar | - |
| baerchen Windows Image | Workstation | 2026-06-05 | Full-Backup geschrieben; Recovery USB + SMB-Mount noch offen | Recovery-USB-Test | | 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 |
--- ---
## Naechste Restore-Test-Kandidaten (priorisiert) ## Naechste Restore-Test-Kandidaten (priorisiert)
Stand 2026-06-05. Die frueheren Kandidaten (Shared PG18, Komodo Mongo, Mailarchiver, Mealie, Traefik) 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. wurden alle am 2026-06-03 abgeschlossen und sind in der Reifegrad-Tabelle belegt.
Verbleibende offene Restore-Pfade ohne vollstaendigen Test: 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**. 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. **AdGuard Home** - Config-Restore in Testpfad oder Wegwerf-Instanz pruefen 2. **Tailscale** - State-/Reconnect-Pfad dokumentiert testen
3. **Tailscale** - State-/Reconnect-Pfad dokumentiert testen
4. **Redis 8 (Shared)** - Restore aus Datenpfad oder Pre-Cutover-Backup in isolierter Testinstanz pruefen
--- ---
@@ -225,28 +223,42 @@ nur Eintragsnamen gelistet. Offen bleibt der physische Ersatzstick-Boot-Test.
### AdGuard Home ### 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:** **Voraussetzungen:**
- Borg-Archiv mit `/mnt/user/appdata/adguard/conf` zugaenglich (produktives Repo oder Teststand) - Borg-Archiv mit `/mnt/user/appdata/adguard/conf` zugaenglich (produktives Repo oder Teststand)
- Testpfad unter `/mnt/user/backups/restore-lab/adguard` vorbereitet - Testpfad unter `/mnt/user/backups/restore-lab/adguard` vorbereitet
- Docker-Faehigkeit auf dem Testhost oder in der Restore-Lab-Umgebung - Docker-Faehigkeit auf dem Testhost oder in der Restore-Lab-Umgebung
**Checkliste:** **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`: 1. Borg-Extract des letzten Archivs nach `/mnt/user/backups/restore-lab/adguard/conf`:
``` ```
borg extract ::ARCHIV /mnt/user/appdata/adguard/conf borg extract ::ARCHIV /mnt/user/appdata/adguard/conf
``` ```
2. Konfigurationsdatei `AdGuardHome.yaml` auf Vollstaendigkeit pruefen (YAML-Syntax valide) 2. Konfigurationsdatei `AdGuardHome.yaml` auf Vollstaendigkeit pruefen (YAML-Syntax valide)
3. Testcontainer starten (kein produktiver DNS-Port 53, stattdessen z. B. `5353`): 3. Testcontainer starten (kein produktiver DNS-Port 53, stattdessen z. B. `15353`):
```yaml ```yaml
ports: ports:
- "5353:53/udp" - "127.0.0.1:15353:53/udp"
- "3001:3000/tcp" - "127.0.0.1:13001:80/tcp"
volumes: volumes:
- /mnt/user/backups/restore-lab/adguard/conf:/opt/adguardhome/conf - /mnt/user/backups/restore-lab/adguard/conf:/opt/adguardhome/conf
``` ```
4. `http://localhost:3001` erreichbar, Login moeglich 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 5353 git.kaleschke.info` gibt plausible Antwort 5. DNS-Aufloesung: `dig @127.0.0.1 -p 15353 git.kaleschke.info` gibt plausible Antwort
6. Testcontainer stoppen und Testpfad aufraeumen 6. Testcontainer stoppen und Testpfad aufraeumen
**Smoke-Test-Kriterium:** AdGuard-Web-UI laeuft, DNS-Aufloesung antwortet, Filterlisten sind geladen. **Smoke-Test-Kriterium:** AdGuard-Web-UI laeuft, DNS-Aufloesung antwortet, Filterlisten sind geladen.
@@ -284,12 +296,27 @@ nur Eintragsnamen gelistet. Offen bleibt der physische Ersatzstick-Boot-Test.
### Redis 8 (Shared) ### 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:** **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` - 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) - Secret-Datei `redis_password.txt` fuer Testinstanz verfuegbar (aus Borg, nicht als Wert dokumentieren)
- Testpfad unter `/mnt/user/backups/restore-lab/redis` vorbereitet - Testpfad unter `/mnt/user/backups/restore-lab/redis` vorbereitet
**Checkliste:** **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: 1. RDB/AOF-Datei aus dem Backup in den Testpfad kopieren:
``` ```
+3 -2
View File
@@ -53,7 +53,8 @@ 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 | 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 | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv |
@@ -61,7 +62,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
| 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 | | 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 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 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: | geplant: `D:\30_Finanzen\BitLocker-RecoveryKey-baerchen-<DATUM>.txt` + Vaultwarden Secure Note + physischer Ausdruck; aktuell BitLocker noch nicht aktiv | geplant | | 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) |
--- ---
+2 -2
View File
@@ -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
@@ -47,7 +47,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten | | `nextcloud` | 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`, 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-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`). | | `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`). |
@@ -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
```
+228
View File
@@ -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, 714 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: niedrigmittel | 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?
+58
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
services: services:
adguard: adguard:
image: adguard/adguardhome:v0.107.76@sha256:7157eb1dc3b26c7af1d6898759a7b3f7d0fa09891fbd2d3caa6abc1057a9179b image: adguard/adguardhome:v0.107.77@sha256:e6f2b8bcda06064ab055b44933a4f0e983c35558b9cdb8d2e7ab1efcee36d890
container_name: adguard container_name: adguard
restart: unless-stopped restart: unless-stopped
volumes: volumes:
@@ -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
+29 -6
View File
@@ -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
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
borg-ui: borg-ui:
image: ainullcode/borg-ui@sha256:b44c0a92b650d80f215a986dadda5c2604c61eb28a7571e19c046eff41d761e7 image: ainullcode/borg-ui@sha256:0922157e8f77a1b2bd23cd09366a458ea6de07fd9306aa1485f9cfe623eca17f
container_name: borg-ui container_name: borg-ui
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
code-server: code-server:
image: lscr.io/linuxserver/code-server:4.122.0@sha256:0caf1b65ebec84b94397108b56da6c33f124c5390f5832da94e75f4609c0e2ad image: lscr.io/linuxserver/code-server:4.123.0@sha256:cb261a7f87674b445e0fd66d87d55900c1b823d276c727ab0d168a75e69e9992
container_name: code-server container_name: code-server
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
filebrowser: filebrowser:
image: filebrowser/filebrowser:v2.63.5@sha256:aefb0c20de10ef8b617995ca5522479ad40d41e6386bd01946a345c6026ff31c image: filebrowser/filebrowser:v2.63.14@sha256:1ec9b0c68297550c92f4a93feed432850c2993b261706cc3cc2e808f94a95e76
container_name: filebrowser container_name: filebrowser
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
-6
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
FROM nousresearch/hermes-agent:v2026.5.29 FROM nousresearch/hermes-agent:v2026.6.5
USER root USER root
+5
View File
@@ -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"
@@ -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
+90
View File
@@ -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"
+41
View File
@@ -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'))"
+8
View File
@@ -42,12 +42,17 @@ Ziel:
- `authelia-compose.test.yml`: isolierte Testinstanz fuer Authelia inkl. Test-Postgres, Filesystem-Notifier (kein echter SMTP-Versand) - `authelia-compose.test.yml`: isolierte Testinstanz fuer Authelia inkl. Test-Postgres, Filesystem-Notifier (kein echter SMTP-Versand)
- `authelia-plan.md`: konkreter Authelia-Testplan - `authelia-plan.md`: konkreter Authelia-Testplan
- `authelia-runbook.md`: Operator-Runbook fuer den ersten Authelia-Lauf - `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-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 - `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
@@ -91,9 +96,12 @@ Aktuell ist das erste validierte Muster vorhanden.
- 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 - 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
- `ntfy`-Wrapper ist fuer Host-Jobs verfuegbar - `ntfy`-Wrapper ist fuer Host-Jobs verfuegbar
- 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. - 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` - 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) - naechste grosse Kandidaten sind Mailarchiver und Mealie; Nextcloud bleibt blockiert (shfs-chmod)
@@ -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
+181
View File
@@ -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"
+74
View File
@@ -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
+16
View File
@@ -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
+152
View File
@@ -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"
+16 -1
View 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
@@ -40,6 +43,18 @@ case "$MODE" in
fi fi
exec "$SCRIPT_DIR/authelia-restore-test.sh" 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) nextcloud)
if [ "$WHATIF" = "--what-if" ]; then if [ "$WHATIF" = "--what-if" ]; then
exec "$SCRIPT_DIR/nextcloud-restore-test.sh" --what-if exec "$SCRIPT_DIR/nextcloud-restore-test.sh" --what-if
@@ -83,7 +98,7 @@ case "$MODE" in
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh" exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
;; ;;
*) *)
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich|authelia|nextcloud|komodo-bootstrap|komodo-mongo-restore|shared-pg-cluster} [--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
+5 -1
View File
@@ -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
@@ -45,7 +47,9 @@ Quartals-Belegung:
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; Authelia am Immich am 2026-05-27; Paperless erneut am 2026-05-31; Authelia am
2026-06-03 (Config-Smoke ohne produktiven Dump-Restore). 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
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
scrutiny: scrutiny:
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:41c5faefb96766d27d58a829fa19b3f4f27da4160926de3255cf142a85a90c12 image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:228483f16a6236d2fa9b2fbfca2e76dc861e648fbc6ae6e680d23e5d00211a5d
container_name: scrutiny container_name: scrutiny
restart: unless-stopped restart: unless-stopped
privileged: true privileged: true
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
speedtest-tracker: speedtest-tracker:
image: lscr.io/linuxserver/speedtest-tracker:1.14.3@sha256:79c00631575dec6d91c10ed904c211224f00813013a305c2284324e195a538bb image: lscr.io/linuxserver/speedtest-tracker:1.14.3@sha256:c3750c40948a9360000ce62d694da92e85584b4ab6d3d9a9d1432d76fa5e0726
container_name: speedtest-tracker container_name: speedtest-tracker
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
@@ -0,0 +1,194 @@
param(
[string]$ReportPath = "G:\Gitea_Clone\homelab-infra\ops\windows-reinstall\docs\baerchen-app-license-readiness-2026-06-06.md"
)
$ErrorActionPreference = "Stop"
function Get-InstalledProgram {
param([string[]]$NamePattern)
$roots = @(
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
Get-ItemProperty $roots -ErrorAction SilentlyContinue |
Where-Object {
$display = $_.DisplayName
if (-not $display) { return $false }
foreach ($pattern in $NamePattern) {
if ($display -like $pattern) { return $true }
}
return $false
} |
Sort-Object DisplayName |
Select-Object DisplayName,DisplayVersion,Publisher,InstallDate
}
function Test-PathSummary {
param([string]$Path)
$item = Get-Item -LiteralPath $Path -ErrorAction SilentlyContinue
if (-not $item) {
return [pscustomobject]@{
Path = $Path
Exists = $false
Type = ""
LastWriteTime = ""
Bytes = ""
}
}
$bytes = ""
if ($item.PSIsContainer) {
$measure = Get-ChildItem -LiteralPath $Path -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum
$bytes = [int64]($measure.Sum)
} else {
$bytes = [int64]$item.Length
}
[pscustomobject]@{
Path = $Path
Exists = $true
Type = $(if ($item.PSIsContainer) { "Directory" } else { "File" })
LastWriteTime = $item.LastWriteTime.ToString("s")
Bytes = $bytes
}
}
function ConvertTo-MarkdownTable {
param(
[Parameter(ValueFromPipeline = $true)]$InputObject,
[string[]]$Columns
)
begin {
$rows = @()
}
process {
$rows += $InputObject
}
end {
if (-not $rows -or $rows.Count -eq 0) {
return "_Keine Treffer._"
}
$lines = @()
$lines += "| " + ($Columns -join " | ") + " |"
$lines += "| " + (($Columns | ForEach-Object { "---" }) -join " | ") + " |"
foreach ($row in $rows) {
$values = foreach ($column in $Columns) {
$value = [string]$row.$column
$value.Replace("|", "\|")
}
$lines += "| " + ($values -join " | ") + " |"
}
$lines -join "`n"
}
}
$programGroups = [ordered]@{
"Passwortmanager / Browser" = @("*Bitwarden*", "*Vaultwarden*", "*1Password*", "*KeePass*", "*KeePassXC*", "*Chrome*", "*Microsoft Edge*", "*Brave*", "*Firefox*")
"Banking4 / Subsembly" = @("*Banking4*", "*Subsembly*")
"WISO / Buhl" = @("*WISO*", "*Buhl*")
"Microsoft 365 / Office / OneDrive" = @("*Microsoft 365*", "*Microsoft Office*", "*Office 16*", "*OneDrive*")
}
$pathChecks = @(
"C:\Users\michi\AppData\Local\Subsembly",
"C:\Users\michi\AppData\Local\Buhl",
"C:\Users\michi\AppData\Local\Buhl Data Service GmbH",
"C:\ProgramData\Buhl Data Service GmbH",
"C:\Users\michi\Documents\steuer",
"C:\Users\michi\Desktop\Banking",
"C:\Users\michi\OneDrive",
"D:\30_Finanzen",
"D:\30_Finanzen\Recovery-Codes",
"D:\30_Finanzen\BitLocker-RecoveryKey-baerchen-2026-06-06.txt"
)
$oneDriveProcess = Get-Process OneDrive -ErrorAction SilentlyContinue |
Select-Object ProcessName,Id,StartTime
$oneDriveAccounts = Get-ChildItem "HKCU:\Software\Microsoft\OneDrive\Accounts" -ErrorAction SilentlyContinue |
Select-Object PSChildName
$officeCscript = @(
"$env:ProgramFiles\Microsoft Office\Office16\OSPP.VBS",
"${env:ProgramFiles(x86)}\Microsoft Office\Office16\OSPP.VBS"
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
$officeStatus = @()
if ($officeCscript) {
$raw = & cscript.exe //Nologo $officeCscript /dstatus 2>$null
$officeStatus = $raw |
Where-Object { $_ -match "LICENSE NAME|LICENSE STATUS|ERROR CODE|Last 5 characters" } |
ForEach-Object { $_.Trim() }
}
$lines = @()
$lines += "# baerchen App-/Lizenz-Readiness - 2026-06-06"
$lines += ""
$lines += "Automatisch erzeugter lokaler Check. Keine Lizenzkeys, Passwoerter, Tokens oder Recovery-Code-Werte wurden ausgelesen oder ins Repo geschrieben."
$lines += ""
$lines += "## Ergebnis"
$lines += ""
$lines += "- Technische Inventarisierung: erledigt"
$lines += "- Manuelle Konto-/Recovery-Bestaetigung: weiterhin Operator-Schritt"
$lines += ""
$lines += "## Installierte Programme"
$lines += ""
foreach ($group in $programGroups.Keys) {
$lines += "### $group"
$lines += ""
$programs = Get-InstalledProgram -NamePattern $programGroups[$group]
$lines += ($programs | ConvertTo-MarkdownTable -Columns DisplayName,DisplayVersion,Publisher,InstallDate)
$lines += ""
}
$lines += "## Relevante Datenpfade"
$lines += ""
$pathResults = $pathChecks | ForEach-Object { Test-PathSummary $_ }
$lines += ($pathResults | ConvertTo-MarkdownTable -Columns Path,Exists,Type,LastWriteTime,Bytes)
$lines += ""
$lines += "## OneDrive / Microsoft 365 Indikatoren"
$lines += ""
$lines += "### OneDrive Prozess"
$lines += ""
$lines += ($oneDriveProcess | ConvertTo-MarkdownTable -Columns ProcessName,Id,StartTime)
$lines += ""
$lines += "### OneDrive Accounts Registry"
$lines += ""
$lines += ($oneDriveAccounts | ConvertTo-MarkdownTable -Columns PSChildName)
$lines += ""
$lines += "### Office Aktivierungsindikatoren"
$lines += ""
if ($officeStatus.Count -gt 0) {
$lines += '```text'
$lines += $officeStatus
$lines += '```'
} else {
$lines += "_Keine Office-OSPP-Aktivierungsdaten gefunden oder Office nicht klassisch installiert._"
}
$lines += ""
$lines += "## Manuell noch zu bestaetigen"
$lines += ""
$lines += "- [ ] Passwortmanager laesst sich oeffnen und enthaelt Homelab-/Banking-/Provider-Eintraege."
$lines += "- [ ] 2FA-Recovery-Codes fuer Microsoft, Hetzner, Cloudflare, Tailscale, Gitea/GitHub und Banken sind offline oder in Vaultwarden auffindbar."
$lines += "- [ ] Banking4 oeffnet den aktuellen Datentresor; ein frischer Backup-/Exportpfad ist bekannt."
$lines += '- [ ] WISO Steuer 2026 oeffnet, Buhl-Konto/Lizenz ist aktiv, Steuerdateien unter `C:\Users\michi\Documents\steuer` bzw. neuem Zielpfad sind sichtbar.'
$lines += "- [ ] Microsoft-Konto zeigt aktives M365/Office-Installationsrecht."
$lines += "- [ ] OneDrive-Sync ist angemeldet und synchronisiert die erwarteten Ordner."
$lines += ""
$lines += "## Bewertung"
$lines += ""
$lines += 'Dieses Dokument ersetzt nicht die manuelle Kontoanmeldung. Es belegt nur, welche lokalen Programme, Datenpfade und Aktivierungsindikatoren auf `baerchen` sichtbar waren.'
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $ReportPath) | Out-Null
$lines -join "`r`n" | Set-Content -LiteralPath $ReportPath -Encoding UTF8
Write-Host "Report written: $ReportPath"
@@ -0,0 +1,86 @@
# baerchen App-/Lizenz-Readiness - 2026-06-06
Automatisch erzeugter lokaler Check. Keine Lizenzkeys, Passwoerter, Tokens oder Recovery-Code-Werte wurden ausgelesen oder ins Repo geschrieben.
## Ergebnis
- Technische Inventarisierung: erledigt
- Manuelle Konto-/Recovery-Bestaetigung: erledigt laut Operator-Bestaetigung 2026-06-06 ("alle Dienste laufen")
## Installierte Programme
### Passwortmanager / Browser
| DisplayName | DisplayVersion | Publisher | InstallDate |
| --- | --- | --- | --- |
| Brave | 149.1.91.168 | Die Brave-Autoren | 20260604 |
| Google Chrome | 149.0.7827.54 | Google LLC | 20260604 |
| Microsoft Edge | 148.0.3967.96 | Microsoft Corporation | 20260604 |
| Microsoft Edge WebView2-Laufzeit | 148.0.3967.96 | Microsoft Corporation | 20260604 |
### Banking4 / Subsembly
| DisplayName | DisplayVersion | Publisher | InstallDate |
| --- | --- | --- | --- |
| Banking4 Home | | Subsembly GmbH | |
### WISO / Buhl
| DisplayName | DisplayVersion | Publisher | InstallDate |
| --- | --- | --- | --- |
| WISO Steuer 2026 | 33.07.3410 | Buhl Data Service GmbH | 20260604 |
### Microsoft 365 / Office / OneDrive
| DisplayName | DisplayVersion | Publisher | InstallDate |
| --- | --- | --- | --- |
| Microsoft 365 - de-de | 16.0.20026.20140 | Microsoft Corporation | |
| Microsoft 365 - en-us | 16.0.20026.20140 | Microsoft Corporation | |
| Microsoft OneDrive | 23.038.0219.0001 | Microsoft Corporation | |
| Office 16 Click-to-Run Extensibility Component | 16.0.20026.20076 | Microsoft Corporation | 20260604 |
| Office 16 Click-to-Run Localization Component | 16.0.20026.20140 | Microsoft Corporation | 20260604 |
## Relevante Datenpfade
| Path | Exists | Type | LastWriteTime | Bytes |
| --- | --- | --- | --- | --- |
| C:\Users\michi\AppData\Local\Subsembly | True | Directory | 2026-06-04T12:23:43 | 43360359 |
| C:\Users\michi\AppData\Local\Buhl | True | Directory | 2026-06-04T12:55:57 | 680833 |
| C:\Users\michi\AppData\Local\Buhl Data Service GmbH | False | | | |
| C:\ProgramData\Buhl Data Service GmbH | True | Directory | 2026-06-04T12:57:08 | 6037194 |
| C:\Users\michi\Documents\steuer | True | Directory | 2026-01-26T11:21:44 | 13069132 |
| C:\Users\michi\Desktop\Banking | False | | | |
| C:\Users\michi\OneDrive | True | Directory | 2026-06-04T12:39:24 | 39370265 |
| D:\30_Finanzen | True | Directory | 2026-06-04T20:14:26 | 128994854 |
| D:\30_Finanzen\Recovery-Codes | False | | | |
| D:\30_Finanzen\BitLocker-RecoveryKey-baerchen-2026-06-06.txt | False | | | |
## OneDrive / Microsoft 365 Indikatoren
### OneDrive Prozess
_Keine Treffer._
### OneDrive Accounts Registry
| PSChildName |
| --- |
| Business1 |
| Personal |
### Office Aktivierungsindikatoren
_Keine Office-OSPP-Aktivierungsdaten gefunden oder Office nicht klassisch installiert._
## Manuell noch zu bestaetigen
- [x] Passwortmanager laesst sich oeffnen und enthaelt Homelab-/Banking-/Provider-Eintraege.
- [x] 2FA-Recovery-Codes fuer Microsoft, Hetzner, Cloudflare, Tailscale, Gitea/GitHub und Banken sind offline oder in Vaultwarden auffindbar.
- [x] Banking4 oeffnet den aktuellen Datentresor; ein frischer Backup-/Exportpfad ist bekannt.
- [x] WISO Steuer 2026 oeffnet, Buhl-Konto/Lizenz ist aktiv, Steuerdateien unter `C:\Users\michi\Documents\steuer` bzw. neuem Zielpfad sind sichtbar.
- [x] Microsoft-Konto zeigt aktives M365/Office-Installationsrecht.
- [x] OneDrive-Sync ist angemeldet und synchronisiert die erwarteten Ordner.
## Bewertung
Dieses Dokument belegt die technische Inventarisierung und die Operator-Bestaetigung vom 2026-06-06. Secret-Werte, Lizenzkeys und Recovery-Code-Werte wurden nicht dokumentiert.
@@ -125,8 +125,8 @@ und nicht als Alltags-USB verwenden.
## Restore-Test ohne echten Restore ## Restore-Test ohne echten Restore
Der Test ist noch offen und muss nach dem ersten erfolgreichen Backup Der Test wurde am 2026-06-06 erfolgreich abgeschlossen
durchgefuehrt werden: (Operator-Bestaetigung):
1. Vom USB-Stick booten. 1. Vom USB-Stick booten.
2. Veeam Recovery Environment starten. 2. Veeam Recovery Environment starten.
@@ -137,8 +137,9 @@ durchgefuehrt werden:
7. Vor Auswahl eines echten Ziel-Datentraegers abbrechen. 7. Vor Auswahl eines echten Ziel-Datentraegers abbrechen.
8. Windows normal von der internen SSD booten. 8. Windows normal von der internen SSD booten.
Erfolgskriterium: Recovery-Umgebung sieht das SMB-Ziel und den Restore Point, Ergebnis: Recovery-Umgebung bootet, SMB-Ziel
ohne dass ein Restore gestartet wurde. `\\kallilabcore\backups\windows-images\baerchen` ist erreichbar, Restore Point
wird angezeigt, und der Test wurde vor einem echten Restore abgebrochen.
## Erster Full-Lauf 2026-06-05 ## Erster Full-Lauf 2026-06-05
@@ -157,8 +158,9 @@ Beleg aus `C:\ProgramData\Veeam\Endpoint\baerchen-c-image\Job.baerchen-c-image.B
- Repository-Speicher danach: ca. 5.99 TB total, ca. 3.99 TB frei - Repository-Speicher danach: ca. 5.99 TB total, ca. 3.99 TB frei
- Job-Konfiguration im Log: `StorageEncryptionEnabled=False` - Job-Konfiguration im Log: `StorageEncryptionEnabled=False`
Damit ist der erste Image-Lauf technisch erfolgreich geschrieben. Noch offen Damit ist der erste Image-Lauf technisch erfolgreich geschrieben und der
bleibt der Recovery-USB-Boot-/SMB-Test. Recovery-USB-/SMB-/Restore-Point-Test wurde am 2026-06-06 erfolgreich
abgeschlossen.
Hilfsskript fuer die Windows-Seite: Hilfsskript fuer die Windows-Seite:
@@ -173,7 +175,6 @@ Sitzung zusaetzlich per `net use` authentifiziert werden muss.
## Offene Punkte ## Offene Punkte
- Recovery-Stick-Boot und SMB-Mount testen.
- Entscheiden, ob Veeam Storage Encryption nachtraeglich aktiviert werden soll. - Entscheiden, ob Veeam Storage Encryption nachtraeglich aktiviert werden soll.
Wenn ja: Passwort in Vaultwarden anlegen, Job umstellen und ein neues Full- Wenn ja: Passwort in Vaultwarden anlegen, Job umstellen und ein neues Full-
Backup erzeugen. Backup erzeugen.
@@ -46,11 +46,16 @@ Noch offen:
- BitLocker-Status mit Adminrechten pruefen. **Nachlauf 2026-06-05:** Status - BitLocker-Status mit Adminrechten pruefen. **Nachlauf 2026-06-05:** Status
wurde geprueft; C:/D:/E:/G:/H: sind `FullyDecrypted`, Protection `Off`. wurde geprueft; C:/D:/E:/G:/H: sind `FullyDecrypted`, Protection `Off`.
Offen bleibt nur die bewusste BitLocker-Entscheidung. Offen bleibt nur die bewusste BitLocker-Entscheidung.
- Passwortmanager, 2FA-Recovery-Codes und Browser-Sync manuell pruefen. - Passwortmanager, 2FA-Recovery-Codes und Browser-Sync manuell pruefen. **Erledigt 2026-06-06 laut Operator-Bestaetigung.**
- Banking4-Speicherort explizit pruefen. - Banking4-Speicherort explizit pruefen. **Erledigt 2026-06-06 laut Operator-Bestaetigung.**
- Banking4 im Programm selbst oeffnen und aktuellen Datentresor/Backup-Export bestaetigen. Der Key und der Datentresor sind bereits lokal auf H: gesichert. - Banking4 im Programm selbst oeffnen und aktuellen Datentresor/Backup-Export bestaetigen. Der Key und der Datentresor sind bereits lokal auf H: gesichert. **Erledigt 2026-06-06 laut Operator-Bestaetigung.**
- WISO Steuer 2026 oeffnen und Lizenz/Buhl-Konto sowie Speicherorte der Steuerdateien bestaetigen. - WISO Steuer 2026 oeffnen und Lizenz/Buhl-Konto sowie Speicherorte der Steuerdateien bestaetigen. **Erledigt 2026-06-06 laut Operator-Bestaetigung.**
- Microsoft-Konto fuer M365 pruefen: Office-Webkonto/Abonnement, Installationsrecht, OneDrive-Sync. - Microsoft-Konto fuer M365 pruefen: Office-Webkonto/Abonnement, Installationsrecht, OneDrive-Sync. **Erledigt 2026-06-06 laut Operator-Bestaetigung.**
- Technisches Nachinstallations-Inventar 2026-06-06 erledigt:
`baerchen-app-license-readiness-2026-06-06.md`. Banking4, WISO Steuer
2026, Microsoft 365/OneDrive und relevante Datenpfade sind lokal sichtbar;
Konto-/App-Oeffnen-Checks wurden am 2026-06-06 durch den Operator
bestaetigt ("alle Dienste laufen").
- Optional Keyfinder-Lauf durchfuehren und Ergebnisse lokal auf H: speichern. - Optional Keyfinder-Lauf durchfuehren und Ergebnisse lokal auf H: speichern.
- `G:\Ollama` bewusst entscheiden: nicht gesichert, ca. 40,9 GB lokale Modell-/Cache-Daten. - `G:\Ollama` bewusst entscheiden: nicht gesichert, ca. 40,9 GB lokale Modell-/Cache-Daten.
- D:, F: und G: vor dem spaeteren Loeschen noch einmal in Ruhe final bestaetigen. - D:, F: und G: vor dem spaeteren Loeschen noch einmal in Ruhe final bestaetigen.
@@ -533,8 +538,8 @@ Zielstruktur:
Status 2026-06-05: Diese Checkliste ist historisch fuer die Freigabe der Status 2026-06-05: Diese Checkliste ist historisch fuer die Freigabe der
Neuinstallation. Die technische Neuinstallation, Laufwerksbereinigung, Neuinstallation. Die technische Neuinstallation, Laufwerksbereinigung,
WinRE-Pruefung und Veeam-Baseline sind in neueren Dokumenten nachgezogen. WinRE-Pruefung und Veeam-Baseline sind in neueren Dokumenten nachgezogen.
Als offene manuelle Pruefungen bleiben vor allem Passwortmanager/2FA, Status 2026-06-06: Passwortmanager/2FA, Banking4, WISO und Microsoft/M365
Banking4, WISO und Microsoft/M365. wurden durch den Operator bestaetigt ("alle Dienste laufen").
- [ ] Backup-Struktur auf H: erstellt - [ ] Backup-Struktur auf H: erstellt
- [ ] Programmliste exportiert - [ ] Programmliste exportiert
@@ -542,10 +547,10 @@ Banking4, WISO und Microsoft/M365.
- [ ] Windows-Aktivierung dokumentiert - [ ] Windows-Aktivierung dokumentiert
- [ ] Benutzerordner gesichert - [ ] Benutzerordner gesichert
- [ ] Browser-Lesezeichen exportiert oder Sync geprüft - [ ] Browser-Lesezeichen exportiert oder Sync geprüft
- [ ] Passwortmanager geprüft - [x] Passwortmanager geprüft
- [ ] 2FA-Recovery-Codes gesichert - [x] 2FA-Recovery-Codes gesichert
- [ ] SSH/API/GPG/Zertifikate gesichert - [ ] SSH/API/GPG/Zertifikate gesichert
- [ ] Banking4-Speicherort geprüft und gesichert - [x] Banking4-Speicherort geprüft und gesichert
- [ ] Homelab-/NAS-Doku gesichert - [ ] Homelab-/NAS-Doku gesichert
- [ ] D:, F: und G: analysiert - [ ] D:, F: und G: analysiert
- [ ] Unklare Daten nach `99_Unsortiert_von_D_F_G` kopiert - [ ] Unklare Daten nach `99_Unsortiert_von_D_F_G` kopiert
+15 -1
View File
@@ -38,6 +38,19 @@
"automerge": false, "automerge": false,
"labels": ["dependencies", "minor-patch"] "labels": ["dependencies", "minor-patch"]
}, },
{
"description": "Kritische Kerninfra (Traefik=Public-Entrypoint, Unbound=DNS, n8n, Nextcloud): nicht im Sammel-PR, eigene einzeln reviewbare PRs, kein Auto-Merge",
"matchManagers": ["docker-compose", "dockerfile"],
"matchPackageNames": [
"traefik",
"shaanmajid/unbound",
"docker.n8n.io/n8nio/n8n",
"nextcloud"
],
"groupName": null,
"automerge": false,
"labels": ["dependencies", "core-critical"]
},
{ {
"description": "Stateful Tier-1 (Postgres, Mongo, Redis): keine Auto-Group, einzelne PRs, kein Auto-Merge", "description": "Stateful Tier-1 (Postgres, Mongo, Redis): keine Auto-Group, einzelne PRs, kein Auto-Merge",
"matchPackageNames": [ "matchPackageNames": [
@@ -99,6 +112,7 @@
"ignorePaths": [ "ignorePaths": [
"**/_archive/**", "**/_archive/**",
"ops/grafana-influxdb/**", "ops/grafana-influxdb/**",
"ops/loki/**" "ops/loki/**",
"ops/komodo/**"
] ]
} }
+8 -5
View File
@@ -41,18 +41,21 @@ access_control:
- git.kaleschke.info - git.kaleschke.info
policy: bypass policy: bypass
# Admin-Dienste - 2FA erforderlich (Operator-UIs mit Host-/Backup-Zugriff) # Expliziter 2FA-Block (Tier-1-Operator-UIs). Redundant, seit die Catch-all
# unten ohnehin alles auf two_factor zwingt - bleibt nur zur Lesbarkeit.
# borg/code brauchen hier keinen Eintrag mehr: sie sind via Catch-all 2FA.
- domain: - domain:
- files.kaleschke.info - files.kaleschke.info
- scrutiny.kaleschke.info - scrutiny.kaleschke.info
- borg.kaleschke.info
- code.kaleschke.info
policy: two_factor policy: two_factor
# Alles andere mit Authelia-Middleware - 1FA. # Alles andere mit Authelia-Middleware - 2FA (Haertung 2026-06-06).
# Vorher one_factor; alle restlichen Admin-UIs (monitoring, glances, glance,
# speedtest, pdf, mail, paperless-gpt, hermes, sp, borg, code ...) sind damit two_factor.
# Voraussetzung: Operator-Account hat Authelia-TOTP enrolled (Tier-1-UIs nutzen es bereits).
# Komodo hat bewusst keine ForwardAuth-Middleware und wird hier nicht ausgewertet. # Komodo hat bewusst keine ForwardAuth-Middleware und wird hier nicht ausgewertet.
- domain: "*.kaleschke.info" - domain: "*.kaleschke.info"
policy: one_factor policy: two_factor
session: session:
name: authelia_session name: authelia_session
+126 -19
View File
@@ -11,6 +11,7 @@ SINCE="${SINCE:-24h}"
MAX_LOG_LINES="${MAX_LOG_LINES:-80}" MAX_LOG_LINES="${MAX_LOG_LINES:-80}"
CERT_MAX_ROWS="${CERT_MAX_ROWS:-12}" CERT_MAX_ROWS="${CERT_MAX_ROWS:-12}"
IMAGE_AGE_WARN_DAYS="${IMAGE_AGE_WARN_DAYS:-180}" IMAGE_AGE_WARN_DAYS="${IMAGE_AGE_WARN_DAYS:-180}"
IMAGE_AGE_ALLOW_FILE="${IMAGE_AGE_ALLOW_FILE:-/mnt/user/services/homelab-infra/services/posture-check/image-age-allow.patterns}"
LOG_VOLUME_TOP_N="${LOG_VOLUME_TOP_N:-10}" LOG_VOLUME_TOP_N="${LOG_VOLUME_TOP_N:-10}"
DISK_USAGE_WARN_PCT="${DISK_USAGE_WARN_PCT:-85}" DISK_USAGE_WARN_PCT="${DISK_USAGE_WARN_PCT:-85}"
CERT_WARN_DAYS="${CERT_WARN_DAYS:-21}" CERT_WARN_DAYS="${CERT_WARN_DAYS:-21}"
@@ -28,6 +29,7 @@ TRAEFIK_ACME_PATH="${TRAEFIK_ACME_PATH:-/mnt/user/appdata/traefik/letsencrypt/ac
NOISE_PATTERNS_FILE="${NOISE_PATTERNS_FILE:-/mnt/user/services/homelab-infra/services/posture-check/log-noise.patterns}" NOISE_PATTERNS_FILE="${NOISE_PATTERNS_FILE:-/mnt/user/services/homelab-infra/services/posture-check/log-noise.patterns}"
NORMALIZE_NOISE_SCRIPT="${NORMALIZE_NOISE_SCRIPT:-/mnt/user/services/homelab-infra/services/posture-check/lib/normalize-noise-patterns.sh}" NORMALIZE_NOISE_SCRIPT="${NORMALIZE_NOISE_SCRIPT:-/mnt/user/services/homelab-infra/services/posture-check/lib/normalize-noise-patterns.sh}"
NOISE_ESCALATION_THRESHOLD="${NOISE_ESCALATION_THRESHOLD:-500}" NOISE_ESCALATION_THRESHOLD="${NOISE_ESCALATION_THRESHOLD:-500}"
NOISE_ESCALATION_EXEMPT_FILE="${NOISE_ESCALATION_EXEMPT_FILE:-/mnt/user/services/homelab-infra/services/posture-check/noise-escalation-exempt.patterns}"
NOISE_BREAKDOWN_TOP_N="${NOISE_BREAKDOWN_TOP_N:-10}" NOISE_BREAKDOWN_TOP_N="${NOISE_BREAKDOWN_TOP_N:-10}"
POSTURE_CHECK_FILE="${POSTURE_CHECK_FILE:-/mnt/user/services/posture-check/last.json}" POSTURE_CHECK_FILE="${POSTURE_CHECK_FILE:-/mnt/user/services/posture-check/last.json}"
LOCK_FILE="${LOCK_FILE:-/tmp/homelab-daily-report.lock}" LOCK_FILE="${LOCK_FILE:-/tmp/homelab-daily-report.lock}"
@@ -459,6 +461,10 @@ with open("/acme.json", "r", encoding="utf-8") as handle:
data = json.load(handle) data = json.load(handle)
now = datetime.now(timezone.utc) now = datetime.now(timezone.utc)
# Deduplicate: for each unique set of domains keep only the longest-lived cert.
# Traefik stores both the old and the newly-issued cert in acme.json during
# the renewal window, which would otherwise produce a false warning.
best = {} # frozenset(domains) -> (days, expire_date_iso, names)
for resolver in data.values(): for resolver in data.values():
for cert in resolver.get("Certificates", []): for cert in resolver.get("Certificates", []):
domain = cert.get("domain", {}).get("main") or "-" domain = cert.get("domain", {}).get("main") or "-"
@@ -474,7 +480,11 @@ for resolver in data.values():
not_after = datetime.strptime(decoded["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc) not_after = datetime.strptime(decoded["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
days = (not_after - now).days days = (not_after - now).days
names = ", ".join([domain, *sans]) names = ", ".join([domain, *sans])
print(f"{days}\t{not_after.date().isoformat()}\t{names}") key = frozenset([domain, *sans])
if key not in best or days > best[key][0]:
best[key] = (days, not_after.date().isoformat(), names)
for days, expires, names in best.values():
print(f"{days}\t{expires}\t{names}")
PY PY
then then
if [ ! -s "$cert_file" ]; then if [ ! -s "$cert_file" ]; then
@@ -573,13 +583,36 @@ collect_image_freshness() {
local image_file="$TMP_DIR/images.tsv" local image_file="$TMP_DIR/images.tsv"
local image_warnings=0 local image_warnings=0
local image_allowed=0
local now_epoch local now_epoch
: > "$image_file" : > "$image_file"
now_epoch="$(date +%s)" now_epoch="$(date +%s)"
# Parse the image-age allowlist: container deliberately pinned to a stable or
# upstream-recommended image. Each entry carries a recheck date; once that
# date has passed the suppression lapses, so a pin gets re-reviewed instead
# of silently aging forever.
local allow_file="$TMP_DIR/image-allow.tsv"
: > "$allow_file"
if [ -f "$IMAGE_AGE_ALLOW_FILE" ]; then
while IFS= read -r line; do
line="${line%%#*}"
line="$(printf '%s' "$line" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
[ -n "$line" ] || continue
local a_name a_date a_epoch
a_name="$(printf '%s' "$line" | awk '{ print $1 }')"
a_date="$(printf '%s' "$line" | awk '{ print $2 }')"
[ -n "$a_name" ] && [ -n "$a_date" ] || continue
a_epoch="$(date -d "$a_date" +%s 2>/dev/null || echo 0)"
if [ "$a_epoch" -ge "$now_epoch" ]; then
printf '%s\t%s\n' "$a_name" "$a_date" >> "$allow_file"
fi
done < "$IMAGE_AGE_ALLOW_FILE"
fi
while IFS= read -r name; do while IFS= read -r name; do
[ -n "$name" ] || continue [ -n "$name" ] || continue
local image_id created_iso created_epoch age_days image_tag local image_id created_iso created_epoch age_days image_tag note recheck
image_id="$(docker inspect --format '{{.Image}}' "$name" 2>/dev/null || true)" image_id="$(docker inspect --format '{{.Image}}' "$name" 2>/dev/null || true)"
[ -n "$image_id" ] || continue [ -n "$image_id" ] || continue
created_iso="$(docker image inspect --format '{{.Created}}' "$image_id" 2>/dev/null || true)" created_iso="$(docker image inspect --format '{{.Created}}' "$image_id" 2>/dev/null || true)"
@@ -588,33 +621,46 @@ collect_image_freshness() {
created_epoch="$(date -d "$created_iso" +%s 2>/dev/null || echo 0)" created_epoch="$(date -d "$created_iso" +%s 2>/dev/null || echo 0)"
[ "$created_epoch" -gt 0 ] || continue [ "$created_epoch" -gt 0 ] || continue
age_days=$(( (now_epoch - created_epoch) / 86400 )) age_days=$(( (now_epoch - created_epoch) / 86400 ))
printf '%d\t%s\t%s\n' "$age_days" "$name" "$image_tag" >> "$image_file" note=""
if [ "$age_days" -ge "$IMAGE_AGE_WARN_DAYS" ]; then if [ "$age_days" -ge "$IMAGE_AGE_WARN_DAYS" ]; then
image_warnings=$((image_warnings + 1)) recheck="$(awk -F '\t' -v n="$name" '$1 == n { print $2; found = 1 } END { exit !found }' "$allow_file" || true)"
if [ -n "$recheck" ]; then
note="bewusst gepinnt (recheck $recheck)"
image_allowed=$((image_allowed + 1))
else
note="ueberaltert"
image_warnings=$((image_warnings + 1))
fi
fi fi
printf '%d\t%s\t%s\t%s\n' "$age_days" "$name" "$image_tag" "$note" >> "$image_file"
done < <(docker ps --format '{{.Names}}') done < <(docker ps --format '{{.Names}}')
set_summary "image_warnings" "$image_warnings" set_summary "image_warnings" "$image_warnings"
set_summary "image_allowed" "$image_allowed"
if [ ! -s "$image_file" ]; then if [ ! -s "$image_file" ]; then
append "- Keine Image-Daten verfuegbar." append "- Keine Image-Daten verfuegbar."
record_section_error "images" "Keine Image-Daten ermittelt" record_section_error "images" "Keine Image-Daten ermittelt"
else else
append "- Schwelle Warnung: Image aelter als $IMAGE_AGE_WARN_DAYS Tage" append "- Schwelle Warnung: Image aelter als $IMAGE_AGE_WARN_DAYS Tage"
append "- Container mit Image >= $IMAGE_AGE_WARN_DAYS Tage: $image_warnings" append "- Container mit ueberaltertem Image (gewarnt): $image_warnings"
append "- Davon bewusst gepinnt (von Warnung ausgenommen): $image_allowed"
append "- Allowlist-Quelle: \`$IMAGE_AGE_ALLOW_FILE\`"
append "" append ""
append "### Aelteste Images (Top 10)" append "### Aelteste Images (Top 10)"
append "" append ""
append "| Alter Tage | Container | Image |" append "| Alter Tage | Container | Image | Hinweis |"
append "|---:|---|---|" append "|---:|---|---|---|"
sort -nr "$image_file" | head -n 10 | while IFS="$(printf '\t')" read -r age name img; do sort -nr "$image_file" | head -n 10 | while IFS="$(printf '\t')" read -r age name img note; do
append "| $age | $name | $img |" append "| $age | $name | $img | ${note:-} |"
done done
append "" append ""
if [ "$image_warnings" -eq 0 ]; then if [ "$image_warnings" -eq 0 ] && [ "$image_allowed" -eq 0 ]; then
append "Bewertung: Keine Container mit ueberalterten Images. CVE-Hygiene aus dieser Sicht ok." append "Bewertung: Keine Container mit ueberalterten Images. CVE-Hygiene aus dieser Sicht ok."
elif [ "$image_warnings" -eq 0 ]; then
append "Bewertung: Keine ungeprueft ueberalterten Images. $image_allowed Container sind bewusst gepinnt und mit Recheck-Datum dokumentiert."
else else
append "Bewertung: $image_warnings Container nutzen Images aelter als $IMAGE_AGE_WARN_DAYS Tage. Update-Pipeline und CVE-Status pruefen." append "Bewertung: $image_warnings Container nutzen ueberalterte Images (nicht in der Allowlist). Update-Pipeline und CVE-Status pruefen."
fi fi
fi fi
append "" append ""
@@ -655,6 +701,31 @@ collect_container_events() {
collect_container_state() { collect_container_state() {
append "## Container-Zustand" append "## Container-Zustand"
append "" append ""
append "### Unhealthy Container"
local unhealthy_file="$TMP_DIR/unhealthy.log"
docker ps --filter health=unhealthy --format '{{.Names}}' > "$unhealthy_file"
if [ ! -s "$unhealthy_file" ]; then
append "- Keine."
else
append "| Container | FailingStreak | Letzter Healthcheck |"
append "|---|---:|---|"
while IFS= read -r name; do
[ -n "$name" ] || continue
local streak hc
streak="$(docker inspect "$name" --format '{{.State.Health.FailingStreak}}' 2>/dev/null || echo '?')"
# Letzten nicht-leeren Health-Log-Eintrag holen, einzeilig machen und
# Pipe-Zeichen escapen, damit die Markdown-Tabelle nicht bricht.
hc="$(docker inspect "$name" --format '{{range .State.Health.Log}}exit={{.ExitCode}} out={{.Output}}~~~{{end}}' 2>/dev/null \
| tr '\n' ' ' \
| awk -F '~~~' '{ for (i = NF - 1; i >= 1; i--) { if ($i != "") { print $i; break } } }' \
| sed -E 's/[[:space:]]+/ /g; s/\|/\\|/g' \
| cut -c1-160)"
append "| \`$name\` | ${streak:-?} | ${hc:-(kein Output)} |"
done < "$unhealthy_file"
fi
append ""
append "### Nicht laufende Container" append "### Nicht laufende Container"
local stopped_file="$TMP_DIR/stopped.log" local stopped_file="$TMP_DIR/stopped.log"
docker ps -a --filter status=exited --filter status=dead --filter status=created --format '{{.Names}}\t{{.Status}}' > "$stopped_file" docker ps -a --filter status=exited --filter status=dead --filter status=created --format '{{.Names}}\t{{.Status}}' > "$stopped_file"
@@ -810,12 +881,35 @@ collect_log_highlights() {
fi fi
fi fi
# Threshold escalation: how many patterns produced more than the threshold? # Escalation-exempt patterns: known noise that is also permanently very loud
local noise_threshold_exceeded=0 # (e.g. Unraid mdadm parse spam). Without this, such a pattern would keep the
# report stuck at >= WARNUNG forever and devalue the OK/WARNUNG/KRITISCH
# signal. Exempt patterns are still counted/shown as noise, but do NOT count
# toward noise_threshold_exceeded. New/unexpected loud patterns still escalate.
local noise_exempt="$TMP_DIR/noise-escalation-exempt.normalized"
: > "$noise_exempt"
if [ -f "$NOISE_ESCALATION_EXEMPT_FILE" ]; then
grep -Ev '^[[:space:]]*(#|$)' "$NOISE_ESCALATION_EXEMPT_FILE" 2>/dev/null \
| sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' \
| grep -v '^$' > "$noise_exempt" || : > "$noise_exempt"
fi
# Threshold escalation: how many NON-exempt patterns exceeded the threshold?
local noise_threshold_exceeded=0 noise_threshold_exempt=0
if [ -s "$noise_by_pattern" ]; then if [ -s "$noise_by_pattern" ]; then
noise_threshold_exceeded="$(awk -v t="$NOISE_ESCALATION_THRESHOLD" '$1 > t { n++ } END { print n + 0 }' "$noise_by_pattern")" noise_threshold_exceeded="$(awk -F '\t' -v t="$NOISE_ESCALATION_THRESHOLD" '
NR == FNR { exempt[$0] = 1; next }
$1 > t && !($2 in exempt) { n++ }
END { print n + 0 }
' "$noise_exempt" "$noise_by_pattern")"
noise_threshold_exempt="$(awk -F '\t' -v t="$NOISE_ESCALATION_THRESHOLD" '
NR == FNR { exempt[$0] = 1; next }
$1 > t && ($2 in exempt) { n++ }
END { print n + 0 }
' "$noise_exempt" "$noise_by_pattern")"
fi fi
set_summary "noise_threshold_exceeded" "$noise_threshold_exceeded" set_summary "noise_threshold_exceeded" "$noise_threshold_exceeded"
set_summary "noise_threshold_exempt" "$noise_threshold_exempt"
local hit_count attention_count known_noise_count local hit_count attention_count known_noise_count
hit_count="$(count_lines < "$hits")" hit_count="$(count_lines < "$hits")"
@@ -836,6 +930,9 @@ collect_log_highlights() {
if [ "$noise_threshold_exceeded" -gt 0 ]; then if [ "$noise_threshold_exceeded" -gt 0 ]; then
append "- WARNUNG: $noise_threshold_exceeded Pattern ueberschreit(en) die Schwelle - bitte pruefen ob noch wirklich Noise." append "- WARNUNG: $noise_threshold_exceeded Pattern ueberschreit(en) die Schwelle - bitte pruefen ob noch wirklich Noise."
fi fi
if [ "${noise_threshold_exempt:-0}" -gt 0 ]; then
append "- Hinweis: $noise_threshold_exempt laute(s) Pattern ist/sind als bewusst eskalations-befreit markiert (siehe \`$NOISE_ESCALATION_EXEMPT_FILE\`) und loesen keine WARNUNG aus."
fi
append "" append ""
if [ "$attention_count" -eq 0 ]; then if [ "$attention_count" -eq 0 ]; then
@@ -885,22 +982,32 @@ collect_log_highlights() {
if [ -s "$noise_by_pattern" ]; then if [ -s "$noise_by_pattern" ]; then
append "#### Pattern mit den meisten Treffern" append "#### Pattern mit den meisten Treffern"
append "" append ""
append "| Pattern | Anzahl |" append "| Pattern | Anzahl | Hinweis |"
append "|---|---:|" append "|---|---:|---|"
head -n "$NOISE_BREAKDOWN_TOP_N" "$noise_by_pattern" \ head -n "$NOISE_BREAKDOWN_TOP_N" "$noise_by_pattern" \
| while IFS="$(printf '\t')" read -r cnt pat; do | while IFS="$(printf '\t')" read -r cnt pat; do
local short="$pat" local short="$pat" note=""
# Mark patterns that are deliberately exempt from escalation.
if [ -s "$noise_exempt" ] && grep -Fxq -- "$pat" "$noise_exempt"; then
if [ "$cnt" -gt "$NOISE_ESCALATION_THRESHOLD" ]; then
note="eskalations-befreit"
fi
elif [ "$cnt" -gt "$NOISE_ESCALATION_THRESHOLD" ]; then
note="ueber Schwelle"
fi
if [ "${#short}" -gt 80 ]; then if [ "${#short}" -gt 80 ]; then
short="${short:0:77}..." short="${short:0:77}..."
fi fi
# Escape pipe characters that would break the markdown table. # Escape pipe characters that would break the markdown table.
short="${short//|/\\|}" short="${short//|/\\|}"
append "| \`$short\` | $cnt |" append "| \`$short\` | $cnt | $note |"
done done
append "" append ""
fi fi
if [ "$noise_threshold_exceeded" -gt 0 ]; then if [ "$noise_threshold_exceeded" -gt 0 ]; then
append "Bewertung: $noise_threshold_exceeded Pattern ueberschreit(en) die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD). Bitte pruefen, ob die als Noise eingeordneten Meldungen noch fachlich Noise sind oder ob sich ein echter Vorfall darunter versteckt." append "Bewertung: $noise_threshold_exceeded nicht-befreite(s) Pattern ueberschreit(en) die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD). Bitte pruefen, ob die als Noise eingeordneten Meldungen noch fachlich Noise sind oder ob sich ein echter Vorfall darunter versteckt."
elif [ "${noise_threshold_exempt:-0}" -gt 0 ]; then
append "Bewertung: Kein nicht-befreites Pattern ueberschreitet die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD). $noise_threshold_exempt lautes Pattern ist bewusst eskalations-befreit und mit Begruendung dokumentiert."
else else
append "Bewertung: Kein Pattern ueberschreitet die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD)." append "Bewertung: Kein Pattern ueberschreitet die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD)."
fi fi
@@ -4,7 +4,9 @@ set -euo pipefail
TEXTFILE_DIR="${TEXTFILE_DIR:-/mnt/user/services/posture-check/textfile}" TEXTFILE_DIR="${TEXTFILE_DIR:-/mnt/user/services/posture-check/textfile}"
OUTPUT_FILE="${OUTPUT_FILE:-$TEXTFILE_DIR/homelab.prom}" OUTPUT_FILE="${OUTPUT_FILE:-$TEXTFILE_DIR/homelab.prom}"
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}" BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
CRITICAL_CONTAINERS="${CRITICAL_CONTAINERS:-traefik authelia postgresql17 gitea komodo-core komodo-mongo komodo-periphery vaultwarden borg-ui ntfy adguard unbound Tailscale-Docker monitoring-alertmanager monitoring-alertmanager-ntfy-bridge monitoring-blackbox-exporter monitoring-cadvisor monitoring-grafana monitoring-loki monitoring-node-exporter monitoring-promtail immich_server immich_postgres immich_redis paperless-ngx nextcloud nextcloud-postgres nextcloud-redis mealie mealie-postgres}" CRITICAL_CONTAINERS="${CRITICAL_CONTAINERS:-traefik authelia postgresql17 gitea komodo-core komodo-mongo komodo-periphery vaultwarden borg-ui ntfy adguard unbound monitoring-alertmanager monitoring-alertmanager-ntfy-bridge monitoring-blackbox-exporter monitoring-cadvisor monitoring-grafana monitoring-loki monitoring-node-exporter monitoring-promtail immich_server immich_postgres immich_redis paperless-ngx nextcloud nextcloud-postgres nextcloud-redis mealie mealie-postgres}"
# Hinweis: Tailscale laeuft als natives Unraid-Plugin (kein Docker-Container) und
# wird daher hier bewusst NICHT als kritischer Container gefuehrt (Stand 2026-06-06).
mkdir -p "$TEXTFILE_DIR" mkdir -p "$TEXTFILE_DIR"
tmp="$(mktemp "$TEXTFILE_DIR/homelab.prom.XXXXXX")" tmp="$(mktemp "$TEXTFILE_DIR/homelab.prom.XXXXXX")"
@@ -0,0 +1,30 @@
# image-age-allow.patterns - Daily Operations Report
#
# Container, die bewusst auf einem aelteren, aber aktuellen/empfohlenen Image
# gepinnt sind, sollen nicht jeden Tag als "Image ueberaltert" warnen.
#
# Format pro Zeile:
# <container-name> <YYYY-MM-DD recheck> # Begruendung
#
# - Spalte 1: exakter Container-Name (docker ps {{.Names}}).
# - Spalte 2: Recheck-Datum. NACH diesem Datum greift die Ausnahme NICHT
# mehr und der Container taucht wieder als Warnung auf -> erzwingt eine
# menschliche Neubewertung statt stillen Alterns.
# - Alles nach '#' ist Kommentar. Leerzeilen werden ignoriert.
#
# Eine Ausnahme heisst NICHT "Image egal", sondern "am Datum X erneut pruefen,
# ob es noch die empfohlene/aktuelle Version ist".
#
# Last reviewed: 2026-06-10
# immich_postgres: exakt das von Immich offiziell empfohlene, per Digest
# gepinnte DB-Image (14-vectorchord0.4.3-pgvectors0.2.0). Immichs eigene
# docker-compose auf main pinnt am 2026-06-10 denselben Tag inkl. identischem
# Digest. Kein Update, solange Immich nichts Neueres empfiehlt.
# Re-check: ob Immich ein neueres Postgres-Image empfiehlt.
immich_postgres 2026-09-10
# monitoring-blackbox-exporter: v0.28.0 ist am 2026-06-10 die NEUESTE Release
# (Dez 2025). Das Image-Alter ist nur Build-Alter, keine veraltete Version.
# Re-check: ob eine blackbox_exporter-Version > v0.28.0 erschienen ist.
monitoring-blackbox-exporter 2026-09-10
+16 -1
View File
@@ -18,7 +18,7 @@
# Removing a pattern: replace with a fresh attention example in the next # Removing a pattern: replace with a fresh attention example in the next
# daily report and consult before reintroducing. # daily report and consult before reintroducing.
# #
# Last reviewed: 2026-05-21 # Last reviewed: 2026-06-10
# Loki internal query cancellations / scheduler chatter. # Loki internal query cancellations / scheduler chatter.
# Why: Loki cancels internal queries continuously when downstream Promtails # Why: Loki cancels internal queries continuously when downstream Promtails
@@ -72,3 +72,18 @@ authelia.*Request timeout occurred.*status_code=408
# noise becomes overwhelming, add a *narrow* pattern restricted to # noise becomes overwhelming, add a *narrow* pattern restricted to
# push contexts only (e.g. `vaultwarden.*push.*(ResolveError|...)`). # push contexts only (e.g. `vaultwarden.*push.*(ResolveError|...)`).
vaultwarden.*(Token has expired|Invalid refresh token|Failed to decode.*refresh_token|POST /identity/connect/token => 401 Unauthorized) vaultwarden.*(Token has expired|Invalid refresh token|Failed to decode.*refresh_token|POST /identity/connect/token => 401 Unauthorized)
# AdGuard: Fritz!Box sends malformed SOA queries for myfritz.net / myfritz.link.
# Why: AVM Fritz!Box devices send multi-question DNS SOA queries that violate
# RFC 1035 ("only 1 question allowed"). AdGuard rejects them with an error
# but they have no operational impact.
# Re-check: if the same error appears for non-AVM domains, or if rate spikes
# well above 1000/day without a Fritz!Box reboot explaining it.
adguard.*bad question section.*only 1 question allowed
# Grafana: usage-stats collector looks for the Amazon Prometheus plugin, which
# is not installed in this setup. The error is emitted once per stats cycle.
# Why: GF_PLUGINS_PREINSTALL_DISABLED=true keeps the plugin list minimal;
# this lookup is harmless and does not affect any dashboard.
# Re-check: only if Amazon Prometheus is added as a datasource.
monitoring-grafana.*grafana-amazonprometheus-datasource not found
@@ -0,0 +1,32 @@
# noise-escalation-exempt.patterns - Daily Operations Report
#
# Pattern, die als Rauschen bekannt UND dauerhaft sehr laut sind, sollen die
# Eskalations-Schwelle (NOISE_ESCALATION_THRESHOLD) nicht in eine WARNUNG
# uebersetzen. Ohne diese Ausnahme haengt der Report-Status strukturell auf
# >= WARNUNG fest (z. B. mdadm-Noise auf Unraid feuert dauerhaft > 5000/Tag),
# was die OK/WARNUNG/KRITISCH-Ampel entwertet.
#
# Wirkung: Ein hier gelistetes Pattern wird weiterhin als Noise gezaehlt und
# in der Breakdown-Tabelle gezeigt (mit Markierung "eskalations-befreit"),
# zaehlt aber NICHT mehr zu noise_threshold_exceeded. Neue/unerwartete laute
# Patterns loesen weiterhin eine WARNUNG aus.
#
# Format:
# - Exakte Pattern-Zeile wie in log-noise.patterns (nach Normalisierung:
# getrimmt, ohne Kommentar). Muss zeichengenau dem Eintrag entsprechen.
# - Zeilen mit '#' sind Kommentare, Leerzeilen werden ignoriert.
#
# Eine Befreiung heisst NICHT "ignorieren", sondern "Volumen ist als Noise
# akzeptiert; nur die ESKALATION ist abgeschaltet".
#
# Last reviewed: 2026-06-10
# node-exporter kann /proc/mdstat auf Unraid nicht parsen (eigener Array-
# Treiber, kein Linux-mdadm). Dauerhaft > 5000/Tag, rein kosmetisch.
# Re-check: nur bei Migration auf echtes mdadm-RAID.
monitoring-node-exporter.*mdadm.*Cannot parse /host/proc/mdstat
# Fritz!Box sendet RFC-1035-widrige Multi-Question-SOA-Queries fuer
# myfritz.net/myfritz.link; AdGuard lehnt sie ab. ~1000+/Tag, kein Impact.
# Re-check: falls derselbe Fehler fuer Nicht-AVM-Domains auftaucht.
adguard.*bad question section.*only 1 question allowed
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
traefik: traefik:
image: traefik:v3.7@sha256:6b9cbca6fac42ab0075f5437d8dc1685cfd188626d8d515839ea94f8b6271c42 image: traefik:v3.7@sha256:d6858791f9e74df44ca4014166647c41cdc2abd3bf2a71b832ca4e1c6a91b257
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
+41
View File
@@ -0,0 +1,41 @@
# traefik/dynamic/plex.yml
#
# Plex runs with network_mode: host for LAN discovery/GDM. The Docker provider
# resolves host-network containers to 127.0.0.1 from inside Traefik, which causes
# Bad Gateway. Keep this one host-network route as a documented File provider
# exception and point Traefik at the Unraid LAN IP.
http:
routers:
plex-web:
rule: Host(`plex.kaleschke.info`) && (Path(`/`) || Path(`/web`))
entryPoints:
- websecure
middlewares:
- plex-web-redirect
- secure-headers@file
tls:
certResolver: le
service: plex
plex:
rule: Host(`plex.kaleschke.info`)
entryPoints:
- websecure
middlewares:
- secure-headers@file
tls:
certResolver: le
service: plex
middlewares:
plex-web-redirect:
redirectRegex:
regex: "^https://plex\\.kaleschke\\.info(/web)?/?$"
replacement: "https://plex.kaleschke.info/web/index.html"
permanent: true
services:
plex:
loadBalancer:
servers:
- url: http://192.168.178.58:32400