Files
homelab-infra/docs/homelab-optimierung.md
T
2026-06-10 20:40:05 +02:00

20 KiB
Raw Blame History

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)

# 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?