Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
20 KiB
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:4nutztimmich-server:release@sha256:...(ebenso ML). Renovate aktualisiert Digests — beimrelease-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 versionoder 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-fullsowie 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-onlywurde am 2026-06-01 bewusst verworfen (forced-command brach Key-Auth). Damit kann jeder mit dem Borg-Key (Host, borg-ui-Container mit/local/secrets-Mount) Archive löschen — ein kompromittierter Host vernichtet auch das Off-site-Backup. - Warum relevant: Das ist die einzige verbliebene Lücke in einer sonst sehr guten Backup-Kette.
- Änderung: In der Hetzner-Robot-Konsole automatische Snapshots der Storage Box aktivieren (z. B. täglich, 7–14 Tage Retention). Snapshots sind host-seitig nicht löschbar und im Storage-Box-Preis enthalten.
- Verifikation: Robot-Konsole zeigt Snapshot-Liste; nach 2 Tagen: zwei Snapshots vorhanden. Restore-Probe: einzelne Datei aus Snapshot über das Snapshot-Verzeichnis lesen.
- Rollback: Snapshots deaktivieren — rein additiv, keine Auswirkung auf Borg.
- Nebenwirkungen: Snapshots zählen ggf. anteilig aufs Quota (aktuell 65 GB / 1 TB — viel Luft). | Nutzen: sehr hoch | Risiko: niedrig | Aufwand: klein (<30 min) | sofort
3. DNS-Fallback gegen den AdGuard-SPOF
- Beobachtung: AdGuard ist einziger LAN-Resolver. Der dokumentierte Vorfall (Bulk-Deploy: AdGuard-Recreate → Host ohne DNS → Komodo-Pulls scheitern) ist genau dieses Muster; das Runbook behandelt nur das Symptom.
- Warum relevant: Jeder AdGuard-Ausfall (Update, OOM, Disk) nimmt LAN + Host-DNS gleichzeitig mit — auch die Reparaturfähigkeit (Image-Pulls!) hängt daran.
- Änderung (gestuft):
- a) Host-Ebene: zweiten Nameserver (z. B.
1.1.1.1) in der Unraid-Netzwerkkonfig als Fallback hinter192.168.178.58eintragen. 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.
- a) Host-Ebene: zweiten Nameserver (z. B.
- Verifikation:
docker stop adguardim Wartungsfenster →nslookup gitea.comauf dem Host funktioniert weiterhin; danachdocker 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.combzw.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-Dashboardcontainer_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 neuenoom-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:truestandardmäß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/perrsync --checksum --dry-rungegen/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 inmonitoring/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-prometheusim Wartungsfenster → externe Benachrichtigung nach ~10 min; danach Start. - Rollback: Rule + Route entfernen.
- Nebenwirkungen: neue (kleine) externe Abhängigkeit — in
docs/EXTERNAL_DEPENDENCIES.mdeintragen. | 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.mdwarnt — 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.mdcommitten. - Verifikation:
git statuszeigt 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.mdnennt "Redis-Caches aufredis:7.4-alpinevereinheitlicht" — real laufen alle aufredis:8.8.0-alpine. Ebenso "PostgreSQL 17"-Pfade/Servicenamen bei PG 18 (letzteres ist dokumentiert bewusst, ersteres nicht). - Warum relevant: Das Masterdokument ist laut eigener Regel die erste Lesepflicht für jeden (auch KI-)Eingriff; veraltete Fakten dort erzeugen falsche Entscheidungen.
- Änderung: Redis-Abschnitt in Sektion 13 auf 8.8 aktualisieren; bei Gelegenheit einen Mini-Check ins Posture-/Audit-Ritual: "stimmen Versionsangaben im Master noch?"
- Verifikation:
grep -n "7.4-alpine" HOMELAB_ARCHITECTURE_MASTER_V2.md→ leer. - Rollback: trivial (Doku). | Nutzen: niedrig–mittel | Risiko: keiner | Aufwand: klein | sofort
Top 5 Risiken (zuerst entschärfen)
- Löschbare Off-site-Backups — Host-Kompromittierung oder ein falscher
borg deletevernichtet auch Hetzner. → Empfehlung 2 (Snapshots). Bis dahin ist das DR-Konzept gegen Ransomware unvollständig. - DNS-SPOF AdGuard — bereits einmal real eingetreten (Teil-Deploy 2026-06); betrifft auch die Selbstheilungsfähigkeit (Image-Pulls). → Empfehlung 3.
- Verdeckte Versionssprünge via
release/latest-Digest-Bumps — v. a. Immich (DB-Migrationen!). → Empfehlung 1. - 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.)
- 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
- Strom: Gibt es eine Messung des Host-Verbrauchs (W idle/last)? Ohne Zahl ist der Bereich Kosten/Strom nicht bewertbar. → Shelly/Messsteckdose?
- 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. - Renovate-Verhalten gewollt? Sollen Digest-Bumps auf
release/latestweiter automatisch als PRs kommen, oder ist die Pinning-Strategie aus Empfehlung 1 die gewünschte Linie für alle Stacks? - 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.
- FRITZ!Box-DNS-Fallback (3b): Filterlücke bei AdGuard-Down akzeptieren oder lieber nur den Host-Fallback (3a) umsetzen?