1 Commits

Author SHA1 Message Date
renovate 3ac9b8b475 chore(deps): update minor-and-patch-updates 2026-06-06 16:20:19 +00:00
26 changed files with 52 additions and 689 deletions
-11
View File
@@ -34,17 +34,6 @@ 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:4ea7ecc47ad1dd2c523b85c3967574b61e39def1b6fd26edf874e21733c4018c image: s1t5/mailarchiver@sha256:ea7fd8c2e3e0ef0941e8dd9e726e35a8de33296f5c7b9ed811df5168ae6a9714
container_name: mail-archiver container_name: mail-archiver
restart: unless-stopped restart: unless-stopped
environment: environment:
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
n8n: n8n:
image: docker.n8n.io/n8nio/n8n:2.26.2@sha256:61ba01bc5e39304bbc928c9dbecd938c3a5cc1331b68affba6a34d0f654c43d9 image: docker.n8n.io/n8nio/n8n:2.25.5@sha256:08862289f9e9b387d91eab66a74d40d307c0c9b74d2504866f8fe61e9063c838
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.5-apache@sha256:56bdc45109067500fd0832fa64832b7c77a167d9394cbf5f0f4b59740b94194d image: nextcloud:33.0.5-apache@sha256:96f8b6ad4adf4044ac6d3cbc10ef99b4897c90792782b5b60a5700e5b1b97b84
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:
unbound: unbound:
image: shaanmajid/unbound:1.25.1@sha256:f140db02a005904802bf5840093e95e675321aa060a00426fdffc2a3ac2eeb6b image: shaanmajid/unbound:1.25.1@sha256:96809ff052e8bd79bba30e067d8b27ed9a2f069b6b2a3484fe1d0eb45aba07c5
container_name: unbound container_name: unbound
restart: unless-stopped restart: unless-stopped
volumes: volumes:
+13 -13
View File
@@ -6,9 +6,8 @@ Ziel: App-uebergreifendes Single-Sign-On ueber Authelia als OpenID-Connect-Provi
(`https://auth.kaleschke.info`). Statt pro App eigener Logins meldet man sich einmal (`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. 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 > **Status:** Entwurf/Runbook. Noch **kein** Client produktiv ausgerollt. Reihenfolge
> und per Login-Smoke verifiziert. Der weitere Rollout bleibt additiv: lokale > bewusst klein: erst ein risikoarmer Proof (Grafana), dann Familien-Apps.
> App-Logins bleiben als Fallback aktiv.
--- ---
@@ -82,19 +81,20 @@ docker exec authelia authelia crypto hash generate pbkdf2 \
| Stufe | App | Domain | OIDC-Support | Policy | Risiko | Begruendung | | 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 | | **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. | | 2 | Immich | `immich.kaleschke.info` | nativ | s. u. (Familie) | mittel | Familien-Fotos, viele Nutzer; nach erfolgreichem Proof |
| 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. | | 3 | Nextcloud | `cloud.kaleschke.info` | App `user_oidc` | s. u. | mittel | klassischer OIDC-Login parallel zu lokalem Admin |
| **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) | | **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 | | 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. **Nicht OIDC:** Vaultwarden hat kein Standard-Endnutzer-OIDC (SSO ist Enterprise/Bitwarden-Feature) -> bleibt eigener Login. ntfy bleibt wie gehabt.
### Policy Familien-Apps ### Policy-Entscheidung Familien-Apps
- Admin-Apps (Grafana, Paperless): `authorization_policy: two_factor`. - Admin-Apps (Grafana, Paperless): `authorization_policy: two_factor`.
- Familien-Apps (Immich, Nextcloud, Mealie): Start mit `one_factor` und lokalen - Familien-Apps (Immich, Nextcloud, Mealie): **offene Operator-Entscheidung** ob
App-Logins als Fallback. 2FA fuer Familie erst spaeter, sobald TOTP-Enrollment `one_factor` (nur Authelia-Passwort, bequemer fuer Familie) oder `two_factor`.
pro Person eingerichtet ist; sonst entsteht unnoetiges Lockout-Risiko. Empfehlung: mit `one_factor` starten, 2FA fuer Familie spaeter, sobald TOTP-Enrollment
pro Person eingerichtet ist (sonst Lockout fuer Familienmitglieder).
--- ---
@@ -177,10 +177,10 @@ GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true
- **Secret-Mechanik je App verschieden:** Grafana `__FILE` (Docker-Secret), - **Secret-Mechanik je App verschieden:** Grafana `__FILE` (Docker-Secret),
Mealie Stack-ENV `${...}`. Hash immer in der Authelia-Host-Config, Klartext nie ins Repo. Mealie Stack-ENV `${...}`. Hash immer in der Authelia-Host-Config, Klartext nie ins Repo.
## Spaetere Feinschliffe vor breitem Rollout ## Offene Operator-Entscheidungen vor breitem Rollout
1. Gruppen/Rollen-Mapping: braucht es Authelia-Gruppen (z. B. `admins`, `family`) fuer 1. Familien-Apps `one_factor` vs `two_factor` (Lockout-Risiko fuer Familie ohne TOTP).
2. Gruppen/Rollen-Mapping: braucht es Authelia-Gruppen (z. B. `admins`, `family`) fuer
App-Rollen (Grafana Admin/Viewer, Nextcloud-Gruppen)? Wenn ja, in der Authelia App-Rollen (Grafana Admin/Viewer, Nextcloud-Gruppen)? Wenn ja, in der Authelia
User-Datenbank Gruppen pflegen. User-Datenbank Gruppen pflegen.
2. Familien-2FA spaeter neu bewerten, nachdem echte Familien-Accounts in Authelia 3. Reihenfolge nach dem Grafana-Proof bestaetigen.
angelegt und TOTP pro Person verstanden ist.
+2 -2
View File
@@ -26,7 +26,7 @@ Host-/Entscheidungsaufgaben beim **Operator**.
| 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` |
| 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 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" |
| 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` | | Authelia OIDC fuer Apps | Operator/Claude | **Live + verifiziert 2026-06-06: Grafana (admin) + Mealie (family, `one_factor`, additiv).** Muster + Gotchas (`extra_hosts`, additiv, E-Mail-Linking) in `docs/AUTHELIA_OIDC_PLAN.md`. **Verbleibt:** Paperless (env/allauth) -, Immich (Admin-UI/Config-File) -, Nextcloud (`user_oidc`-App/occ) -. Familien-Accounts in Authelia (nur `micha` existiert) folgen beim Onboarding | `docs/AUTHELIA_OIDC_PLAN.md`, `security/authelia/configuration.yml` |
--- ---
@@ -79,7 +79,7 @@ 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 von diffusen TBDs auf bewusste Entscheidungen mit Review-Triggern gehoben. - `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/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.
+2 -123
View File
@@ -193,8 +193,8 @@ ist die vollstaendige Wahrheit.
- Familien-Dienste/Ports konkretisieren — erst wenn ein reales Familiengeraet dazukommt. - Familien-Dienste/Ports konkretisieren — erst wenn ein reales Familiengeraet dazukommt.
- **Zwei-Tailscale-Konsolidierung: ERLEDIGT 2026-06-06** — redundanter Docker-Stack - **Zwei-Tailscale-Konsolidierung: ERLEDIGT 2026-06-06** — redundanter Docker-Stack
abgebaut, nur noch die native Plugin-Instanz `kallilabcore` (Subnet-Router) aktiv. abgebaut, nur noch die native Plugin-Instanz `kallilabcore` (Subnet-Router) aktiv.
- **Tailnet-Konsole aufraeumen: ERLEDIGT 2026-06-06** — Node-Eintraege `kallilab-core` - Tailnet-Konsole aufraeumen: Node-Eintraege `kallilab-core` (jetzt down) und
und alter Offline-`baerchen` aus der Admin-Konsole entfernt. alter Offline-`baerchen` entfernen (trivial, nur tote Geraeteeintraege).
- State-Pfad `/mnt/user/appdata/tailscale` (vom entfernten Docker-Stack) bei - State-Pfad `/mnt/user/appdata/tailscale` (vom entfernten Docker-Stack) bei
Gelegenheit nach `_archive/tailscale-removed-2026-06-06/` (kein Sofort-Loeschen). Gelegenheit nach `_archive/tailscale-removed-2026-06-06/` (kein Sofort-Loeschen).
- Optionaler Off-LAN-Routentest: von einem Operator-Geraet im Mobilfunk - Optionaler Off-LAN-Routentest: von einem Operator-Geraet im Mobilfunk
@@ -288,126 +288,6 @@ docker network inspect frontend_net | jq '.[0].Containers | keys'
docker network inspect backend_net | jq '.[0].Internal' docker network inspect backend_net | jq '.[0].Internal'
``` ```
## 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 |
@@ -420,4 +300,3 @@ Abschnitt „SSH-Konfiguration Host".
| 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". |
+1 -9
View File
@@ -93,15 +93,7 @@ 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`, `ops/komodo` | Renovate scant alte/abgeloeste Stacks nicht; `ops/komodo` ist bewusst raus (siehe unten) | | Ignore-Pfade | `_archive`, `ops/grafana-influxdb`, `ops/loki` | Renovate scant alte/abgeloeste Stacks nicht |
## 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,7 +35,6 @@ 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
-228
View File
@@ -1,228 +0,0 @@
# 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
@@ -1,58 +0,0 @@
# 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>
+2 -7
View File
@@ -66,18 +66,15 @@ 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:
- 172.23.0.3 - 1.1.1.1
- 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:
@@ -370,8 +367,6 @@ networks:
driver: bridge driver: bridge
frontend_net: frontend_net:
external: true external: true
dns_net:
external: true
volumes: volumes:
prometheus_data: prometheus_data:
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
borg-ui: borg-ui:
image: ainullcode/borg-ui@sha256:0922157e8f77a1b2bd23cd09366a458ea6de07fd9306aa1485f9cfe623eca17f image: ainullcode/borg-ui@sha256:acb0fbe83dc4a3843abc06f814c5f1061a0701b2cfc574da2e851d17a34ab745
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.123.0@sha256:cb261a7f87674b445e0fd66d87d55900c1b823d276c727ab0d168a75e69e9992 image: lscr.io/linuxserver/code-server:4.123.0@sha256:9dd4555720db04eb92d92cc84e7a34f0862bada5679889446a3004c45b5fa59b
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.14@sha256:1ec9b0c68297550c92f4a93feed432850c2993b261706cc3cc2e808f94a95e76 image: filebrowser/filebrowser:v2.63.13@sha256:e79c381fdbf549a48adc6268c74b920b70cab53663995a2e8142964eedea10c7
container_name: filebrowser container_name: filebrowser
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
-5
View File
@@ -2,11 +2,6 @@ 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
+1 -1
View File
@@ -1,6 +1,6 @@
services: services:
scrutiny: scrutiny:
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:228483f16a6236d2fa9b2fbfca2e76dc861e648fbc6ae6e680d23e5d00211a5d image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:addb5e19071c5fd4725de5f12eca6243039d3e1227f021432d73863fc8c7d83c
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:c3750c40948a9360000ce62d694da92e85584b4ab6d3d9a9d1432d76fa5e0726 image: lscr.io/linuxserver/speedtest-tracker:1.14.3@sha256:79c00631575dec6d91c10ed904c211224f00813013a305c2284324e195a538bb
container_name: speedtest-tracker container_name: speedtest-tracker
restart: unless-stopped restart: unless-stopped
security_opt: security_opt:
+1 -15
View File
@@ -38,19 +38,6 @@
"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": [
@@ -112,7 +99,6 @@
"ignorePaths": [ "ignorePaths": [
"**/_archive/**", "**/_archive/**",
"ops/grafana-influxdb/**", "ops/grafana-influxdb/**",
"ops/loki/**", "ops/loki/**"
"ops/komodo/**"
] ]
} }
+18 -125
View File
@@ -11,7 +11,6 @@ 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}"
@@ -29,7 +28,6 @@ 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}"
@@ -461,10 +459,6 @@ 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 "-"
@@ -480,11 +474,7 @@ 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])
key = frozenset([domain, *sans]) print(f"{days}\t{not_after.date().isoformat()}\t{names}")
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
@@ -583,36 +573,13 @@ 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 note recheck local image_id created_iso created_epoch age_days image_tag
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)"
@@ -621,46 +588,33 @@ 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 ))
note="" printf '%d\t%s\t%s\n' "$age_days" "$name" "$image_tag" >> "$image_file"
if [ "$age_days" -ge "$IMAGE_AGE_WARN_DAYS" ]; then if [ "$age_days" -ge "$IMAGE_AGE_WARN_DAYS" ]; then
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)) 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 ueberaltertem Image (gewarnt): $image_warnings" append "- Container mit Image >= $IMAGE_AGE_WARN_DAYS Tage: $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 | Hinweis |" append "| Alter Tage | Container | Image |"
append "|---:|---|---|---|" append "|---:|---|---|"
sort -nr "$image_file" | head -n 10 | while IFS="$(printf '\t')" read -r age name img note; do sort -nr "$image_file" | head -n 10 | while IFS="$(printf '\t')" read -r age name img; do
append "| $age | $name | $img | ${note:-} |" append "| $age | $name | $img |"
done done
append "" append ""
if [ "$image_warnings" -eq 0 ] && [ "$image_allowed" -eq 0 ]; then if [ "$image_warnings" -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 ueberalterte Images (nicht in der Allowlist). Update-Pipeline und CVE-Status pruefen." append "Bewertung: $image_warnings Container nutzen Images aelter als $IMAGE_AGE_WARN_DAYS Tage. Update-Pipeline und CVE-Status pruefen."
fi fi
fi fi
append "" append ""
@@ -701,31 +655,6 @@ 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"
@@ -881,35 +810,12 @@ collect_log_highlights() {
fi fi
fi fi
# Escalation-exempt patterns: known noise that is also permanently very loud # Threshold escalation: how many patterns produced more than the threshold?
# (e.g. Unraid mdadm parse spam). Without this, such a pattern would keep the local noise_threshold_exceeded=0
# 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 -F '\t' -v t="$NOISE_ESCALATION_THRESHOLD" ' noise_threshold_exceeded="$(awk -v t="$NOISE_ESCALATION_THRESHOLD" '$1 > t { n++ } END { print n + 0 }' "$noise_by_pattern")"
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")"
@@ -930,9 +836,6 @@ 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
@@ -982,32 +885,22 @@ 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 | Hinweis |" append "| Pattern | Anzahl |"
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" note="" local short="$pat"
# 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 | $note |" append "| \`$short\` | $cnt |"
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 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." 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."
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,9 +4,7 @@ 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 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 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}"
# 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")"
@@ -1,30 +0,0 @@
# 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
+1 -16
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-06-10 # Last reviewed: 2026-05-21
# 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,18 +72,3 @@ 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
@@ -1,32 +0,0 @@
# 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:d6858791f9e74df44ca4014166647c41cdc2abd3bf2a71b832ca4e1c6a91b257 image: traefik:v3.7@sha256:fcdef599e6259359833dd2e1d49f9e964f66825d69bd3dd468f51102ce013d03
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
security_opt: security_opt: