From 5fbda4989dca3d85aeba23bb2f67daed751b8dc2 Mon Sep 17 00:00:00 2001 From: Micha Date: Fri, 26 Jun 2026 08:29:32 +0200 Subject: [PATCH] Document review matrix and drift checks --- CLAUDE.md | 1 + HOMELAB_ARCHITECTURE_MASTER_V2.md | 5 +- docs/AI_CONTEXT.md | 1 + docs/AUTH_MATRIX.md | 7 ++ docs/EXTERNAL_DEPENDENCIES.md | 5 +- docs/HOMELAB_REVIEW_MATRIX.md | 141 ++++++++++++++++++++++++ docs/MASTER_TODO.md | 2 + docs/README.md | 1 + docs/REPO_MAP.md | 2 + docs/SERVICE_CATALOG.md | 10 +- docs/WORKFLOW.md | 8 +- ops/borg-ui/BACKUP_SCOPE.md | 17 +++ ops/healthchecks/README.md | 5 +- services/posture-check/posture-check.sh | 44 +++++++- services/traefik-dynamic-diff.sh | 44 ++++++++ 15 files changed, 282 insertions(+), 11 deletions(-) create mode 100644 docs/HOMELAB_REVIEW_MATRIX.md create mode 100644 services/traefik-dynamic-diff.sh diff --git a/CLAUDE.md b/CLAUDE.md index 5a6166d..0649299 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,7 @@ Zusaetzlich je nach Thema: - Rollback: `docs/ROLLBACK.md` - Secrets: `docs/SECRETS_MAP.md` - GitOps-/Komodo-/Runtime-Drift: `docs/GITOPS_DRIFT_RUNBOOK.md` +- Periodische Homelab-Reviews / Abdeckungslandkarte: `docs/HOMELAB_REVIEW_MATRIX.md` - Gesamtbild fuer KI-Agenten: `docs/AI_CONTEXT.md` - Architektur-/Betriebsentscheidungen mit Begruendung: `docs/DECISIONS.md` diff --git a/HOMELAB_ARCHITECTURE_MASTER_V2.md b/HOMELAB_ARCHITECTURE_MASTER_V2.md index a747f56..3943268 100644 --- a/HOMELAB_ARCHITECTURE_MASTER_V2.md +++ b/HOMELAB_ARCHITECTURE_MASTER_V2.md @@ -171,6 +171,7 @@ Diese Dienste sind **keine Public Apps**: - `super-productivity` — sp.kaleschke.info (Middleware) - `n8n` — n8n.kaleschke.info (Traefik ohne pauschale Middleware, native Auth + Webhook-Ausnahme analog Komodo) - `healthchecks` — hc.kaleschke.info (Traefik, native Healthchecks-Auth; Ping-/API-Endpunkte ohne ForwardAuth analog n8n) +- `dawarich` — dawarich.kaleschke.info (UI hinter Authelia-Middleware; API-Key-Tracking-Pfade fuer OwnTracks/Overland/Traccar bewusst ohne ForwardAuth, siehe §10) - `Traefik-Dashboard` - `AdGuard Home` — Admin-UI auf Port 8082 (`80` im Container), kein Traefik, nur Tailscale-IP `100.80.98.33`; 2026-05-26 bewusst keine 2FA-/Traefik-Umstellung @@ -285,6 +286,7 @@ Legende Status: | `plex` | ✅ | `host` | Traefik via `plex.kaleschke.info` + Plex native Auth; LAN direkt `:32400` | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme; Traefik routet per File-Provider-Ausnahme auf `http://192.168.178.58:32400`, weil Docker-Labels Host-Netz-Container aus Traefik heraus auf `127.0.0.1` routen wuerden; kein direkter WAN-Port 32400 und Plex Remote Access bleibt aus; Server geclaimt von `Xeridos`; Smart-TVs (Schlafzimmer, Wohnzimmer) ueber WLAN-LAN per mDNS | — | | `super-productivity` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | Persoenliche Task-PWA des Operators; Issues kommen aus Gitea `Micha/mails` via n8n-Mail-Workflow | Deploy + Webhook + DNS-Eintrag offen | | `n8n` | ✅ vorbereitet | `frontend_net` | Traefik, native Auth (keine pauschale Authelia) | Workflow-Automation; erster Workflow: GMX-Mail -> OpenAI-Extraktion -> Gitea-Issue in `Micha/mails`; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret | Deploy + Webhook + Owner-Setup offen | +| `dawarich_app` | ✅ | `frontend_net`, `backend_net` | Traefik via `dawarich.kaleschke.info`; UI hinter Authelia, API-Key-Pfade ohne ForwardAuth | Standort-Historie (Google-Timeline-Ersatz) mit eigener PostGIS-DB + Redis; `SELF_HOSTED=true`, `STORE_GEODATA=true`, keine externe Geocoding-Egress in der Compose gesetzt; Sidekiq-Worker im `backend_net` | Backup-Scope (`dawarich.dump` + Appdata) noch nicht verdrahtet, siehe `docs/MASTER_TODO.md` | ### 7.5 Admin / Operations @@ -311,7 +313,7 @@ Legende Status: | `monitoring-influxdb3-core` | ✅ | `monitoring_net`, `monitoring_influx_lan` + LAN-Bind | LAN-Port nur fuer interne Writer | InfluxDB 3 Core fuer Home-Assistant-/Ecowitt-Langzeitdaten; keine Traefik-/Public-Freigabe; Port 8181 nur via `INFLUXDB_BIND_IP` | HA-Write-Token und Sensor-Export finalisieren | | `monitoring-loki` | ✅ | `monitoring_net` | intern | interner Container-Logspeicher ohne Public Route; Monitoring-Grafana greift ueber Loki-Datasource zu | Retention/Storage beobachten | | `monitoring-promtail` | ✅ | `monitoring_net` | intern | Docker-Log-Collector mit read-only Docker-Socket-Ausnahme; schreibt nach Loki | Socket-Ausnahme regelmaessig pruefen | -| `healthchecks` | ✅ | `frontend_net`, `healthchecks_internal` | Traefik, native Auth | self-hosted Heartbeat-Monitor fuer interne Jobs; Ping-/API ohne ForwardAuth (analog n8n); externe Host-down-/Backup-Waechter bleiben auf healthchecks.io-Cloud; live seit 2026-06-23 | Gitea-Webhook noch manuell anzulegen | +| `healthchecks` | ✅ | `frontend_net`, `healthchecks_internal` | Traefik, native Auth | self-hosted Heartbeat-Monitor fuer interne Jobs; Ping-/API ohne ForwardAuth (analog n8n); externe Host-down-/Backup-Waechter bleiben auf healthchecks.io-Cloud; live seit 2026-06-23, Gitea->Komodo-Webhook aktiv | Status in `docs/MASTER_TODO.md` | | `healthchecks-postgres` | ✅ | `healthchecks_internal` | intern | dedizierte PostgreSQL 18, nie `frontend_net` | — | | `grafana` / `influxdb3-core` / `loki` / `alloy` | entfernt | - | abgeloest | alte Docker-Runtime frei von Altcontainern; Compose-Pfade am 2026-05-26 aus aktivem Repo entfernt | Rollback nur ueber Git-Historie | @@ -403,6 +405,7 @@ Die Blockmigration aus der Portainer-/Dockerman-Phase ist abgeschlossen: Traefik | `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) | | `healthchecks` | keine pauschale Authelia-Middleware | Ping-Endpunkte (`/ping/*`) und API muessen ohne ForwardAuth erreichbar sein, sonst koennen Cron-Jobs nicht melden; Healthchecks bringt eigene Login-Auth fuers Dashboard mit (analog n8n/Komodo). Der self-hosted Dienst deckt bewusst nur INTERNE Job-Checks ab; Host-down-/Backup-Waechter bleiben extern auf healthchecks.io-Cloud | +| `dawarich` (API-Key-Pfade) | UI hinter Authelia, aber separater Router `dawarich-api` ohne ForwardAuth | Tracking-Ingest-Pfade (`/api/v1/owntracks/points`, `/overland/batches`, `/traccar/points`, `/points`, `/tracks`, `/settings`, `/health`) muessen fuer OwnTracks/Overland/Traccar-Clients ohne ForwardAuth erreichbar sein (analog n8n-Webhook); Schutz ist der Dawarich-App-API-Key. Der UI-Router `dawarich` (`priority=10`) bleibt unter `authelia@file`. Standortdaten sind hochsensibel — keine weiteren Pfade ohne bewusste Entscheidung in die Bypass-Regel aufnehmen. Effektive Auth-Sicht: `docs/AUTH_MATRIX.md` | | `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. | | `homeassistant` | Traefik ohne Authelia, Fach-YAML aus separatem Repo | Home Assistant bringt eigene Auth, mobile Apps, Webhooks und Integrationsfluesse mit. Der Container haengt in `frontend_net` fuer Traefik und in `smarthome_net` fuer MQTT/Zigbee2MQTT/ESPHome. `.storage` und Secrets bleiben in Appdata und werden per Borg gesichert, nicht versioniert. | | `homeassistant` (Ecowitt) | LAN-only Host-Port `8123` auf `192.168.178.58` | Ecowitt-GW3000 kann kein HTTPS und pusht per HTTP an den HA-Webhook. HA bekommt einen Host-Bind nur auf der LAN-IP (`192.168.178.58:8123:8123`, nicht `0.0.0.0`/WAN), analog InfluxDB 8181. Kein Traefik-Umbau des globalen HTTP-Redirects noetig, da Ecowitt rein im LAN pusht. Webhook nicht `local_only`, geschuetzt durch 128-bit-Zufalls-ID. Siehe `docs/DECISIONS.md` (2026-06-13). | diff --git a/docs/AI_CONTEXT.md b/docs/AI_CONTEXT.md index 4be5d56..f3bffb1 100644 --- a/docs/AI_CONTEXT.md +++ b/docs/AI_CONTEXT.md @@ -23,6 +23,7 @@ Diese Datei enthaelt bewusst **keinen** Arbeitsstand mehr — Status nur in 4. bei Service-Fragen `docs/SERVICE_CATALOG.md` 5. bei Restore/DR `docs/DISASTER_RECOVERY.md` und `docs/RESTORE_MATRIX.md` 6. bei "warum ist das so?"-Fragen `docs/DECISIONS.md` +7. bei periodischen Reviews `docs/HOMELAB_REVIEW_MATRIX.md` ## Harte Regeln diff --git a/docs/AUTH_MATRIX.md b/docs/AUTH_MATRIX.md index c0e3be5..9476480 100644 --- a/docs/AUTH_MATRIX.md +++ b/docs/AUTH_MATRIX.md @@ -45,6 +45,8 @@ buendelt das an **einem** Ort und **verlinkt** auf die Quellen statt zu kopieren | `n8n.kaleschke.info` | Keine pauschale Authelia (native) | Webhook-Endpunkte `/webhook/*` | Ausnahmen-Tabelle; ⚠ Middleware-Abweichung lt. policy-check | | `plex.kaleschke.info` | Keine Authelia (native Plex) | File-Provider-Route; WAN-Port 32400 + Remote Access aus | DECISIONS 2026-05-28 | | `home.kaleschke.info` (homeassistant) | Keine Authelia (native HA) | Traefik + `smarthome_net`; LAN-Port 8123 | Ausnahmen-Tabelle; ⚠ Middleware-Abweichung lt. policy-check | +| `dawarich.kaleschke.info` (UI) | **two_factor** | `authelia@file,secure-headers@file` (Router `dawarich`, `priority=10`) | `apps/dawarich/docker-compose.yml` Labels; Standortdaten hinter Authelia | +| `dawarich.kaleschke.info` (API-Key-Ingest-Pfade) | **Bypass/native** (App-API-Key, kein 2FA) | Router `dawarich-api` (`priority=100`) nur `secure-headers@file`, Pfade `/api/v1/{health,settings,points,tracks,owntracks,overland,traccar,...}` | `apps/dawarich/docker-compose.yml` Labels; OwnTracks/Overland/Traccar-Tracking muss ohne ForwardAuth pushen koennen (analog n8n-Webhook) | | AdGuard-Admin | **Tailscale-only**, nicht oeffentlich | Host-Bind `100.80.98.33:8082`, kein Traefik | DECISIONS 2026-05-26 | | `influxdb3-core` :8181 | **LAN-only** Writer (HA) | Host-Port, kein Traefik, nicht in `frontend_net` | dokumentierte Ausnahme | @@ -60,6 +62,11 @@ buendelt das an **einem** Ort und **verlinkt** auf die Quellen statt zu kopieren `homeassistant` sind erwartbar (native Ausnahmen). `grafana` steht **nicht** in der Ausnahmen-Tabelle und ist als Catch-all-`two_factor` gefuehrt — die abweichende Middleware ist live zu bestaetigen (offen). +- **Dawarich API-Key-Pfade**: Der `dawarich-api`-Router umgeht Authelia bewusst + fuer die Tracking-Ingest-Pfade (hochsensible Standortdaten). Schutz liegt allein + im Dawarich-App-API-Key. Review-Trigger = neue oeffentliche API-Pfade in der + Router-Regel oder veraendertes Risikoprofil der Standortdaten. Begruendung jetzt + auch in `HOMELAB_ARCHITECTURE_MASTER_V2.md` §10. ## Pflege diff --git a/docs/EXTERNAL_DEPENDENCIES.md b/docs/EXTERNAL_DEPENDENCIES.md index db973af..dc6a15c 100644 --- a/docs/EXTERNAL_DEPENDENCIES.md +++ b/docs/EXTERNAL_DEPENDENCIES.md @@ -18,11 +18,12 @@ Dieses Dokument beschreibt externe Anbieter und Konten, von denen Betrieb, Recov | GitHub Mirror | Externer Repo-Mirror `michaelkaleschke-spec/homelab-infra` (privat) | mittel/hoch | Gitea-Verlust abfederbar, aber Bare-Metal-Bootstrap braucht Read-Zugang (PAT oder SSH-Deploy-Key); ohne diesen ist der Mirror im DR nicht klonbar | GitHub-Konto; Push-PAT liegt in Gitea-Mirror-Settings; **Read-PAT/Deploy-Key fuer DR muss zusaetzlich offline im DR-Kit liegen** | Mirror-Status regelmaessig pruefen; lokalen Clone als zweite Kopie behalten; Read-PAT mit Scope `repo:read` separat erzeugen und im DR-Kit ablegen | | Tailscale | Remote-/Operator-Zugang | hoch | Remote-Zugriff erschwert, lokale Bedienung bleibt | Tailnet-Konto; Node `Kallilabcore`, IPv4 `100.80.98.33` | Break-glass per LAN und physischem Zugriff; Tailnet-Recovery-Codes sichern | | GMX SMTP | Authelia Notifier, Vaultwarden-Einladungen, Ops-Report-Mail | mittel | Mail-Notifier und Vaultwarden-Einladungen fallen aus; Login selbst nicht zwingend | GMX-Konto; SMTP-Secrets liegen hostseitig | ntfy/zweiter SMTP als Fallback pruefen | -| OpenAI API | Paperless-GPT LLM und Vision-OCR | mittel | Automatische Dokument-Titel, Tags, Korrespondenten und LLM-OCR fallen aus; Paperless selbst laeuft weiter | OpenAI-Projekt/API-Key ausserhalb Repo | Key in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten | +| OpenAI API | Paperless-GPT LLM/Vision-OCR (Dokumenttext/-bilder) **und** n8n Mail->LLM->Gitea-Workflow (GMX-Mailinhalt bis ~8000 Zeichen) | mittel | Automatische Dokument-Titel/Tags/Korrespondenten/LLM-OCR und die n8n-Mail-Extraktion fallen aus; Paperless und n8n laufen sonst weiter | je eigener OpenAI-API-Key ausserhalb Repo (Paperless-GPT Stack-ENV; n8n Credentials Store) | Keys in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten. **Datenklasse/Retention/Loeschpfad je Egress-Pfad noch offen** (`docs/HOMELAB_REVIEW_MATRIX.md` Domaene 12, `docs/MASTER_TODO.md`) | | 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 | | 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 (self-hosted) und ggf. mobile Plattform-Pushes; Egress = Alert-Payloads/Topic-Namen an Upstream-Push | niedrig/mittel | Alerts erreichen Mobilgeraete ggf. nicht | App-/Device-seitig | Kritische Alerts zusaetzlich in Grafana/Glance sichtbar halten; Payloads/Topics ohne PII/Pfade/Secrets halten (Naming-Regel offen, `docs/HOMELAB_REVIEW_MATRIX.md` Domaene 12) | +| healthchecks.io (Cloud) | Externer Dead-Man's-Switch fuer Host-down-/Backup-Stillstand: Borg-Pre-Hook, baerchen-Nearline-Pull, geplanter Monitoring-Watchdog (#8); interne Job-Checks laufen self-hosted | mittel | Stiller Ausfall von Borg-/Nearline-Lauf wird nicht extern sichtbar; Backups selbst laufen weiter | healthchecks.io-Account; Ping-/Capability-URLs als Host-Secrets (`docs/SECRETS_MAP.md`) | Datenklasse = operative Metadaten (Check-Name/Timing); Naming ohne PII/Pfade/Secrets. Skripte sind endpoint-agnostisch -> bei Bedarf auf self-hosted `hc.kaleschke.info` umstellbar. Bewusste Architektur (DECISIONS 2026-06-23): Host-down-Waechter bleiben extern | | Operator-DR-Workstation | Bare-Metal-Recovery-Arbeitsplatz (Gaming-PC Windows, lokaler Repo-Clone `G:\Gitea_Clone\homelab-infra`) | kritisch | Ohne Workstation kein Borg-Extract, kein Hetzner-Zugriff, kein Repo-Bootstrap; der Unraid-Host ist im Bare-Metal-Fall gerade weg | Operator-PC, WSL2 + Borg-Client, SSH-Key fuer Hetzner Storage Box, Offline-Kopie der Borg-Passphrase | Setup als bewusste DR-Vorbedingung pflegen (siehe Abschnitt "DR-Workstation Bare-Metal-Kit") | ## Kritische Secrets ausserhalb des Repos diff --git a/docs/HOMELAB_REVIEW_MATRIX.md b/docs/HOMELAB_REVIEW_MATRIX.md new file mode 100644 index 0000000..32e50d0 --- /dev/null +++ b/docs/HOMELAB_REVIEW_MATRIX.md @@ -0,0 +1,141 @@ +# Homelab Review Matrix + +Typ: Inventar/Review-Landkarte - Stand: 2026-06-26 - Status: aktiv + +Diese Matrix erzwingt Abdeckung, nicht Methode. Jede Domaene wird +threat-model-first und mit echtem Ist-Zustand geprueft: Repo, Compose, +Runbooks, Host-Checks, Docker Inspect, API-Exports und Restore-Belege. +Generische Best-Practice-Listen sind hier nur Hilfsmittel, nicht das Ziel. + +## Ritual + +1. Aelteste oder sicherheitskritische Domaene aus dem Review-Log waehlen. +2. Genau diese eine Domaene tief pruefen. +3. Repo-Soll und Live-Ist getrennt halten: "belegt", "nicht belegt", + "offene Host-Pruefung". +4. Keine Secret-Werte lesen, zitieren oder ins Repo schreiben. +5. Konkrete Befunde in die passende Heimat ueberfuehren: + `MASTER_TODO.md` fuer offene Arbeit, `DECISIONS.md` fuer Entscheidungen, + Zielbild in Architecture/Inventare/Service Catalog, Ablauf in Runbooks. + +Kadenz-Vorschlag: eine Domaene alle zwei Wochen. Sicherheitskritisch haeufiger: +Exposure, Identity, Backup-Restore, Data Privacy/Egress, GitOps-Control-Plane. + +## Review-Log + +| # | Domaene | Reifegrad (1-5) | Zuletzt geprueft | Befund / offene Punkte | +|---|---|:---:|:---:|---| +| 1 | Exposure & Ingress | ? | - | | +| 2 | Identity & Access | ? | - | Authelia-Ausnahmen und native Auth regelmaessig gegen Live-Routen testen | +| 3 | Secrets & Key-Management | ? | in Umsetzung | SOPS+age-Migration/Rotation offen; keine Secret-Werte in Git | +| 4 | Netzwerk-Segmentierung & Blast Radius | ? | - | | +| 5 | Container- & Host-Haertung | ? | - | | +| 6 | Supply Chain | ? | teilweise | Digest-Pinning belegt; Signatur-Verifikation/SBOM/CVE-Gate offen | +| 7 | Backup != Restore | ? | - | Borg->Hetzner belegt; Restore-Drills/Immutability weiter rotieren | +| 8 | Datenintegritaet | ? | - | | +| 9 | Observability-Luecken | ? | - | Alert-on-Absence/Dead-Man-Switch belegt, weiter beweisen | +| 10 | Exit & Portierbarkeit | ? | - | | +| 11 | Update- & Patch-Hygiene | ? | teilweise | Renovate belegt; Host/Engine/Auth-Layer separat betrachten | +| 12 | Data Privacy / Egress / Datenklassifikation | 2 | 2026-06-26 | OpenAI-/Mail-/Healthchecks-/ntfy-/Borg-Egress kartiert; Retention/Loeschpfade und Dawarich-Abdeckung offen | +| 13 | GitOps-Control-Plane & Drift | 3 | 2026-06-26 | Repo-Soll stark; Live-Abgleich fuer Komodo/Runtime/Traefik/Authelia offen; Self-Stack und manuelle Sync-Ausnahmen sind hoechster Hebel | + +## 12 - Data Privacy / Egress / Datenklassifikation + +**Bedrohung:** Personenbezogene oder sensible Daten verlassen das Lab +unkontrolliert: falscher Dienst, falscher Token, keine Retention, kein +Loesch-/Exportpfad. + +**Leitfragen** + +- Welche Dienste verarbeiten personenbezogene oder sensible Daten? +- Welche Daten verlassen das Lab an externe Dritte, ueber welchen Pfad und mit + welchem Token/Account? +- Welche Retention gilt beim Dritten? Gibt es Loeschung, Export und Opt-out? +- Gibt es pro Dienst eine Datenklasse und eine erlaubte Egress-Klasse? +- Driftet die reale Datenverarbeitung von `SERVICE_CATALOG.md`, + `EXTERNAL_DEPENDENCIES.md`, Architecture oder Restore-Doku ab? + +**Messpunkte** + +```bash +rg -n "OPENAI|api\.openai|ANTHROPIC|api_key|API_KEY|webhook|https?://" -g "docker-compose.y*ml" -g "*.env*" +rg -ni "openai|gpt|external|cloud|telemetry|analytics" apps/ ops/ monitoring/ +rg -n "httpRequest|emailReadImap|webhook|openai|Authorization|api/v1" apps/n8n/workflows -g "*.json" +rg -n "Dawarich|dawarich|OpenAI|healthchecks.io|ntfy.sh|GitHub Mirror|Hetzner|GMX" docs HOMELAB_ARCHITECTURE_MASTER_V2.md +``` + +**Befund 2026-06-26** + +| Pfad | Belegt | Risiko | +|---|---|---| +| Paperless-GPT -> OpenAI | `apps/paperless-gpt/docker-compose.yml` nutzt OpenAI fuer LLM/Vision OCR | Dokumenttext/Bilder/Metadaten koennen extern verarbeitet werden; Retention/Loeschpfad nicht im Repo dokumentiert | +| n8n Mail -> OpenAI -> Gitea | Workflow-JSON im Repo, Export war inaktiv; live nicht verifiziert | Mailinhalt bis 8000 Zeichen kann an OpenAI gehen; Live-Status und Retention offen | +| GMX IMAP/SMTP | n8n, Vaultwarden, Authelia/Mailpfade dokumentiert | Mailprovider-Retention und Scope nicht als Matrix erfasst | +| Borg -> Hetzner | `EXTERNAL_DEPENDENCIES.md`, Borg-Scope | Verschluesselte Offsite-Kopie vieler Datenklassen; Immutability/Retention weiter beweisen | +| ntfy/Healthchecks | self-hosted + externe Cloud-Waechter dokumentiert | Alert-/Checknamen koennen operative Metadaten leaken | +| Dawarich | Service Catalog + Compose vorhanden | Standortdaten sind hochsensibel; Architecture/Auth/Restore/Backup-Abdeckung war nicht konsistent sichtbar | + +**Naechste Optimierungen** + +- OpenAI-Retention, Datenkontrollen, Loesch-/Exportpfad und erlaubte Datenklassen + fuer Paperless-GPT und n8n dokumentieren. +- Paperless-GPT nur ueber Tag-/Allowlist oder klare Ausschlussregel auf + Dokumente anwenden. +- Dawarich in Architecture/Auth/Restore/Backup-Scope konsistent eintragen oder + bewusst als Ausnahme dokumentieren. +- Healthchecks/ntfy-Naming-Regel: keine PII, keine Pfade, keine Secrets, keine + aussagekraeftigen internen Namen in Payloads. + +## 13 - GitOps-Control-Plane & Drift + +**Bedrohung:** Der Steuerpfad driftet vom deklarierten Sollzustand ab oder wird +kompromittiert. Gitea, Komodo, Webhooks, Traefik dynamic config und Authelia +Host-Config verteilen Fehler auf alle Stacks. + +**Leitfragen** + +- Stimmen lokaler Clone, Gitea `origin/master`, Komodo Stack Workspace, + Docker Runtime und Host-Dateien ueberein? +- Welche Stacks laufen live ohne Repo-Quelle, und welche Repo-Compose-Dateien + haben keinen Komodo-Stack? +- Welche Stack-ENV-Werte werden live in Komodo gepflegt und sind nicht in Git? +- Wie sind Gitea->Komodo-Webhooks authentifiziert, auf Branch `master` + begrenzt und gegen falsche Ausloeser geschuetzt? +- Wer hat Schreibrechte auf Gitea, Komodo und Bot-/Deploy-Tokens? +- Kann ein Push direkt deployen, oder gibt es technische Branch Protection? +- Ist der Recovery-Pfad ohne laufendes Komodo belegt? + +**Messpunkte** + +```bash +powershell -ExecutionPolicy Bypass -File .\ops\policy-checks\check_repo.ps1 +SEND_NTFY=0 bash /mnt/user/services/homelab-infra/services/posture-check/compose-runtime-drift.sh +SEND_NTFY=0 bash /mnt/user/services/homelab-infra/services/posture-check/komodo-stack-hygiene.sh +bash /mnt/user/services/homelab-infra/services/authelia-diff.sh +rsync -nci --checksum /mnt/user/services/homelab-infra/traefik/dynamic/ /mnt/user/appdata/traefik/dynamic/ +``` + +**Befund 2026-06-26** + +| Bereich | Belegt | Risiko | +|---|---|---| +| Repo-Soll | `check_repo.ps1`: 0 Critical; lokaler `HEAD` == `origin/master` | Git-Ebene sauber | +| Runtime-Ist | Aus der Windows-Shell nicht gegen Unraid verifizierbar | Host-Pruefung offen; lokale Docker-Missing-Meldungen nicht als Homelab-Drift werten | +| Komodo Self-Stack | `ops/komodo/docker-compose.yml` ist Spiegel/Recovery-Anker; live inline in Komodo | Repo-Aenderung wirkt nicht live; hoechster Drift-Hebel | +| Stack-ENV | Komodo Stack-ENV bewusst nicht vollstaendig in Git | Git allein ist kein vollstaendiger Sollzustand | +| Traefik dynamic | Manuelle Sync-Ausnahme | Auth-Middlewares/File-Routen koennen vom Repo abweichen | +| Authelia Host-Config | `authelia-diff.sh` vergleicht `access_control` | Live-Host-Datei und OIDC-Clientnamen muessen regelmaessig abgeglichen werden | +| Gitea/Komodo Integritaet | Webhook-Regeln dokumentiert; Branch Protection nicht repo-belegt | Prozess-Gate belegt, technisches Enforcement offen | + +**Naechste Optimierungen** + +- Auf dem Unraid-Host `compose-runtime-drift.sh` und + `komodo-stack-hygiene.sh` ausfuehren und Ergebnisse in + `/mnt/user/services/posture-check/*.json` bewerten. +- Traefik dynamic Drift-Check analog Authelia-Diff als read-only Check + betreiben (`services/traefik-dynamic-diff.sh` / `traefik_dynamic_drift`). +- Komodo Inline-Compose gegen `ops/komodo/docker-compose.yml` diffen, Secret- + Werte maskieren, nur Variablennamen/Pfade dokumentieren. +- Gitea Branch Protection, aktive Hooks, PATs und Deploy-Keys live + inventarisieren; ungenutzte Zugriffe entfernen. +- Healthchecks-Doku-Widerspruch "Webhook offen" vs "Webhook aktiv" bereinigen. diff --git a/docs/MASTER_TODO.md b/docs/MASTER_TODO.md index d87c382..ee15814 100644 --- a/docs/MASTER_TODO.md +++ b/docs/MASTER_TODO.md @@ -25,6 +25,8 @@ Host-Reports (`/mnt/user/backups/restore-reports/`) und in der Git-Historie. | Restore-Test Tailscale | Operator | State-Validierung + Reconnect nur auf Wegwerf-Host/VM, danach Geraet in Tailscale-Admin entfernen | `ops/restore-tests/tailscale-runbook.md` | | Authelia OIDC fuer Apps | Operator/Codex | Live: Grafana + Mealie login-verifiziert; Paperless Secret verdrahtet und Service-Smoke am 2026-06-17 gruen, finaler Browser-Login mit Operator-Account offen. Immich + Nextcloud bewusst geparkt bis Family-Onboarding (siehe `docs/DECISIONS.md` 2026-06-06) | `docs/AUTHELIA_OIDC_PLAN.md` | | Home Assistant Tibber | Operator/Codex | Tibber per HA-UI-Config-Flow verbinden. Danach Energy-Dashboard um echte Kosten/Preisquelle ergaenzen; SolarEdge-PV, Netz und Speicher sind bereits konfiguriert und validiert | `docs/runbooks/smart-home-bootstrap.md`, `docs/DECISIONS.md` | +| Review-Follow-up Data Privacy/Egress | Operator/Codex | (1) Datenklasse + Retention/Loeschpfad je externem Egress-Pfad dokumentieren (OpenAI fuer Paperless-GPT+n8n, GMX, healthchecks.io, ntfy-Upstream, Hetzner, GitHub-Mirror). (2) **Dawarich-Backup-Scope schliessen** (Standortdaten aktuell nicht DR-gesichert): `dawarich.dump` in `pre-backup-dumps.sh`, Pfad in Quellliste + Borg-UI-Mount + `BACKUP_SCOPE.md`, danach `RESTORE_MATRIX.md` Tier-2-Zeile + Reifegrad. Dawarich in Architecture §4.2/§7.4/§10 + `AUTH_MATRIX.md` ist am 2026-06-26 angeglichen. | `docs/HOMELAB_REVIEW_MATRIX.md` Domaene 12 | +| Review-Follow-up GitOps-Control-Plane | Operator/Codex | Auf Unraid `compose-runtime-drift.sh`, `komodo-stack-hygiene.sh`, `authelia-diff.sh` und `traefik-dynamic-diff.sh` laufen lassen; danach Branch-Protection/Hooks/PATs in Gitea live inventarisieren | `docs/HOMELAB_REVIEW_MATRIX.md` Domaene 13 | | Nearline-Pull Dead-Man's-Switch | Operator | **S4U-Root-Cause 2026-06-21 behoben + verifiziert:** Task `KalliLab H Drive Nearline Pull` von S4U auf LogonType `Interactive` ("Nur wenn Benutzer angemeldet") umgestellt (kein Passwort noetig, da `michi` Dauer-Konsolen-User) -> per Planer mit `0x0` bestaetigt. Spiegel frisch, Exit-Code-Leak gefixt, Heartbeat-Pings gepusht. **Verbleibt (optional, niedrige Dringlichkeit):** je einen Healthchecks-Check anlegen + Capability-URL hinterlegen (baerchen ENV `HEALTHCHECKS_NEARLINE_URL`/Datei; Unraid `/mnt/user/appdata/secrets/healthchecks_borg_url`) | `ops/h-drive-nearline/README.md` | | Healthchecks self-hosted (interne Jobs) | Operator | **Live seit 2026-06-23** auf `https://hc.kaleschke.info` (Komodo-Stack-ID `6a3acf2ca7867a4fbab9bfc1`, beide Container healthy, Superuser angelegt). Gitea->Komodo-Webhook seit 2026-06-23 aktiv. Projekt `KalliLab CORE` + ntfy-Integration (`homelab-alerts`). **7 interne Jobs verdrahtet + verifiziert (Status `up`):** `posture-check` (stuendlich), `cert-token-check`/`compose-runtime-drift`/`daily-status-report` (taeglich), `komodo-stack-hygiene` (woechentlich), `renovate`/`gitea-bundle-mirror` (alle 6h) - je endpoint-agnostischer Ping via EXIT-Trap, Capability-URL als Host-Secret `healthchecks__url`. **Bewusst NICHT self-hosted:** Borg-Pre-Hook + Nearline bleiben healthchecks.io-Cloud (Host-down-Erkennung); guarded Restore-Jobs (Vaultwarden/Gitea/Paperless/Authelia/Immich) sind wegen Shell-Guard nicht je Cron-Trigger monitorbar. **Verbleibt nur noch optional:** die 2 externen Cloud-Checks scharfschalten. | `ops/healthchecks/README.md` | diff --git a/docs/README.md b/docs/README.md index 72d4116..fd0f97c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,7 @@ geloescht (Git-Historie ist das Archiv). Verbindliche Doku-Regeln: | `WORKFLOW.md` | verbindlicher GitOps-/No-Drift-Ablauf | | `REPO_MAP.md` | technische Landkarte des Repositories + Doku-Regeln | | `SERVICE_CATALOG.md` | produktiver Service-Katalog | +| `HOMELAB_REVIEW_MATRIX.md` | periodische Review-Landkarte fuer Security, Drift, Restore und Privacy | | `DECISIONS.md` | Entscheidungs-Register (ADR-light) | | `MASTER_TODO.md` | einzige operative Statusliste | diff --git a/docs/REPO_MAP.md b/docs/REPO_MAP.md index 00b9ef0..021ffd6 100644 --- a/docs/REPO_MAP.md +++ b/docs/REPO_MAP.md @@ -29,6 +29,7 @@ Details gilt immer die betroffene Compose-Datei oder das jeweilige Runbook. | `HOMELAB_ARCHITECTURE_MASTER_V2.md` | Architektur, Netzmodell, Ausnahmen | | `docs/WORKFLOW.md` | vor operativen Aenderungen | | `docs/SERVICE_CATALOG.md` | Service-Zweck, Pfade, Besonderheiten | +| `docs/HOMELAB_REVIEW_MATRIX.md` | periodische Review-Landkarte fuer Abdeckung, Messpunkte und offene Review-Follow-ups | | `docs/DISASTER_RECOVERY.md` | echter Wiederanlauf | | `docs/RESTORE_MATRIX.md` | Restore-Quelle je Dienst | | `docs/SECRETS_MAP.md` | Secret-Namen und Pfade ohne Werte | @@ -49,6 +50,7 @@ Details gilt immer die betroffene Compose-Datei oder das jeweilige Runbook. | `services/posture-check/posture-check.sh` | Host-Posture-Check | | `services/posture-check/export-prometheus-textfile.sh` | Borg-/Container-/Drift-Metriken | | `services/authelia-diff.sh` | Authelia ACL Repo-zu-Host-Vergleich | +| `services/traefik-dynamic-diff.sh` | Traefik dynamic Repo-zu-Host-Vergleich | | `ops/h-drive-nearline/pull-critical-backups.ps1` | H:/ Nearline-Pull | ## Doku-Regeln diff --git a/docs/SERVICE_CATALOG.md b/docs/SERVICE_CATALOG.md index 02ce105..7aef108 100644 --- a/docs/SERVICE_CATALOG.md +++ b/docs/SERVICE_CATALOG.md @@ -43,8 +43,8 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und | `immich_machine_learning` | Immich ML | `apps/immich/docker-compose.yml` | intern | `immich_default`, `immich_egress` | `model-cache` | rebuildbar | nein | keine Traefik-Route; `immich_egress` (nicht-internal) nur fuer Modell-Download zu huggingface, sonst scheitert Smart Search/Gesichtserkennung an DNS | | `mealie` | Rezeptverwaltung | `apps/mealie/docker-compose.yml` | `https://mealie.kaleschke.info` | `mealie-postgres`, Traefik | `/mnt/user/appdata/mealie/data` | Tier 2, Borg + `mealie.dump` | ja | App + DB in internem Netz getrennt | | `mealie-postgres` | Mealie-Datenbank | `apps/mealie/docker-compose.yml` | intern | `mealie_internal` | `/mnt/user/appdata/mealie/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/mealie-postgres17`, `mealie_postgres_password.txt` | Dump `mealie.dump` | nein | interne DB; PostgreSQL 18 | -| `dawarich_app` | Standort-Historie / Google-Timeline-Ersatz | `apps/dawarich/docker-compose.yml` | `https://dawarich.kaleschke.info` | eigene PostGIS-DB, eigene Redis, Traefik + Authelia, optional Home Assistant Push | `/mnt/user/appdata/dawarich/{postgres17,redis,shared,public,watched,storage}`, `dawarich_*.txt` Secrets | Tier 2, Borg + `dawarich.dump` | ja + Authelia | UI hinter Authelia; API-Key-Tracking-Endpunkte fuer OwnTracks/Overland/Traccar ohne ForwardAuth priorisiert. App und Sidekiq nutzen `freikin/dawarich:1.8.1`; Prometheus-Scrape nach aktueller Dawarich-Doku ueber `dawarich_app:3000/metrics`, Sidekiq-Metriken intern ueber `:9394`. | -| `dawarich_db` | Dawarich PostGIS-Datenbank | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/postgres17`, `dawarich_postgres_password.txt`, `dawarich_grafana_ro_password.txt` | Dump `dawarich.dump`; raw DB nur bei gleichem PG/PostGIS und sauberem Shutdown | nein | PostGIS 17-3.5 Alpine; Grafana-Read-only-User `dawarich_grafana_ro` per Init-Script | +| `dawarich_app` | Standort-Historie / Google-Timeline-Ersatz | `apps/dawarich/docker-compose.yml` | `https://dawarich.kaleschke.info` | eigene PostGIS-DB, eigene Redis, Traefik + Authelia, optional Home Assistant Push | `/mnt/user/appdata/dawarich/{postgres17,redis,shared,public,watched,storage}`, `dawarich_*.txt` Secrets | Tier 2 (Ziel); **Backup-Scope noch offen** - `dawarich.dump` + Appdata sind weder in `ops/borg-ui/BACKUP_SCOPE.md` noch in der Quellliste/`pre-backup-dumps.sh`; bis dahin Standortdaten **nicht** DR-gesichert (siehe `docs/MASTER_TODO.md`) | ja + Authelia | UI hinter Authelia; API-Key-Tracking-Endpunkte fuer OwnTracks/Overland/Traccar ohne ForwardAuth priorisiert (Auth-Sicht: `docs/AUTH_MATRIX.md`, Ausnahme: Architecture §10). App und Sidekiq nutzen `freikin/dawarich:1.8.1`; Prometheus-Scrape nach aktueller Dawarich-Doku ueber `dawarich_app:3000/metrics`, Sidekiq-Metriken intern ueber `:9394`. | +| `dawarich_db` | Dawarich PostGIS-Datenbank | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/postgres17`, `dawarich_postgres_password.txt`, `dawarich_grafana_ro_password.txt` | Dump `dawarich.dump` **geplant** (noch nicht in `pre-backup-dumps.sh`); raw DB nur bei gleichem PG/PostGIS und sauberem Shutdown | nein | PostGIS 17-3.5 Alpine; Grafana-Read-only-User `dawarich_grafana_ro` per Init-Script | | `dawarich_redis` | Dawarich Cache/Queue-Backend | `apps/dawarich/docker-compose.yml` | intern | `backend_net` | `/mnt/user/appdata/dawarich/redis`, `dawarich_redis_password.txt` | Teil von Dawarich-Restore, aber aus DB/Appdaten rekonstruierbar | nein | Redis 8.8 Alpine, keine Host-Ports | | `mail-archiver` | Mail-Archivierung | `apps/mail-archiver/docker-compose.yml` | `https://mail.kaleschke.info` | PostgreSQL 18, Internet/IMAP, Traefik, Authelia | `/mnt/user/appdata/mailarchiver/data-protection-keys` | Tier 2, `postgresql17-mailarchiver.dump` | ja + Authelia | Hybrid-Dienst: `frontend_net` fuer Internet, `backend_net` fuer DB; App-eigene Auth bleibt zusaetzliche Schutzschicht; Dump-Dateiname behaelt den historischen Cluster-Namen | | `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten | @@ -60,7 +60,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und | Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs | |---|---|---|---|---|---|---|---|---| | `glance` | einziges Homelab-Uebersichts-/Status-Dashboard | `ops/glance/docker-compose.yml`, `ops/glance/config/glance.yml` | `https://glance.kaleschke.info` | Traefik + Authelia, interne HTTP-Checks, Immich API, AdGuard API, Speedtest API, interner Docker-Socket-Proxy | Repo-Konfiguration; keine kritische Persistenz | Tier 3, rebuildbar | ja + Authelia | Zeigt aktive Dienste, Immich-Community-Widget, Internet-/DNS-/VPN-Monitore, AdGuard-DNS-Stats, Speedtest-Livewerte, Docker-Containergruppen, Server-Stats, Zeitfortschritt, Bookmarks und eine Infrastruktur-Seite; Docker-API nur ueber `glance-docker-socket-proxy` auf internem Netz | -| `komodo-core` | GitOps UI/API/Stack-Manager | `ops/komodo/docker-compose.yml` | `https://komodo.kaleschke.info` | Mongo, Gitea, Traefik | `/mnt/user/appdata/komodo/core`, `komodo_keys` | Tier 1 | ja, native Auth | keine pauschale Authelia-ForwardAuth; Gitea DNS override | +| `komodo-core` | GitOps UI/API/Stack-Manager | `ops/komodo/docker-compose.yml` | `https://komodo.kaleschke.info` | Mongo, Gitea, Traefik | `/mnt/user/appdata/komodo/core`, `komodo_keys` | Tier 1 | ja, native Auth | keine pauschale Authelia-ForwardAuth; Gitea DNS override; **Self-Stack ist inline in Komodo verwaltet** (`repo=""`, `webhook_enabled=false`, kein GitOps-Push), `ops/komodo/docker-compose.yml` ist nur Spiegel/Recovery-Anker - Repo-Aenderungen wirken nicht automatisch live (DECISIONS 2026-05-04 / 2026-06-23); IP-Allowlist statt public | | `komodo-mongo` | Komodo Datenbank | `ops/komodo/docker-compose.yml` | intern | `komodo_net` | `/mnt/user/appdata/komodo/mongo`, `komodo_mongo_password.txt` | Tier 1, `komodo-mongo.archive.gz` | nein | Dump am 2026-05-04 bestaetigt; nach Major-Upgrades pruefen | | `komodo-periphery` | Komodo Host-Agent | `ops/komodo/docker-compose.yml` | intern Core -> Periphery | Docker socket, `/mnt/user/services`, `frontend_net`, `komodo_net` | `/mnt/user/appdata/komodo/periphery`, `komodo_keys` | Tier 1 | nein | Docker-Socket-Ausnahme; `/mnt/user/services` Mount fuer Stack-Workspaces | | `borg-ui` | Borg Backup-/Restore UI | `ops/borg-ui/docker-compose.yml` | `https://borg.kaleschke.info` | Traefik + Authelia, Borg repo credentials | `/mnt/user/appdata/borg-ui/data`, `/mnt/user/backups/borg/dumps`, Restore-Ziel | Tier 3 / Backup kritisch, `borg-ui.sqlite` | ja + Authelia | breite Mounts bewusst; `/local/secrets` im DR-Scope; Nextcloud-Daten werden read-only nach `/local/nextcloud/data` eingebunden | @@ -82,7 +82,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und | `hermes-gateway` | Hermes Agent Gateway/API intern | `ops/hermes-agent/docker-compose.yml` | intern `8642` auf `hermes_net` | SSH Runner (VM 192.168.178.143), LLM Provider, optional Home Assistant | `/mnt/user/appdata/hermes-agent/data`, SSH key path | Tier 3, Borg/Share | nein | NAS-Stack bleibt deaktiviert, solange die separate Hermes-VM/Runner-Seite nicht wiederhergestellt ist; kein Docker-Socket | | `hermes-dashboard` | Hermes Dashboard | `ops/hermes-agent/docker-compose.yml` | `https://hermes.kaleschke.info` via `${HERMES_DASHBOARD_HOST}` | `hermes-gateway`, Traefik + Authelia | shared read-only data mount | Tier 3, Borg/Share | ja + Authelia | Compose-Profil `dashboard`; aktuell VM-seitig offen, nicht Teil des NAS-Finalstarts | | `n8n` | Workflow-Automation; aktuell genutzt fuer Mail->LLM->Gitea-Issue (Inbox `Micha/mails`) | `apps/n8n/docker-compose.yml`, `apps/n8n/workflows/*.json` | `https://n8n.kaleschke.info` | Traefik (ohne pauschale Authelia, analog Komodo/Nextcloud), GMX IMAP, OpenAI API, Gitea API | `/mnt/user/appdata/n8n/data` (SQLite, Credentials, Workflows) | Tier 2, Borg + `n8n-data` (Credentials sind nur mit `N8N_ENCRYPTION_KEY` entschluesselbar) | ja, native Auth | Wegen Webhook-Endpunkten (`/webhook/*`) bewusst ohne `authelia@file`; eigene Login-/Owner-Auth bleibt Pflicht; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret, Verlust macht Credentials unbrauchbar. | -| `healthchecks` | Self-hosted Cron-/Job-Heartbeat-Monitor (Dead-Man's-Switch) fuer interne Jobs/Scripte | `ops/healthchecks/docker-compose.yml`, `ops/healthchecks/README.md` | `https://hc.kaleschke.info` | Traefik (native Auth, ohne pauschale Authelia), `healthchecks-postgres`, ntfy | keine kritische App-Persistenz (Check-Metadaten in der DB) | Tier 3, rebuildbar | ja, native Auth | Hub fuer INTERNE Checks; die externen Host-down-/Backup-Waechter (Borg-Pre-Hook, Nearline-Pull, Monitoring-Watchdog #8) bleiben bewusst auf healthchecks.io-Cloud (ein On-Host-Waechter kann Host-Down nicht melden). Ping-/API-Endpunkte ohne ForwardAuth (analog n8n). Stack-ENV: `HEALTHCHECKS_SECRET_KEY`, `HEALTHCHECKS_DB_PASSWORD`, `HEALTHCHECKS_SUPERUSER_EMAIL/PASSWORD`. **Live seit 2026-06-23** (Komodo-Stack-ID `6a3acf2ca7867a4fbab9bfc1`, deployt via API; Superuser angelegt). Offen: Gitea->Komodo-Webhook noch manuell anzulegen | +| `healthchecks` | Self-hosted Cron-/Job-Heartbeat-Monitor (Dead-Man's-Switch) fuer interne Jobs/Scripte | `ops/healthchecks/docker-compose.yml`, `ops/healthchecks/README.md` | `https://hc.kaleschke.info` | Traefik (native Auth, ohne pauschale Authelia), `healthchecks-postgres`, ntfy | keine kritische App-Persistenz (Check-Metadaten in der DB) | Tier 3, rebuildbar | ja, native Auth | Hub fuer INTERNE Checks; die externen Host-down-/Backup-Waechter (Borg-Pre-Hook, Nearline-Pull, Monitoring-Watchdog #8) bleiben bewusst auf healthchecks.io-Cloud (ein On-Host-Waechter kann Host-Down nicht melden). Ping-/API-Endpunkte ohne ForwardAuth (analog n8n). Stack-ENV: `HEALTHCHECKS_SECRET_KEY`, `HEALTHCHECKS_DB_PASSWORD`, `HEALTHCHECKS_SUPERUSER_EMAIL/PASSWORD`. **Live seit 2026-06-23** (Komodo-Stack-ID `6a3acf2ca7867a4fbab9bfc1`, deployt via API; Superuser angelegt). Gitea->Komodo-Webhook seit 2026-06-23 aktiv (Status: `docs/MASTER_TODO.md`) | | `healthchecks-postgres` | Healthchecks-Datenbank | `ops/healthchecks/docker-compose.yml` | intern | `healthchecks_internal` | `/mnt/user/appdata/healthchecks/postgres18`, `healthchecks_postgres_password.txt` | Check-Metadaten, rebuildbar | nein | interne DB; PostgreSQL 18; nie `frontend_net` | ## Smart Home @@ -96,7 +96,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und | Service | Zweck | Autoritativer Pfad | URL / Zugang | Abhaengigkeiten | Datenpfade | Backup / Restore | Traefik | Besonderheiten / TODOs | |---|---|---|---|---|---|---|---|---| -| `posture-check` | Host-Posture-Audit fuer Filesystem, Mover-Drift, NVMe-SMART, Fuellstand und Authelia-Repo<->Host-Drift | `services/posture-check/posture-check.sh` | Unraid User-Script / Cron / Borg Pre-Hook | `findmnt`, `df`, `nvme`, optional `curl` fuer ntfy; ruft `services/authelia-diff.sh` fuer `authelia_config_drift` auf | `/mnt/user/services/posture-check/last.json` | Repo-Skript + letzter JSON-Status | nein | Muss auf dem Unraid-Host bei Boot, stuendlich und vor Borg laufen; Disk1-NTFS ist nach Disk1 Phase 2 nicht mehr erlaubt (`ALLOW_DISK1_NTFS=0` Standard); Warning/Critical alarmieren via ntfy nur bei neuer Ursache oder nach `ALERT_REPEAT_SECONDS`. Authelia-Drift-Check braucht einen Repo-Spiegel unter `/mnt/user/services/homelab-infra/` (siehe `docs/WORKFLOW.md` Sektion "Ausnahme: Authelia configuration.yml") | +| `posture-check` | Host-Posture-Audit fuer Filesystem, Mover-Drift, NVMe-SMART, Fuellstand, Authelia-Repo<->Host-Drift und Traefik-dynamic-Repo<->Host-Drift | `services/posture-check/posture-check.sh` | Unraid User-Script / Cron / Borg Pre-Hook | `findmnt`, `df`, `nvme`, `rsync`, optional `curl` fuer ntfy; ruft `services/authelia-diff.sh` fuer `authelia_config_drift` und `services/traefik-dynamic-diff.sh` fuer `traefik_dynamic_drift` auf | `/mnt/user/services/posture-check/last.json` | Repo-Skript + letzter JSON-Status | nein | Muss auf dem Unraid-Host bei Boot, stuendlich und vor Borg laufen; Disk1-NTFS ist nach Disk1 Phase 2 nicht mehr erlaubt (`ALLOW_DISK1_NTFS=0` Standard); Warning/Critical alarmieren via ntfy nur bei neuer Ursache oder nach `ALERT_REPEAT_SECONDS`. Authelia- und Traefik-dynamic-Drift-Checks brauchen einen Repo-Spiegel unter `/mnt/user/services/homelab-infra/` | | `docker-critical-events` | Live-Alarmierung fuer Docker `die`/`oom`/`kill` Events | `services/posture-check/docker-critical-events.sh`, Supervisor `services/posture-check/docker-critical-events-supervisor.sh` | Unraid User-Script / Hintergrundprozess | Docker CLI, ntfy | `/mnt/user/services/posture-check/docker-critical-events-last.log`, PID/Outfile unter `/mnt/user/services/posture-check/` | Repo-Skript + letzter Event-Log | nein | Optional als Unraid User-Script `at array start` starten; Supervisor kann `start`, `stop`, `status`, `smoke`; sendet nach `homelab-alerts` | ## Backup- und Restore-Hinweise diff --git a/docs/WORKFLOW.md b/docs/WORKFLOW.md index 99da56b..70e1cae 100644 --- a/docs/WORKFLOW.md +++ b/docs/WORKFLOW.md @@ -262,6 +262,7 @@ Diese Ausnahme bleibt bewusst bestehen. Der File-Provider wird weiterhin nur fue | `traefik/dynamic/middlewares.yml` | `/mnt/user/appdata/traefik/dynamic/middlewares.yml` | | `traefik/dynamic/tls.yml` | `/mnt/user/appdata/traefik/dynamic/tls.yml` | | `traefik/dynamic/dashboards.yml` | `/mnt/user/appdata/traefik/dynamic/dashboards.yml` | +| `traefik/dynamic/plex.yml` | `/mnt/user/appdata/traefik/dynamic/plex.yml` | ### Pflicht-Workflow bei Aenderungen an Traefik Dynamic Config @@ -269,10 +270,15 @@ Diese Ausnahme bleibt bewusst bestehen. Der File-Provider wird weiterhin nur fue 2. Commit + Push 3. Datei manuell auf den Host kopieren 4. Traefik laedt dynamic config automatisch neu -5. Aenderung testen +5. `services/traefik-dynamic-diff.sh` muss `exit 0` liefern +6. Aenderung testen > **Merksatz:** Git-Commit allein reicht hier nicht. Ohne den manuellen Kopier-Schritt wirkt die Aenderung nicht. +Der Posture-Check (`services/posture-check/posture-check.sh`) ruft +`services/traefik-dynamic-diff.sh` als Check `traefik_dynamic_drift` auf und +meldet Repo-zu-Host-Drift als Warning via ntfy. + --- ## Ausnahme: Authelia configuration.yml diff --git a/ops/borg-ui/BACKUP_SCOPE.md b/ops/borg-ui/BACKUP_SCOPE.md index 77cb2b5..eef5326 100644 --- a/ops/borg-ui/BACKUP_SCOPE.md +++ b/ops/borg-ui/BACKUP_SCOPE.md @@ -79,6 +79,23 @@ Filebrowser serviert `/mnt/user/projekte`, `/mnt/user/documents` und `/mnt/user/ - **`/mnt/user/projekte`** ist aktuell in **keinem** Borg-Scope. Ad-hoc-Dateien, die direkt unter `documents/` oder `photos/` (ausserhalb der genannten App-Ordner) abgelegt werden, ebenfalls nicht. - Entscheidung Operator offen (Eintrag in `docs/MASTER_TODO.md`): Entweder `projekte` als eigenen read-only Borg-UI-Mount + Quelllisten-Eintrag aufnehmen, oder bewusst als "nur lokal, nicht DR-relevant" bestaetigen. Bis zur Entscheidung gilt: dort liegende Originaldaten sind **nicht** wiederherstellbar. +### Dawarich (Standortdaten) - noch nicht im Scope + +Dawarich (`apps/dawarich/docker-compose.yml`) speichert hochsensible +Standort-Historie in einer eigenen PostGIS-DB unter +`/mnt/user/appdata/dawarich/{postgres17,redis,shared,public,watched,storage}`. +`docs/SERVICE_CATALOG.md` nennt als Ziel "Tier 2, Borg + `dawarich.dump`", aber: + +- Es gibt **keinen** `dawarich`-Eintrag in der Service Inventory oben. +- `pre-backup-dumps.sh` erzeugt **kein** `dawarich.dump` (PostGIS-`pg_dump`). +- Die Quellliste (`all-important-sources.txt`) enthaelt **keinen** + `/local/appdata/dawarich`-Pfad, und Borg-UI mountet ihn nicht. + +Konsequenz: Standortdaten sind aktuell **nicht** DR-gesichert. Schliessen +(Operator + Host): `dawarich.dump` in `pre-backup-dumps.sh` (PostGIS), Pfad in +Quellliste + Borg-UI-Mount + diese Tabelle aufnehmen, danach `RESTORE_MATRIX.md` +Tier-2-Zeile + Reifegrad ergaenzen. Tracking: `docs/MASTER_TODO.md`. + ### Komodo keys Production still stores Komodo Core/Periphery keys in the Docker named volume `komodo_komodo_keys`. This is a known open migration item and is not fixed by the Borg source list alone. Target state: move the keys to a host path such as `/mnt/user/appdata/komodo/keys` and mount that path into both Komodo containers, then include it in Borg. Do not treat this as solved until the live Compose stack has been migrated and Periphery reconnect has been verified. diff --git a/ops/healthchecks/README.md b/ops/healthchecks/README.md index 7178629..8147c8d 100644 --- a/ops/healthchecks/README.md +++ b/ops/healthchecks/README.md @@ -1,4 +1,4 @@ -Typ: Runbook · Stand: 2026-06-23 · Status: live (Komodo-Stack-ID `6a3acf2ca7867a4fbab9bfc1`); offen nur der Gitea->Komodo-Webhook +Typ: Runbook · Stand: 2026-06-23 · Status: live (Komodo-Stack-ID `6a3acf2ca7867a4fbab9bfc1`); Gitea->Komodo-Webhook seit 2026-06-23 aktiv (Status: `docs/MASTER_TODO.md`) # Healthchecks (self-hosted) — Cron-/Job-Heartbeat-Monitor @@ -72,6 +72,9 @@ Datei-Secret vor; beide Werte muessen identisch sein. ## Deploy + Pflicht-Webhook +> Status 2026-06-23: Deploy erfolgt + Gitea->Komodo-Webhook aktiv. Diese Schritte +> bleiben als Referenz / Rebuild-Anleitung. Laufender Status: `docs/MASTER_TODO.md`. + 1. Stack in Komodo aus Gitea `Micha/homelab-infra` anlegen, `webhook_enabled` an. 2. Gitea-Webhook auf die neue Stack-ID anlegen (`http://komodo-core:9120/listener/github/stack//deploy`), diff --git a/services/posture-check/posture-check.sh b/services/posture-check/posture-check.sh index b0abb4e..6435421 100755 --- a/services/posture-check/posture-check.sh +++ b/services/posture-check/posture-check.sh @@ -11,7 +11,9 @@ ALLOW_DISK1_NTFS="${ALLOW_DISK1_NTFS:-0}" ALERT_STATE_PATH="${ALERT_STATE_PATH:-/mnt/user/services/posture-check/last-alert.state}" ALERT_REPEAT_SECONDS="${ALERT_REPEAT_SECONDS:-86400}" SKIP_AUTHELIA_DRIFT="${SKIP_AUTHELIA_DRIFT:-0}" +SKIP_TRAEFIK_DYNAMIC_DRIFT="${SKIP_TRAEFIK_DYNAMIC_DRIFT:-0}" AUTHELIA_DIFF_SCRIPT="${AUTHELIA_DIFF_SCRIPT:-/mnt/user/services/homelab-infra/services/authelia-diff.sh}" +TRAEFIK_DYNAMIC_DIFF_SCRIPT="${TRAEFIK_DYNAMIC_DIFF_SCRIPT:-/mnt/user/services/homelab-infra/services/traefik-dynamic-diff.sh}" mkdir -p "$TMP_DIR" RESULTS_FILE="$TMP_DIR/results.$$" @@ -232,10 +234,12 @@ check_authelia_config_drift() { return fi - local output + local output="" local rc + set +e output="$(bash "$AUTHELIA_DIFF_SCRIPT" 2>&1)" rc=$? + set -e case "$rc" in 0) @@ -256,6 +260,43 @@ check_authelia_config_drift() { esac } +check_traefik_dynamic_drift() { + if [ "$SKIP_TRAEFIK_DYNAMIC_DRIFT" = "1" ]; then + add_result "ok" "traefik_dynamic_drift" "Traefik dynamic drift check skipped via SKIP_TRAEFIK_DYNAMIC_DRIFT=1" + return + fi + + if [ ! -x "$TRAEFIK_DYNAMIC_DIFF_SCRIPT" ] && [ ! -f "$TRAEFIK_DYNAMIC_DIFF_SCRIPT" ]; then + add_result "warning" "traefik_dynamic_drift" "Traefik dynamic diff script missing: $TRAEFIK_DYNAMIC_DIFF_SCRIPT" + return + fi + + local output="" + local rc + set +e + output="$(bash "$TRAEFIK_DYNAMIC_DIFF_SCRIPT" 2>&1)" + rc=$? + set -e + + case "$rc" in + 0) + add_result "ok" "traefik_dynamic_drift" "Traefik dynamic repo baseline matches host directory" + ;; + 1) + add_result "warning" "traefik_dynamic_drift" "Traefik dynamic repo<->host drift; run traefik-dynamic-diff.sh for details" + ;; + 2) + add_result "warning" "traefik_dynamic_drift" "Traefik dynamic diff aborted: $output" + ;; + 4) + add_result "warning" "traefik_dynamic_drift" "Traefik dynamic diff missing tool: $output" + ;; + *) + add_result "warning" "traefik_dynamic_drift" "Traefik dynamic diff returned unexpected rc=$rc: $output" + ;; + esac +} + send_ntfy() { local severity="$1" local topic="$2" @@ -426,6 +467,7 @@ main() { check_nvme_smart check_authelia_config_drift + check_traefik_dynamic_drift write_json } diff --git a/services/traefik-dynamic-diff.sh b/services/traefik-dynamic-diff.sh new file mode 100644 index 0000000..738fb83 --- /dev/null +++ b/services/traefik-dynamic-diff.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Vergleicht die Traefik dynamic File-Provider-Dateien aus dem Repo-Spiegel +# gegen die produktive Host-Dateiablage. Der Check ist read-only: +# rsync laeuft mit --dry-run und schreibt nichts. +# +# Aufruf-Defaults siehe Variablen unten. Aufruf typischerweise: +# bash services/traefik-dynamic-diff.sh +# +# Exit-Codes: +# 0 Repo und Host sind identisch +# 1 Drift festgestellt (rsync itemized output auf stdout) +# 2 Repo- oder Host-Verzeichnis fehlt +# 4 internes Werkzeug fehlt (rsync) + +set -uo pipefail + +TRAEFIK_DYNAMIC_REPO_DIR="${TRAEFIK_DYNAMIC_REPO_DIR:-/mnt/user/services/homelab-infra/traefik/dynamic}" +TRAEFIK_DYNAMIC_HOST_DIR="${TRAEFIK_DYNAMIC_HOST_DIR:-/mnt/user/appdata/traefik/dynamic}" + +if ! command -v rsync >/dev/null 2>&1; then + echo "traefik-dynamic-diff: missing required command 'rsync'" >&2 + exit 4 +fi + +if [ ! -d "$TRAEFIK_DYNAMIC_REPO_DIR" ]; then + echo "traefik-dynamic-diff: repo directory not found: $TRAEFIK_DYNAMIC_REPO_DIR" >&2 + exit 2 +fi + +if [ ! -d "$TRAEFIK_DYNAMIC_HOST_DIR" ]; then + echo "traefik-dynamic-diff: host directory not found: $TRAEFIK_DYNAMIC_HOST_DIR" >&2 + exit 2 +fi + +diff_output="$(rsync -rni --checksum --delete \ + "$TRAEFIK_DYNAMIC_REPO_DIR"/ \ + "$TRAEFIK_DYNAMIC_HOST_DIR"/)" + +if [ -n "$diff_output" ]; then + printf '%s\n' "$diff_output" + exit 1 +fi + +exit 0