Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| af4b7015ee | |||
| d48d473942 | |||
| e80e5dd49f | |||
| 3c339474a7 | |||
| c79afdfab0 | |||
| 8172793c68 | |||
| 8e46440944 | |||
| dfe1dc1c99 | |||
| 4007da3302 | |||
| 9836ea3c4f | |||
| 803f84b3af | |||
| d05ca63545 | |||
| 9847baf327 | |||
| 8ec5bc55d9 | |||
| 9c844074e0 | |||
| c126b71852 | |||
| e89b88a513 | |||
| 8bb250220b | |||
| 2f64aee109 | |||
| ed55b88ec1 | |||
| ce747f687f | |||
| cf11b4d75b | |||
| 796901ec6b | |||
| de7b714b4d | |||
| 8045e22873 | |||
| 52f8c2adcb | |||
| 0ddae675a8 | |||
| 7ce8e948cd | |||
| 2a87220862 | |||
| f2d4cad566 | |||
| e7370e4820 | |||
| dc26eb313c | |||
| dc7cbfa6cd | |||
| cf9ca59eb1 | |||
| d2a9c3b8cb | |||
| 0177350e64 | |||
| 2f3a029098 | |||
| a4c79d9d81 | |||
| 18a90fbb4b | |||
| 30f076c85a | |||
| 6e65f81503 | |||
| 6123584a02 | |||
| c33e29016b | |||
| 2628a0c795 | |||
| c7eed6bdad | |||
| 6c61ad3860 | |||
| 2d1b541847 |
@@ -145,6 +145,7 @@ Diese Dienste sind über echte `*.kaleschke.info`-Domains erreichbar:
|
|||||||
- `gitea` (Web) — git.kaleschke.info
|
- `gitea` (Web) — git.kaleschke.info
|
||||||
- `immich_server` — immich.kaleschke.info
|
- `immich_server` — immich.kaleschke.info
|
||||||
- `nextcloud` — cloud.kaleschke.info
|
- `nextcloud` — cloud.kaleschke.info
|
||||||
|
- `plex` — plex.kaleschke.info (Traefik, native Plex-Auth; Plex Remote Access/Port 32400 bleibt aus)
|
||||||
|
|
||||||
### 4.2 Nicht öffentlich / nur Tailscale oder Traefik + Middleware
|
### 4.2 Nicht öffentlich / nur Tailscale oder Traefik + Middleware
|
||||||
Diese Dienste sind **keine Public Apps**:
|
Diese Dienste sind **keine Public Apps**:
|
||||||
@@ -273,7 +274,7 @@ Legende Status:
|
|||||||
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
| `immich_server` | ✅ | `immich_default`, `frontend_net` | Traefik | aktiv via `immich.kaleschke.info` | — |
|
||||||
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
|
| `immich_machine_learning` | ✅ | `immich_default` | intern | bleibt intern | — |
|
||||||
| `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels |
|
| `nextcloud` | ✅ | `frontend_net`, `nextcloud_internal` | Traefik | aktiv via `cloud.kaleschke.info`, nativer Nextcloud-Login, WebDAV/CardDAV faehig | CalDAV/CardDAV-Redirect via Traefik-Labels |
|
||||||
| `plex` | ✅ | `host` | Plex native, **LAN/Tailscale-only** (Remote Access aus seit 2026-05-28) | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme; Server geclaimt von `Xeridos`; Smart-TVs (Schlafzimmer, Wohnzimmer) ueber WLAN-LAN per mDNS | — |
|
| `plex` | ✅ | `host` | Traefik via `plex.kaleschke.info` + Plex native Auth; LAN direkt `:32400` | Compose-Stack unter `host-services/plex/`; Host-Netz bleibt fuer Discovery / Plex GDM dokumentierte Ausnahme; Traefik routet per File-Provider-Ausnahme auf `http://192.168.178.58:32400`, weil Docker-Labels Host-Netz-Container aus Traefik heraus auf `127.0.0.1` routen wuerden; kein direkter WAN-Port 32400 und Plex Remote Access bleibt aus; Server geclaimt von `Xeridos`; Smart-TVs (Schlafzimmer, Wohnzimmer) ueber WLAN-LAN per mDNS | — |
|
||||||
| `super-productivity` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | Persoenliche Task-PWA des Operators; Issues kommen aus Gitea `Micha/mails` via n8n-Mail-Workflow | Deploy + Webhook + DNS-Eintrag offen |
|
| `super-productivity` | ✅ vorbereitet | `frontend_net` | Traefik + Middleware | Persoenliche Task-PWA des Operators; Issues kommen aus Gitea `Micha/mails` via n8n-Mail-Workflow | Deploy + Webhook + DNS-Eintrag offen |
|
||||||
| `n8n` | ✅ vorbereitet | `frontend_net` | Traefik, native Auth (keine pauschale Authelia) | Workflow-Automation; erster Workflow: GMX-Mail -> OpenAI-Extraktion -> Gitea-Issue in `Micha/mails`; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret | Deploy + Webhook + Owner-Setup offen |
|
| `n8n` | ✅ vorbereitet | `frontend_net` | Traefik, native Auth (keine pauschale Authelia) | Workflow-Automation; erster Workflow: GMX-Mail -> OpenAI-Extraktion -> Gitea-Issue in `Micha/mails`; `N8N_ENCRYPTION_KEY` ist Stack-ENV-Pflichtsecret | Deploy + Webhook + Owner-Setup offen |
|
||||||
|
|
||||||
@@ -308,7 +309,7 @@ Legende Status:
|
|||||||
|
|
||||||
| Container | Status | Ziel |
|
| Container | Status | Ziel |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| — | — | Plex ist nicht mehr offen: der Dienst ist als Repo-Compose-Stack unter `host-services/plex/` dokumentiert; `host`-Netz bleibt als Discovery-Ausnahme. |
|
| — | — | Plex ist nicht mehr direkt offen: der Dienst ist als Repo-Compose-Stack unter `host-services/plex/` dokumentiert; `host`-Netz bleibt als Discovery-Ausnahme. Externer Zugriff laeuft ausschliesslich ueber Traefik/443 auf `plex.kaleschke.info`; keine direkte 32400-WAN-Freigabe. Technisch nutzt Plex als einzige Host-Netz-Route `traefik/dynamic/plex.yml`, weil Docker-Labels fuer `network_mode: host` in Traefik auf `127.0.0.1:32400` zeigen. |
|
||||||
|
|
||||||
### 7.8 Entfernte Container
|
### 7.8 Entfernte Container
|
||||||
|
|
||||||
@@ -407,6 +408,7 @@ Für den laufenden Betrieb gilt stattdessen:
|
|||||||
| `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP`; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume |
|
| `monitoring-influxdb3-core` | Host-Port 8181 auf LAN-IP; `user: "0"` | Home Assistant laeuft in einer VM ausserhalb des Compose-Netzes und muss Metriken schreiben koennen; keine Traefik-Route, kein `frontend_net`, Zugriff nur ueber Token und LAN-IP `INFLUXDB_BIND_IP`; InfluxDB 3 Core benoetigt im aktuellen Container-Setup Root-Rechte fuer den lokalen Object-Store-Pfad im named volume |
|
||||||
| `monitoring-promtail` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket |
|
| `monitoring-promtail` | Docker-Socket read-only | Docker-Log-Discovery fuer Loki; keine Schreibrechte, keine Appdaten-Persistenz ueber den Socket |
|
||||||
| `n8n` | keine pauschale Authelia-Middleware | Webhook-Endpunkte (`/webhook/*`, `/webhook-test/*`) muessen ohne ForwardAuth erreichbar bleiben; n8n bringt eigene Owner-/Login-Auth mit (analog Komodo/Nextcloud) |
|
| `n8n` | keine pauschale Authelia-Middleware | Webhook-Endpunkte (`/webhook/*`, `/webhook-test/*`) muessen ohne ForwardAuth erreichbar bleiben; n8n bringt eigene Owner-/Login-Auth mit (analog Komodo/Nextcloud) |
|
||||||
|
| `plex` | Traefik ohne Authelia, File-Provider-Ausnahme trotz Host-Netz | Plex bringt native Konto-/Client-Auth mit; vorgeschaltete ForwardAuth wuerde Plex Web, Apps und Client-Flows stoeren. Docker-Labels sind fuer diesen Host-Netz-Container ungeeignet, weil Traefik sonst `127.0.0.1:32400` nutzt; daher `traefik/dynamic/plex.yml` mit Ziel `192.168.178.58:32400`. Route nur ueber Traefik/443 (`plex.kaleschke.info`), direkter Plex-WAN-Port 32400 und Plex Remote Access bleiben deaktiviert. |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -495,7 +497,8 @@ Endstand:
|
|||||||
|
|
||||||
- `PlexOnlineUsername="Xeridos"`, `PlexOnlineMail="michideheld@gmx.de"`, `PlexOnlineHome="1"`.
|
- `PlexOnlineUsername="Xeridos"`, `PlexOnlineMail="michideheld@gmx.de"`, `PlexOnlineHome="1"`.
|
||||||
- Bibliotheken neu angelegt via Plex-Web → Verwalte Mediatheken → `/data/movies`, `/data/Heimatfilme` etc.
|
- Bibliotheken neu angelegt via Plex-Web → Verwalte Mediatheken → `/data/movies`, `/data/Heimatfilme` etc.
|
||||||
- `PublishServerOnPlexOnlineKey="0"` (Remote Access deaktiviert), Plex-Relay aus → Plex bleibt strikt LAN/Tailscale-only, konsistent zum Tailscale-First-Operator-Modell.
|
- `PublishServerOnPlexOnlineKey="0"` (Remote Access deaktiviert), Plex-Relay aus.
|
||||||
|
- 2026-06-06: Externer Komfortzugriff ueber `https://plex.kaleschke.info` via Traefik ergaenzt. Das ist **kein** Plex-Remote-Access und keine direkte FRITZ!Box-Freigabe auf `32400`; Plex bleibt hinter Traefik/443 und nutzt native Plex-Auth.
|
||||||
|
|
||||||
Konsequenzen fuer Doku/Betrieb:
|
Konsequenzen fuer Doku/Betrieb:
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,17 @@ services:
|
|||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
image: ghcr.io/immich-app/immich-machine-learning:release@sha256:a2501141440f10516d329fdfba2c68082e19eb9ba6016c061ac80d23beadf7f3
|
image: ghcr.io/immich-app/immich-machine-learning:release@sha256:a2501141440f10516d329fdfba2c68082e19eb9ba6016c061ac80d23beadf7f3
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
# Workaround fuer gunicorn-25.1.0-Control-Socket-Bug: der Worker haengt
|
||||||
|
# nach "Control socket listening at /usr/src/gunicorn.ctl" und erreicht
|
||||||
|
# nie "Application startup complete" -> Container bleibt dauerhaft
|
||||||
|
# unhealthy, ML (Gesichtserkennung/CLIP/Smart-Search) ist tot.
|
||||||
|
# --no-control-socket deaktiviert das fehlerhafte Feature. immich-ml
|
||||||
|
# startet gunicorn als Subprozess, der GUNICORN_CMD_ARGS aus der Env
|
||||||
|
# liest und anhaengt. Bestaetigte Upstream-Regression seit Immich 2.6
|
||||||
|
# (immich#27228, gunicorn#3510). Re-check: bei Immich-Update, das
|
||||||
|
# gunicorn auf >25.1.0/<25.1.0 mit Fix bringt, wieder entfernen.
|
||||||
|
GUNICORN_CMD_ARGS: "--no-control-socket"
|
||||||
volumes:
|
volumes:
|
||||||
- model-cache:/cache
|
- model-cache:/cache
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
mail-archiver:
|
mail-archiver:
|
||||||
image: s1t5/mailarchiver@sha256:ea7fd8c2e3e0ef0941e8dd9e726e35a8de33296f5c7b9ed811df5168ae6a9714
|
image: s1t5/mailarchiver@sha256:4ea7ecc47ad1dd2c523b85c3967574b61e39def1b6fd26edf874e21733c4018c
|
||||||
container_name: mail-archiver
|
container_name: mail-archiver
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -4,6 +4,12 @@ services:
|
|||||||
container_name: mealie
|
container_name: mealie
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# OIDC: Authelia ueber Host-LAN-IP -> Traefik erreichbar (Container-DNS loest
|
||||||
|
# auth.kaleschke.info sonst nicht; gleiches Muster wie Komodo. SNI bleibt der
|
||||||
|
# Hostname, Let's-Encrypt-Cert validiert weiter.
|
||||||
|
extra_hosts:
|
||||||
|
- "auth.kaleschke.info:192.168.178.58"
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
TZ: Europe/Berlin
|
TZ: Europe/Berlin
|
||||||
ALLOW_SIGNUP: "false"
|
ALLOW_SIGNUP: "false"
|
||||||
@@ -18,6 +24,16 @@ services:
|
|||||||
|
|
||||||
BASE_URL: https://mealie.kaleschke.info
|
BASE_URL: https://mealie.kaleschke.info
|
||||||
|
|
||||||
|
# --- Authelia OIDC SSO (additiv, 2026-06-06; lokaler Login bleibt) ---
|
||||||
|
OIDC_AUTH_ENABLED: "true"
|
||||||
|
OIDC_PROVIDER_NAME: Authelia
|
||||||
|
OIDC_CONFIGURATION_URL: https://auth.kaleschke.info/.well-known/openid-configuration
|
||||||
|
OIDC_CLIENT_ID: mealie
|
||||||
|
OIDC_CLIENT_SECRET: ${MEALIE_OIDC_CLIENT_SECRET}
|
||||||
|
OIDC_SIGNUP_ENABLED: "true"
|
||||||
|
OIDC_AUTO_REDIRECT: "false"
|
||||||
|
OIDC_REMEMBER_ME: "true"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
- /mnt/user/appdata/mealie/data:/app/data
|
- /mnt/user/appdata/mealie/data:/app/data
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
n8n:
|
n8n:
|
||||||
image: docker.n8n.io/n8nio/n8n:2.25.5@sha256:08862289f9e9b387d91eab66a74d40d307c0c9b74d2504866f8fe61e9063c838
|
image: docker.n8n.io/n8nio/n8n:2.26.2@sha256:61ba01bc5e39304bbc928c9dbecd938c3a5cc1331b68affba6a34d0f654c43d9
|
||||||
container_name: n8n
|
container_name: n8n
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
nextcloud:
|
nextcloud:
|
||||||
image: nextcloud:33.0.5-apache@sha256:96f8b6ad4adf4044ac6d3cbc10ef99b4897c90792782b5b60a5700e5b1b97b84
|
image: nextcloud:33.0.5-apache@sha256:56bdc45109067500fd0832fa64832b7c77a167d9394cbf5f0f4b59740b94194d
|
||||||
container_name: nextcloud
|
container_name: nextcloud
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ services:
|
|||||||
image: ghcr.io/paperless-ngx/paperless-ngx:2.20.15@sha256:6c86cad803970ea782683a8e80e7403444c5bf3cf70de63b4d3c8e87500db92f
|
image: ghcr.io/paperless-ngx/paperless-ngx:2.20.15@sha256:6c86cad803970ea782683a8e80e7403444c5bf3cf70de63b4d3c8e87500db92f
|
||||||
container_name: paperless-ngx
|
container_name: paperless-ngx
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# OIDC: Authelia ueber Host-LAN-IP -> Traefik erreichbar (Container-DNS sonst nicht)
|
||||||
|
extra_hosts:
|
||||||
|
- "auth.kaleschke.info:192.168.178.58"
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
environment:
|
environment:
|
||||||
@@ -17,6 +20,11 @@ services:
|
|||||||
- PAPERLESS_OCR_LANGUAGE=deu+eng
|
- PAPERLESS_OCR_LANGUAGE=deu+eng
|
||||||
- PAPERLESS_URL=https://paperless.kaleschke.info
|
- PAPERLESS_URL=https://paperless.kaleschke.info
|
||||||
|
|
||||||
|
# --- Authelia OIDC SSO (additiv, 2026-06-06; lokaler Login bleibt) ---
|
||||||
|
- PAPERLESS_APPS=allauth.socialaccount.providers.openid_connect
|
||||||
|
- PAPERLESS_SOCIAL_AUTO_SIGNUP=true
|
||||||
|
- 'PAPERLESS_SOCIALACCOUNT_PROVIDERS={"openid_connect":{"OAUTH_PKCE_ENABLED":true,"APPS":[{"provider_id":"authelia","name":"Authelia","client_id":"paperless","secret":"${PAPERLESS_OIDC_SECRET}","settings":{"server_url":"https://auth.kaleschke.info"}}]}}'
|
||||||
|
|
||||||
# Barcode / ASN
|
# Barcode / ASN
|
||||||
- PAPERLESS_CONSUMER_ENABLE_BARCODES=1
|
- PAPERLESS_CONSUMER_ENABLE_BARCODES=1
|
||||||
- PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=1
|
- PAPERLESS_CONSUMER_ENABLE_ASN_BARCODE=1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
unbound:
|
unbound:
|
||||||
image: shaanmajid/unbound:1.25.1@sha256:96809ff052e8bd79bba30e067d8b27ed9a2f069b6b2a3484fe1d0eb45aba07c5
|
image: shaanmajid/unbound:1.25.1@sha256:f140db02a005904802bf5840093e95e675321aa060a00426fdffc2a3ac2eeb6b
|
||||||
container_name: unbound
|
container_name: unbound
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
# Authelia OIDC fuer Apps - Plan & Runbook
|
||||||
|
|
||||||
|
Stand: 2026-06-06. Authelia-Version: **v4.39.20**.
|
||||||
|
|
||||||
|
Ziel: App-uebergreifendes Single-Sign-On ueber Authelia als OpenID-Connect-Provider
|
||||||
|
(`https://auth.kaleschke.info`). Statt pro App eigener Logins meldet man sich einmal
|
||||||
|
bei Authelia an (inkl. 2FA) und wird per OIDC an die App durchgereicht.
|
||||||
|
|
||||||
|
> **Status:** aktives Runbook. Grafana und Mealie sind seit 2026-06-06 live
|
||||||
|
> und per Login-Smoke verifiziert. Der weitere Rollout bleibt additiv: lokale
|
||||||
|
> App-Logins bleiben als Fallback aktiv.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Grundregeln (wichtig)
|
||||||
|
|
||||||
|
- **Secrets gehoeren nie ins Repo.** OIDC-Client-Secrets (Klartext und pbkdf2-Hash)
|
||||||
|
liegen ausschliesslich in der Host-Config `/mnt/user/appdata/authelia/config/configuration.yml`
|
||||||
|
(Hash) und im jeweiligen App-Stack (Klartext, via Komodo Stack-ENV / Secret-Datei),
|
||||||
|
plus optional Vaultwarden. Dieses Dokument enthaelt nur Schema und Variablennamen.
|
||||||
|
- **OIDC-Clients leben host-seitig**, wie der bestehende `beszel`-Client. Die Repo-Baseline
|
||||||
|
`security/authelia/configuration.yml` haelt nur die nicht-geheime Struktur
|
||||||
|
(`access_control` etc.); `services/authelia-diff.sh` vergleicht standardmaessig nur
|
||||||
|
`access_control`, OIDC-Clients auf dem Host loesen also keinen Drift-Alarm aus.
|
||||||
|
- **Issuer/Endpoints** (Authelia OIDC):
|
||||||
|
- Issuer: `https://auth.kaleschke.info`
|
||||||
|
- Authorization: `https://auth.kaleschke.info/api/oidc/authorization`
|
||||||
|
- Token: `https://auth.kaleschke.info/api/oidc/token`
|
||||||
|
- Userinfo: `https://auth.kaleschke.info/api/oidc/userinfo`
|
||||||
|
- JWKS: `https://auth.kaleschke.info/jwks.json`
|
||||||
|
- Discovery: `https://auth.kaleschke.info/.well-known/openid-configuration`
|
||||||
|
- **PKCE an, wo moeglich** (`require_pkce: true`, `S256`), wie beim Beszel-Client.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Client-Schema (Authelia v4.39, gespiegelt vom bestehenden `beszel`-Client)
|
||||||
|
|
||||||
|
Pro App ein Block unter `identity_providers.oidc.clients` in der **Host-Config**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
identity_providers:
|
||||||
|
oidc:
|
||||||
|
clients:
|
||||||
|
- client_id: '<app>'
|
||||||
|
client_name: '<App-Name>'
|
||||||
|
client_secret: '<pbkdf2-sha512-Hash - NUR auf dem Host>'
|
||||||
|
public: false
|
||||||
|
authorization_policy: 'two_factor' # admin-Apps: two_factor; Familien-Apps: s.u.
|
||||||
|
require_pkce: true
|
||||||
|
pkce_challenge_method: 'S256'
|
||||||
|
redirect_uris:
|
||||||
|
- 'https://<app>.kaleschke.info/<oidc-callback-pfad>'
|
||||||
|
scopes:
|
||||||
|
- 'openid'
|
||||||
|
- 'profile'
|
||||||
|
- 'email'
|
||||||
|
- 'groups'
|
||||||
|
response_types:
|
||||||
|
- 'code'
|
||||||
|
grant_types:
|
||||||
|
- 'authorization_code'
|
||||||
|
token_endpoint_auth_method: 'client_secret_basic'
|
||||||
|
userinfo_signed_response_alg: 'none'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Client-Secret erzeugen (auf dem Host)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec authelia authelia crypto hash generate pbkdf2 \
|
||||||
|
--variant sha512 --random --random.length 72 --random.charset rfc3986
|
||||||
|
```
|
||||||
|
|
||||||
|
- Ausgabe: **Random Password** (Klartext) + **Digest** (pbkdf2-Hash).
|
||||||
|
- **Hash** -> Host-Config `client_secret`.
|
||||||
|
- **Klartext** -> App-Stack (Komodo Stack-ENV/Secret) + optional Vaultwarden.
|
||||||
|
- Klartext **nicht** ins Repo, nicht in Logs.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reihenfolge / Rollout
|
||||||
|
|
||||||
|
| Stufe | App | Domain | OIDC-Support | Policy | Risiko | Begruendung |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| **1 (Proof) ERLEDIGT 2026-06-06** | Grafana (monitoring) | `monitoring.kaleschke.info` | nativ (`generic_oauth`) | `two_factor` | niedrig | **Live + Login verifiziert.** Authelia-Client `grafana` (host), Secret als Datei `/mnt/user/appdata/secrets/grafana_oidc_client_secret` via `__FILE`, ForwardAuth-Middleware durch OIDC ersetzt, lokaler Admin bleibt Fallback |
|
||||||
|
| 2 | Immich | `immich.kaleschke.info` | nativ (Admin-UI/Config-File) | s. u. (Familie) | mittel | **GEPARKT bis Onboarding (Entscheidung 2026-06-06):** nur `micha` hat Authelia-Account, Familien-SSO-Nutzen entsteht erst mit Familien-Accounts; Immich ist mobil-lastig (hoechste Stoeranfaelligkeit) und braucht UI/Config-File. Erst nach Onboarding gezielt. Runbook bereit. |
|
||||||
|
| 3 | Nextcloud | `cloud.kaleschke.info` | App `user_oidc` (+occ) | s. u. | mittel | **GEPARKT bis Onboarding (Entscheidung 2026-06-06):** wie Immich; braucht `user_oidc`-App-Install + `occ`. Lokaler Login bleibt. Erst nach Onboarding. Runbook bereit. |
|
||||||
|
| **4 ERLEDIGT 2026-06-06** | Mealie | `mealie.kaleschke.info` | nativ | `one_factor` | niedrig | **Live + Login verifiziert.** OIDC-Env additiv (lokaler Login bleibt), Secret als Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}`, `extra_hosts` noetig (s. Gotchas) |
|
||||||
|
| 5 | Paperless-ngx | `paperless.kaleschke.info` | `django-allauth` (Umgebungsvariablen) | `two_factor` | mittel | dokumentenlastig, Operator-nah |
|
||||||
|
|
||||||
|
**Nicht OIDC:** Vaultwarden hat kein Standard-Endnutzer-OIDC (SSO ist Enterprise/Bitwarden-Feature) -> bleibt eigener Login. ntfy bleibt wie gehabt.
|
||||||
|
|
||||||
|
### Policy Familien-Apps
|
||||||
|
|
||||||
|
- Admin-Apps (Grafana, Paperless): `authorization_policy: two_factor`.
|
||||||
|
- Familien-Apps (Immich, Nextcloud, Mealie): Start mit `one_factor` und lokalen
|
||||||
|
App-Logins als Fallback. 2FA fuer Familie erst spaeter, sobald TOTP-Enrollment
|
||||||
|
pro Person eingerichtet ist; sonst entsteht unnoetiges Lockout-Risiko.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Stufe 1 konkret: Grafana (empfohlener Erststart)
|
||||||
|
|
||||||
|
### A) Authelia (Host) - Client anlegen
|
||||||
|
1. Secret erzeugen (Befehl oben). Klartext + Hash notieren.
|
||||||
|
2. In `/mnt/user/appdata/authelia/config/configuration.yml` unter
|
||||||
|
`identity_providers.oidc.clients` neuen Block einfuegen:
|
||||||
|
```yaml
|
||||||
|
- client_id: 'grafana'
|
||||||
|
client_name: 'Grafana'
|
||||||
|
client_secret: '<HASH>'
|
||||||
|
public: false
|
||||||
|
authorization_policy: 'two_factor'
|
||||||
|
require_pkce: true
|
||||||
|
pkce_challenge_method: 'S256'
|
||||||
|
redirect_uris:
|
||||||
|
- 'https://monitoring.kaleschke.info/login/generic_oauth'
|
||||||
|
scopes: ['openid', 'profile', 'email', 'groups']
|
||||||
|
response_types: ['code']
|
||||||
|
grant_types: ['authorization_code']
|
||||||
|
token_endpoint_auth_method: 'client_secret_basic'
|
||||||
|
userinfo_signed_response_alg: 'none'
|
||||||
|
```
|
||||||
|
3. `docker restart authelia`, Health + Log pruefen (`Startup complete`, keine Fehler).
|
||||||
|
|
||||||
|
### B) Grafana (Komodo Stack-ENV) - generic_oauth
|
||||||
|
Im `monitoring`-Stack (Grafana) setzen (Klartext-Secret aus Schritt A):
|
||||||
|
```
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ENABLED=true
|
||||||
|
GF_AUTH_GENERIC_OAUTH_NAME=Authelia
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_ID=grafana
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET=<KLARTEXT-SECRET>
|
||||||
|
GF_AUTH_GENERIC_OAUTH_SCOPES=openid profile email groups
|
||||||
|
GF_AUTH_GENERIC_OAUTH_AUTH_URL=https://auth.kaleschke.info/api/oidc/authorization
|
||||||
|
GF_AUTH_GENERIC_OAUTH_TOKEN_URL=https://auth.kaleschke.info/api/oidc/token
|
||||||
|
GF_AUTH_GENERIC_OAUTH_API_URL=https://auth.kaleschke.info/api/oidc/userinfo
|
||||||
|
GF_AUTH_GENERIC_OAUTH_USE_PKCE=true
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP=true
|
||||||
|
# optional Rollen-Mapping ueber groups:
|
||||||
|
# GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH=contains(groups[*], 'admins') && 'Admin' || 'Viewer'
|
||||||
|
```
|
||||||
|
- `GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET` als Stack-ENV-only (kein `_FILE`-Support) -> in
|
||||||
|
`docs/SECRETS_MAP.md` als `grafana_oidc_client_secret` (Stack-ENV) nachziehen.
|
||||||
|
|
||||||
|
### C) Test + Rollback
|
||||||
|
- Test: `monitoring.kaleschke.info` -> "Sign in with Authelia" -> Authelia-Login (2FA) -> zurueck in Grafana, eingeloggt.
|
||||||
|
- **Fallback bleibt:** lokaler Grafana-Admin-Login (`/login`) ist weiter aktiv -> kein Lockout.
|
||||||
|
- Rollback: `GF_AUTH_GENERIC_OAUTH_ENABLED=false` (Grafana redeploy) und/oder Client-Block in Authelia entfernen + `docker restart authelia`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Doku-Nachzug bei jedem neuen Client
|
||||||
|
|
||||||
|
- `docs/SECRETS_MAP.md`: pro App `<app>_oidc_client_secret` (Stack-ENV) + Hinweis "Hash in Authelia-Host-Config".
|
||||||
|
- `docs/SERVICE_CATALOG.md`: App-Zeile um "OIDC via Authelia" ergaenzen.
|
||||||
|
- Dieses Dokument: Rollout-Tabelle abhaken.
|
||||||
|
- `docs/MASTER_TODO.md`: Fortschritt im OIDC-Punkt nachziehen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gotchas (aus dem realen Rollout 2026-06-06)
|
||||||
|
|
||||||
|
- **`extra_hosts` ist Pflicht fuer App-Container, die selbst zu Authelia connecten**
|
||||||
|
(OIDC-Discovery/Token sind Server-zu-Server): Der App-Container loest
|
||||||
|
`auth.kaleschke.info` per Docker-DNS oft nicht auf -> `httpx.ConnectTimeout` /
|
||||||
|
500 beim OAuth-Start. Fix wie Komodo:
|
||||||
|
```yaml
|
||||||
|
extra_hosts:
|
||||||
|
- "auth.kaleschke.info:192.168.178.58"
|
||||||
|
```
|
||||||
|
Cert validiert weiter (SNI/Hostname bleibt gleich, nur die IP wird gemappt).
|
||||||
|
Gilt fuer Mealie (bestaetigt) und sehr wahrscheinlich Paperless/Immich/Nextcloud.
|
||||||
|
- **Additiv heisst additiv:** OIDC als zusaetzlichen Login aktivieren, lokalen
|
||||||
|
Login NICHT abschalten, `AUTO_REDIRECT`/Force-OIDC aus -> kein Lockout.
|
||||||
|
- **Account-Linking per E-Mail:** Apps verknuepfen den OIDC-User i. d. R. per
|
||||||
|
E-Mail-Claim. Stimmt die Authelia-E-Mail mit dem App-Account, wird verknuepft;
|
||||||
|
sonst legt die App (bei aktivem Signup) einen neuen User an.
|
||||||
|
- **Secret-Mechanik je App verschieden:** Grafana `__FILE` (Docker-Secret),
|
||||||
|
Mealie Stack-ENV `${...}`. Hash immer in der Authelia-Host-Config, Klartext nie ins Repo.
|
||||||
|
|
||||||
|
## Spaetere Feinschliffe vor breitem Rollout
|
||||||
|
|
||||||
|
1. Gruppen/Rollen-Mapping: braucht es Authelia-Gruppen (z. B. `admins`, `family`) fuer
|
||||||
|
App-Rollen (Grafana Admin/Viewer, Nextcloud-Gruppen)? Wenn ja, in der Authelia
|
||||||
|
User-Datenbank Gruppen pflegen.
|
||||||
|
2. Familien-2FA spaeter neu bewerten, nachdem echte Familien-Accounts in Authelia
|
||||||
|
angelegt und TOTP pro Person verstanden ist.
|
||||||
@@ -21,7 +21,7 @@ Dieses Dokument beschreibt externe Anbieter und Konten, von denen Betrieb, Recov
|
|||||||
| OpenAI API | Paperless-GPT LLM und Vision-OCR | mittel | Automatische Dokument-Titel, Tags, Korrespondenten und LLM-OCR fallen aus; Paperless selbst laeuft weiter | OpenAI-Projekt/API-Key ausserhalb Repo | Key in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten |
|
| OpenAI API | Paperless-GPT LLM und Vision-OCR | mittel | Automatische Dokument-Titel, Tags, Korrespondenten und LLM-OCR fallen aus; Paperless selbst laeuft weiter | OpenAI-Projekt/API-Key ausserhalb Repo | Key in Vaultwarden/Komodo sichern, bei Offenlegung rotieren; Kosten/Usage im OpenAI-Projekt beobachten |
|
||||||
| Let's Encrypt | TLS-Zertifikate | hoch | Cert-Erneuerung faellt aus | automatisch via Traefik und Cloudflare DNS-Challenge | Cert-Expiry Alert einrichten; Cloudflare-Token und Traefik-Storage pruefen |
|
| Let's Encrypt | TLS-Zertifikate | hoch | Cert-Erneuerung faellt aus | automatisch via Traefik und Cloudflare DNS-Challenge | Cert-Expiry Alert einrichten; Cloudflare-Token und Traefik-Storage pruefen |
|
||||||
| Container Registries | Image Pulls von Docker Hub, GHCR, LSCR, Gitea Registry u. a. | mittel | Redeploy/Update blockiert | ueberwiegend oeffentlich; keine produktiven Registry-Tokens im Repo | Gepinnte Digests und lokale Runtime helfen kurzfristig; Updates geplant und einzeln deployen |
|
| Container Registries | Image Pulls von Docker Hub, GHCR, LSCR, Gitea Registry u. a. | mittel | Redeploy/Update blockiert | ueberwiegend oeffentlich; keine produktiven Registry-Tokens im Repo | Gepinnte Digests und lokale Runtime helfen kurzfristig; Updates geplant und einzeln deployen |
|
||||||
| Plex Konto/Remote Access | Plex native Auth, ggf. Remote Access und Claim | mittel | Plex-Clients/Remote-Funktionen koennen ausfallen | Plex-Konto ausserhalb Repo; `PLEX_CLAIM` nur fuer Setup | LAN-Medienpfade bleiben lokal; Konto-Recovery separat sichern |
|
| Plex Konto | Plex native Auth, Claim und Client-Zugriff ueber `plex.kaleschke.info` | mittel | Plex-Web/App-Login und Clients koennen ausfallen; LAN-Medienpfade bleiben lokal | Plex-Konto ausserhalb Repo; `PLEX_CLAIM` nur fuer Setup | Plex Remote Access bleibt aus; externer Zugriff laeuft ueber Traefik/443. Konto-Recovery separat sichern |
|
||||||
| Mobile Push | ntfy und ggf. mobile Plattform-Pushes | niedrig/mittel | Alerts erreichen Mobilgeraete ggf. nicht | App-/Device-seitig | Kritische Alerts zusaetzlich in Grafana/Glance sichtbar halten |
|
| Mobile Push | ntfy und ggf. mobile Plattform-Pushes | niedrig/mittel | Alerts erreichen Mobilgeraete ggf. nicht | App-/Device-seitig | Kritische Alerts zusaetzlich in Grafana/Glance sichtbar halten |
|
||||||
| Operator-DR-Workstation | Bare-Metal-Recovery-Arbeitsplatz (Gaming-PC Windows, lokaler Repo-Clone `G:\Gitea_Clone\homelab-infra`) | kritisch | Ohne Workstation kein Borg-Extract, kein Hetzner-Zugriff, kein Repo-Bootstrap; der Unraid-Host ist im Bare-Metal-Fall gerade weg | Operator-PC, WSL2 + Borg-Client, SSH-Key fuer Hetzner Storage Box, Offline-Kopie der Borg-Passphrase | Setup als bewusste DR-Vorbedingung pflegen (siehe Abschnitt "DR-Workstation Bare-Metal-Kit") |
|
| Operator-DR-Workstation | Bare-Metal-Recovery-Arbeitsplatz (Gaming-PC Windows, lokaler Repo-Clone `G:\Gitea_Clone\homelab-infra`) | kritisch | Ohne Workstation kein Borg-Extract, kein Hetzner-Zugriff, kein Repo-Bootstrap; der Unraid-Host ist im Bare-Metal-Fall gerade weg | Operator-PC, WSL2 + Borg-Client, SSH-Key fuer Hetzner Storage Box, Offline-Kopie der Borg-Passphrase | Setup als bewusste DR-Vorbedingung pflegen (siehe Abschnitt "DR-Workstation Bare-Metal-Kit") |
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ Nachteile, ehrlich gesagt: Wenn der Server zuhause aus ist, sind die Apps weg, b
|
|||||||
| **Vaultwarden** | Passwoerter sicher speichern und auf jedem Geraet nachschauen | Bitwarden-App (kostenlos), beim ersten Start Server-URL auf `vault.kaleschke.info` aendern lassen |
|
| **Vaultwarden** | Passwoerter sicher speichern und auf jedem Geraet nachschauen | Bitwarden-App (kostenlos), beim ersten Start Server-URL auf `vault.kaleschke.info` aendern lassen |
|
||||||
| **Mealie** | Rezepte sammeln, Wochenplan, Einkaufsliste | Web `mealie.kaleschke.info` oder Mealie-App |
|
| **Mealie** | Rezepte sammeln, Wochenplan, Einkaufsliste | Web `mealie.kaleschke.info` oder Mealie-App |
|
||||||
| **Paperless** | Briefe und wichtige Dokumente scannen, durchsuchen, ablegen | Web `paperless.kaleschke.info`; Scan-Workflow erklaert Michi |
|
| **Paperless** | Briefe und wichtige Dokumente scannen, durchsuchen, ablegen | Web `paperless.kaleschke.info`; Scan-Workflow erklaert Michi |
|
||||||
| **Plex** | Filme und Musik auf Fernseher, Handy und Tablet | Plex-App auf dem Geraet, mit Konto anmelden |
|
| **Plex** | Filme und Musik auf Fernseher, Handy und Tablet | Web `https://plex.kaleschke.info` oder Plex-App auf dem Geraet, mit Konto anmelden |
|
||||||
|
|
||||||
> Wenn du eine App auf dem Handy installierst und sie fragt nach einer Server-URL, ist das immer eine `...kaleschke.info`-Adresse. Wenn du dir nicht sicher bist, frag bevor du etwas eintippst.
|
> Wenn du eine App auf dem Handy installierst und sie fragt nach einer Server-URL, ist das immer eine `...kaleschke.info`-Adresse. Wenn du dir nicht sicher bist, frag bevor du etwas eintippst.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Guest / IoT Network Runbook
|
||||||
|
|
||||||
|
Stand: 2026-06-06
|
||||||
|
|
||||||
|
Dieses Runbook beschreibt den sicheren Weg, das FRITZ!Box-Gastnetz zu aktivieren,
|
||||||
|
ohne versehentlich Homelab-Admin-Ports aus dem Gastsegment erreichbar zu machen.
|
||||||
|
|
||||||
|
## Zielbild
|
||||||
|
|
||||||
|
- Normales LAN bleibt `192.168.178.0/24`.
|
||||||
|
- Kallilabcore bleibt im normalen LAN unter `192.168.178.58`.
|
||||||
|
- FRITZ!Box-Gast-WLAN darf Internetzugang haben, aber keinen Zugriff auf
|
||||||
|
`192.168.178.0/24`.
|
||||||
|
- Homelab-Admin-Pfade bleiben Operator-only:
|
||||||
|
- Tailscale fuer Admin-Zugriff
|
||||||
|
- Authelia/2FA fuer geschuetzte Web-UIs
|
||||||
|
- keine LAN-Admin-Ports aus dem Gastnetz
|
||||||
|
|
||||||
|
## Vorbedingungen
|
||||||
|
|
||||||
|
Vor dem Einschalten des Gast-WLANs muessen diese Preflights gruen sein:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
G:\Gitea_Clone\homelab-infra\ops\maintenance\check-guest-iot-isolation.ps1 -Mode LanPreflight
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung im normalen LAN:
|
||||||
|
|
||||||
|
- `192.168.178.58:8082` ist blockiert (AdGuard Admin nur Tailscale).
|
||||||
|
- `192.168.178.58:8181` ist blockiert (InfluxDB nicht LAN-exponiert).
|
||||||
|
- `192.168.178.58:80`, `443`, `222` koennen im normalen LAN erreichbar sein.
|
||||||
|
|
||||||
|
Auf Unraid zusaetzlich:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mnt/user/services/homelab-infra/ops/maintenance/check-guest-iot-preflight.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Validierung 2026-06-06: Host-Preflight erfolgreich, Report
|
||||||
|
`/mnt/user/backups/restore-reports/guest-iot-preflight-2026-06-06-131316.md`.
|
||||||
|
Ergebnis: FRITZ!Box 7590 per TR-064 erreichbar, `192.168.178.58:8082`
|
||||||
|
blockiert, `100.80.98.33:8082` erreichbar, `192.168.178.58:8181` blockiert.
|
||||||
|
|
||||||
|
Gast-WLAN-Smoke 2026-06-06: Operator hat ein iPhone mit `Fritzi Gastzugang`
|
||||||
|
verbunden und folgende Ziele getestet; alle waren aus dem Gast-WLAN nicht
|
||||||
|
erreichbar:
|
||||||
|
|
||||||
|
- `http://192.168.178.58:8082`
|
||||||
|
- `http://192.168.178.58:8181`
|
||||||
|
- `http://192.168.178.58:222`
|
||||||
|
- `https://192.168.178.58`
|
||||||
|
- `http://192.168.178.1`
|
||||||
|
|
||||||
|
Damit ist die Gastnetz-Isolation fuer die getesteten Homelab-/Router-Adminpfade
|
||||||
|
validiert.
|
||||||
|
|
||||||
|
## FRITZ!Box Schritte
|
||||||
|
|
||||||
|
In der FRITZ!Box UI:
|
||||||
|
|
||||||
|
1. `WLAN -> Gastzugang` oeffnen.
|
||||||
|
2. `Gastzugang aktiv` einschalten.
|
||||||
|
3. WPA2/WPA3-Verschluesselung aktiv lassen.
|
||||||
|
4. Eigenen Gast-SSID-Namen setzen, z. B. `Fritzi-Gast`.
|
||||||
|
5. Starkes Passwort setzen und in Vaultwarden ablegen.
|
||||||
|
6. Option `Geraete im Gastnetz duerfen miteinander kommunizieren` deaktiviert
|
||||||
|
lassen, sofern nicht bewusst gebraucht.
|
||||||
|
7. Option fuer Zugriff auf das Heimnetz / private Netzwerk deaktiviert lassen.
|
||||||
|
8. Gastzugang speichern.
|
||||||
|
|
||||||
|
Wichtig: Die genaue FRITZ!OS-8.25-UI-Beschriftung kann leicht variieren. Der
|
||||||
|
entscheidende Punkt ist: Gastgeraete duerfen keinen Zugriff auf das Heimnetz
|
||||||
|
haben.
|
||||||
|
|
||||||
|
## Verifikation
|
||||||
|
|
||||||
|
Ein Handy oder Laptop mit dem Gast-WLAN verbinden, dann auf diesem Geraet testen:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
G:\Gitea_Clone\homelab-infra\ops\maintenance\check-guest-iot-isolation.ps1 -Mode Guest
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung aus dem Gast-WLAN:
|
||||||
|
|
||||||
|
- `192.168.178.58:80` blockiert
|
||||||
|
- `192.168.178.58:443` blockiert
|
||||||
|
- `192.168.178.58:222` blockiert
|
||||||
|
- `192.168.178.58:8082` blockiert
|
||||||
|
- `192.168.178.58:8181` blockiert
|
||||||
|
- `192.168.178.1:80` blockiert oder nur Gast-Gateway-Ansicht
|
||||||
|
|
||||||
|
Wenn der Test `Risk count: 0` meldet, ist die Isolation fuer die getesteten
|
||||||
|
Homelab-Admin-Pfade ausreichend.
|
||||||
|
|
||||||
|
## Betrieb
|
||||||
|
|
||||||
|
- Familien-/Gaestegeraete kommen ins Gast-WLAN, wenn sie keinen direkten Zugriff
|
||||||
|
auf LAN-Geraete brauchen.
|
||||||
|
- Homelab-Apps fuer Familie laufen perspektivisch ueber HTTPS/OIDC, nicht ueber
|
||||||
|
direkten LAN-Zugriff.
|
||||||
|
- Geraete, die lokale Discovery brauchen (z. B. manche Smart-TV/Plex-Szenarien),
|
||||||
|
bleiben im normalen LAN oder bekommen eine separate bewusste Entscheidung.
|
||||||
|
|
||||||
|
## Rollback
|
||||||
|
|
||||||
|
Wenn nach Aktivierung etwas Unerwartetes passiert:
|
||||||
|
|
||||||
|
1. FRITZ!Box: `WLAN -> Gastzugang` oeffnen.
|
||||||
|
2. Gastzugang deaktivieren.
|
||||||
|
3. Speichern.
|
||||||
|
4. Normalen LAN-Zugriff pruefen:
|
||||||
|
```powershell
|
||||||
|
G:\Gitea_Clone\homelab-infra\ops\maintenance\check-guest-iot-isolation.ps1 -Mode LanPreflight
|
||||||
|
```
|
||||||
|
|
||||||
|
Es werden durch dieses Runbook keine Docker-Stacks, Secrets oder produktiven
|
||||||
|
Appdaten veraendert.
|
||||||
@@ -15,7 +15,7 @@ Entscheidungen festgehalten. Details in den jeweiligen Abschnitten unten.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| USV / Power Loss | **Bewusst auf Q3/2026 geparkt.** Keine Anschaffung dieses Quartal; Power-Loss bleibt akzeptiertes Risiko. | Naechstes Hardware-Upgrade, erneuter realer Stromausfall mit Datenfolge, oder Q3-Review (ab 2026-07-01) |
|
| USV / Power Loss | **Bewusst auf Q3/2026 geparkt.** Keine Anschaffung dieses Quartal; Power-Loss bleibt akzeptiertes Risiko. | Naechstes Hardware-Upgrade, erneuter realer Stromausfall mit Datenfolge, oder Q3-Review (ab 2026-07-01) |
|
||||||
| Cold-Backup-Rotation | **Bewusst Hetzner-only.** Off-site bleibt allein das Hetzner-Borg-Repo; keine zweite rotierende Cold-Kopie. | Stark wachsender Datenwert, wiederholte Hetzner-Probleme, oder geaenderte Betreiber-Praeferenz |
|
| Cold-Backup-Rotation | **Bewusst Hetzner-only.** Off-site bleibt allein das Hetzner-Borg-Repo; keine zweite rotierende Cold-Kopie. | Stark wachsender Datenwert, wiederholte Hetzner-Probleme, oder geaenderte Betreiber-Praeferenz |
|
||||||
| Stromverbrauch messen | **Operator-Entscheidung offen: Messgeraet beschaffen.** Aktuell kein Smart-Plug/Messgeraet vorhanden, daher keine Messwerte. | Beschaffung eines USB-/Smart-Plug-Messgeraets (z. B. schaltbare Mess-Steckdose) |
|
| Stromverbrauch messen | **Bewusst ohne Messung (Entscheidung 2026-06-06).** Kein Messgeraet; Werte bleiben dauerhaft offen, kein Beschaffungs-Todo. | Nur falls spaeter doch ein Messgeraet angeschafft wird oder Strom-/Kostenfrage relevant wird |
|
||||||
|
|
||||||
## Zweck
|
## Zweck
|
||||||
|
|
||||||
@@ -157,10 +157,10 @@ Bewertung:
|
|||||||
|
|
||||||
## Stromverbrauch
|
## Stromverbrauch
|
||||||
|
|
||||||
**Operator-Entscheidung offen: Messgeraet beschaffen.** Stand 2026-06-05 ist kein
|
**Bewusst ohne Messung (Operator-Entscheidung 2026-06-06).** Es wird kein
|
||||||
Smart-Plug/Messgeraet vorhanden, daher liegen keine Messwerte vor. Die Werte
|
Messgeraet beschafft; Idle/Normal/Backup/Last bleiben dauerhaft offen. Kein
|
||||||
bleiben bewusst offen, bis ein messfaehiges Geraet beschafft ist. Erst danach
|
offener Todo. Falls spaeter doch eine Mess-Steckdose angeschafft wird, reicht
|
||||||
werden Idle/Normal/Backup/Last in einem Durchlauf erfasst.
|
ein einziger Messdurchlauf, um die Tabelle zu fuellen.
|
||||||
|
|
||||||
| Zustand | Verbrauch | Messmethode | Datum |
|
| Zustand | Verbrauch | Messmethode | Datum |
|
||||||
|---|---:|---|---|
|
|---|---:|---|---|
|
||||||
|
|||||||
+15
-13
@@ -26,20 +26,14 @@ 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` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Operator-Entscheidung
|
## Operator-Entscheidung
|
||||||
|
|
||||||
| Thema | Entscheidungsfrage | Quelle |
|
**Stand 2026-06-06: keine offenen Operator-Entscheidungen.** Alle am 2026-06-06
|
||||||
|---|---|---|
|
entschieden — Ergebnisse in "Aktiv", "Geparkt" bzw. "Entschieden 2026-06-06".
|
||||||
| BitLocker-Entscheidung `baerchen` | C: (und ggf. D:) aktivieren oder bewusst deaktiviert lassen? Bei Ja: Recovery-Key vorher nach `D:\30_Finanzen\...`, Vaultwarden und physisch sichern. (Claude fasst BitLocker bewusst nicht an.) | `docs/SECRETS_MAP.md`, `ops/windows-reinstall/docs/laufwerks-neustruktur-2026-06-04.md` |
|
|
||||||
| Veeam Storage Encryption `baerchen` | Reicht der erste unverschluesselte Full-Lauf, oder soll Veeam Storage Encryption aktiviert werden? Bei Ja: Passwort in Vaultwarden anlegen, Job umstellen und neues Full-Backup erzeugen | `ops/windows-reinstall/docs/windows-image-backup-baseline.md`, `docs/SECRETS_MAP.md` |
|
|
||||||
| Stromverbrauch messen | Messgeraet/Smart-Plug beschaffen? Ohne Geraet bleiben Idle/Normal/Backup/Last bewusst offen. **Status 2026-06-05: kein Geraet vorhanden.** | `docs/HARDWARE_INVENTORY.md` Abschnitt "Stromverbrauch" |
|
|
||||||
| Nextcloud 2FA / Brute-Force-Haertung | Operator-TOTP (`twofactor_totp`) jetzt aktivieren? Familien-/OIDC-weite Policy separat | `docs/AUDIT_2026-05-25_TODO.md` |
|
|
||||||
| Authelia Rest-2FA | Weitere Admin-UIs (`monitoring`, `glances`, `glance`, `speedtest`, `pdf`, `mail`, `sp` ...) von `one_factor` auf `two_factor` heben oder bewusst belassen? | `docs/AUDIT_2026-05-25_TODO.md` |
|
|
||||||
| Authelia OIDC fuer Apps | App-uebergreifendes SSO einfuehren - haengt an Familien-/SSO-Grundsatzentscheidung | `docs/AUDIT_2026-05-25_TODO.md` |
|
|
||||||
| Gast-/IoT-Netz | Bewusst kein Gast-WLAN/IoT-Netz aktivieren, solange nicht gebraucht. Vorbedingung bei spaeterer Aktivierung: LAN-Admin-Ports vorher per FRITZ!Box-Filter gegen das Gastsegment sperren | `docs/NETWORK_INVENTORY.md` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -53,10 +47,11 @@ Bewusst nicht jetzt - mit Review-Trigger.
|
|||||||
| Cold-Backup-Rotation | **Bewusst Hetzner-only** (2026-06-05). Keine zweite rotierende Cold-Kopie. Trigger: stark wachsender Datenwert, wiederholte Hetzner-Probleme, geaenderte Praeferenz | `docs/HARDWARE_INVENTORY.md` |
|
| Cold-Backup-Rotation | **Bewusst Hetzner-only** (2026-06-05). Keine zweite rotierende Cold-Kopie. Trigger: stark wachsender Datenwert, wiederholte Hetzner-Probleme, geaenderte Praeferenz | `docs/HARDWARE_INVENTORY.md` |
|
||||||
| WAN-Ausfallschutz | **Spaeter evaluieren** (2026-06-05). Mobilfunk-Failover inaktiv; lokale Apps laufen bei WAN-Ausfall weiter. Trigger: haeufigere/laengere DSL-Ausfaelle oder kritischer Remote-Zugang | `docs/NETWORK_INVENTORY.md` |
|
| WAN-Ausfallschutz | **Spaeter evaluieren** (2026-06-05). Mobilfunk-Failover inaktiv; lokale Apps laufen bei WAN-Ausfall weiter. Trigger: haeufigere/laengere DSL-Ausfaelle oder kritischer Remote-Zugang | `docs/NETWORK_INVENTORY.md` |
|
||||||
| Docker Critical Events Watcher | **Aktiviert 2026-06-05:** Unraid User Script `docker-critical-events-at-start` nutzt den Supervisor und steht in `schedule.json` auf `frequency: start`; Watcher manuell gestartet, Status `running`. Optionaler ntfy-Smoke wurde nachts bewusst nicht gesendet und kann spaeter mit `docker-critical-events-supervisor.sh smoke` nachgeholt werden | `docs/SERVICE_CATALOG.md`, `services/posture-check/docker-critical-events.sh`, `services/posture-check/unraid-user-scripts.md` |
|
| Docker Critical Events Watcher | **Aktiviert 2026-06-05:** Unraid User Script `docker-critical-events-at-start` nutzt den Supervisor und steht in `schedule.json` auf `frequency: start`; Watcher manuell gestartet, Status `running`. Optionaler ntfy-Smoke wurde nachts bewusst nicht gesendet und kann spaeter mit `docker-critical-events-supervisor.sh smoke` nachgeholt werden | `docs/SERVICE_CATALOG.md`, `services/posture-check/docker-critical-events.sh`, `services/posture-check/unraid-user-scripts.md` |
|
||||||
| Negativ-Test Backup-Frische | Quartalsweise: bewusst kaputten/fehlenden Dump in Testpfad simulieren, pruefen ob `homelab-alerts` feuert | `docs/AUDIT_2026-05-25_TODO.md` |
|
| Negativ-Test Backup-Frische | **Validiert 2026-06-06:** `ops/restore-tests/negative-freshness-alert-test.sh` simuliert fehlende Dumps nur in einem synthetischen Restore-Lab-Pfad und sendet einen Test-Alert nach `homelab-alerts`; Host-Lauf schrieb Report `/mnt/user/backups/restore-reports/freshness-negative-2026-06-06-130320.md` (10 Criticals, produktive Dumps unangetastet). Quartalsweise wiederholen: `ops/restore-tests/run-restore-checks.sh freshness-negative` | `ops/restore-tests/README.md`, `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
| End-to-end-DR-Drill | Komplett-Bootstrap Phase 1-5 auf Wegwerf-Host; realistisch erst mit zweiter Hardware (siehe auch Extern blockiert) | `docs/AUDIT_2026-05-25_TODO.md`, `docs/DISASTER_RECOVERY.md` |
|
| End-to-end-DR-Drill | Komplett-Bootstrap Phase 1-5 auf Wegwerf-Host; realistisch erst mit zweiter Hardware (siehe auch Extern blockiert) | `docs/AUDIT_2026-05-25_TODO.md`, `docs/DISASTER_RECOVERY.md` |
|
||||||
| Wiederkehrende Restore-Drills | Vaultwarden, Gitea, Authelia, Komodo, Paperless, Immich, Traefik, PostgreSQL, Mongo, Nextcloud, Mealie, Mail-Archiver nach Matrix-Intervallen rotieren | `docs/RESTORE_MATRIX.md`, `docs/RESTORE_HANDBOOK.md` |
|
| Wiederkehrende Restore-Drills | Vaultwarden, Gitea, Authelia, Komodo, Paperless, Immich, Traefik, PostgreSQL, Mongo, Nextcloud, Mealie, Mail-Archiver nach Matrix-Intervallen rotieren | `docs/RESTORE_MATRIX.md`, `docs/RESTORE_HANDBOOK.md` |
|
||||||
| Dedizierter SMB-User `veeam-baerchen` | Optional spaeter, nur wenn Unraid-User-/Share-Rechte bewusst angefasst werden | `ops/windows-reinstall/docs/windows-image-backup-baseline.md` |
|
| Dedizierter SMB-User `veeam-baerchen` | Optional spaeter, nur wenn Unraid-User-/Share-Rechte bewusst angefasst werden | `ops/windows-reinstall/docs/windows-image-backup-baseline.md` |
|
||||||
|
| Nextcloud 2FA (Operator-TOTP) | **Geparkt (Entscheidung 2026-06-06):** Operator-TOTP fuer Nextcloud erst zusammen mit der app-weiten Familien-/OIDC-Policy entscheiden. Trigger: OIDC-/SSO-Block (jetzt aktiv) erreicht die App-Login-Ebene | `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
| Tailnet-Konsole aufraeumen (Rest) | Nach Docker-Stack-Abbau (2026-06-06) nur noch tote Node-Eintraege: `kallilab-core` (down) und alter Offline-`baerchen` in der Tailscale-Admin-Konsole entfernen. Optional State-Pfad `/mnt/user/appdata/tailscale` nach `_archive/`. Trivial, kein Risiko | `docs/NETWORK_INVENTORY.md` |
|
| Tailnet-Konsole aufraeumen (Rest) | Nach Docker-Stack-Abbau (2026-06-06) nur noch tote Node-Eintraege: `kallilab-core` (down) und alter Offline-`baerchen` in der Tailscale-Admin-Konsole entfernen. Optional State-Pfad `/mnt/user/appdata/tailscale` nach `_archive/`. Trivial, kein Risiko | `docs/NETWORK_INVENTORY.md` |
|
||||||
| CrowdSec vor Traefik | Bewusst nicht umgesetzt; einzige WAN-Tuer ist `443/tcp`, Authelia `regulation:` deckt Brute-Force ab. Neu bewerten bei breiterer Attack Surface | `docs/AUDIT_2026-05-25_TODO.md` |
|
| CrowdSec vor Traefik | Bewusst nicht umgesetzt; einzige WAN-Tuer ist `443/tcp`, Authelia `regulation:` deckt Brute-Force ab. Neu bewerten bei breiterer Attack Surface | `docs/AUDIT_2026-05-25_TODO.md` |
|
||||||
| Hermes-Agent | NAS-Stack bleibt deaktiviert; Review-Deadline 2026-07-25 | `docs/AUDIT_2026-05-25_TODO.md`, `docs/SERVICE_CATALOG.md` |
|
| Hermes-Agent | NAS-Stack bleibt deaktiviert; Review-Deadline 2026-07-25 | `docs/AUDIT_2026-05-25_TODO.md`, `docs/SERVICE_CATALOG.md` |
|
||||||
@@ -75,9 +70,7 @@ Wartet auf ein externes Ereignis oder eine Abhaengigkeit.
|
|||||||
|
|
||||||
| Thema | Blockiert durch | Naechster Schritt sobald entblockt | Quelle |
|
| Thema | Blockiert durch | Naechster Schritt sobald entblockt | Quelle |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `baerchen` Recovery-Test ohne Restore | Haengt am physischen USB-Boot-Test | Von USB `VEEAMRE` booten, SMB-Ziel mounten, Restore Point anzeigen, vor echtem Restore abbrechen. **Owner: Codex/Operator** | `ops/windows-reinstall/docs/windows-image-backup-baseline.md`, `docs/RESTORE_MATRIX.md` |
|
|
||||||
| End-to-end-DR-Drill (Hardware-Teil) | Keine zweite Wegwerf-Hardware verfuegbar | Sobald zweite Hardware da ist: Komplett-Bootstrap Phase 1-5 fahren | `docs/DISASTER_RECOVERY.md` |
|
| End-to-end-DR-Drill (Hardware-Teil) | Keine zweite Wegwerf-Hardware verfuegbar | Sobald zweite Hardware da ist: Komplett-Bootstrap Phase 1-5 fahren | `docs/DISASTER_RECOVERY.md` |
|
||||||
| Stromverbrauch-Messwerte | Kein Messgeraet beschafft | Nach Beschaffung einer schaltbaren Mess-Steckdose einen Messdurchlauf Idle/Normal/Backup/Last fahren | `docs/HARDWARE_INVENTORY.md` |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,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 (Operator-Entscheidung offen) von diffusen TBDs auf bewusste Entscheidungen mit Review-Triggern gehoben.
|
- `docs/HARDWARE_INVENTORY.md`: USV (Q3-Park), Cold-Backup (Hetzner-only) und Stromverbrauch von diffusen TBDs auf bewusste Entscheidungen mit Review-Triggern gehoben.
|
||||||
- `docs/NETWORK_INVENTORY.md`: Tailscale-Inventar am 2026-06-05 **real per read-only SSH gemessen** und eingetragen: IPv6 `fd7a:115c:a1e0::2c01:62b2`, Exit Node `nein`, **Subnet-Router fuer `192.168.178.0/24` aktiv** (widerlegt fruehere Vermutung), Tailnet `taild9fcf2.ts.net`, Geraete-Snapshot + Dubletten-Hinweis. WAN-Failover und Gast-/IoT geschaerft. `zu messen`-Platzhalter entfernt. **`Tailscale-Inventar messen` damit geschlossen.**
|
- `docs/NETWORK_INVENTORY.md`: Tailscale-Inventar am 2026-06-05 **real per read-only SSH gemessen** und eingetragen: IPv6 `fd7a:115c:a1e0::2c01:62b2`, Exit Node `nein`, **Subnet-Router fuer `192.168.178.0/24` aktiv** (widerlegt fruehere Vermutung), Tailnet `taild9fcf2.ts.net`, Geraete-Snapshot + Dubletten-Hinweis. WAN-Failover und Gast-/IoT geschaerft. `zu messen`-Platzhalter entfernt. **`Tailscale-Inventar messen` damit geschlossen.**
|
||||||
- `ops/maintenance/check-unraid-flash-backup.sh` neu: read-only Validierung des Flash-Artefakts (sha256, Frische, Kern-Configs, keine Extraktion). Am 2026-06-05 gegen den Host getestet: Exit 0, sha256 OK, 390 Eintraege, 8/8 Kern-Configs. `docs/RESTORE_MATRIX.md` mit Testdatum/Ergebnis aktualisiert. **Artefakt-Validierung des Unraid-Flash-Backups damit erledigt; nur Stick-Boot-Test offen.**
|
- `ops/maintenance/check-unraid-flash-backup.sh` neu: read-only Validierung des Flash-Artefakts (sha256, Frische, Kern-Configs, keine Extraktion). Am 2026-06-05 gegen den Host getestet: Exit 0, sha256 OK, 390 Eintraege, 8/8 Kern-Configs. `docs/RESTORE_MATRIX.md` mit Testdatum/Ergebnis aktualisiert. **Artefakt-Validierung des Unraid-Flash-Backups damit erledigt; nur Stick-Boot-Test offen.**
|
||||||
- `docs/FAMILY_ONBOARDING.md`: Michi-Checkliste in eine echte Erste-Termin-Checkliste (Vorbereitung, Reihenfolge, Erfolgskriterium, bewusst spaeter) umgebaut.
|
- `docs/FAMILY_ONBOARDING.md`: Michi-Checkliste in eine echte Erste-Termin-Checkliste (Vorbereitung, Reihenfolge, Erfolgskriterium, bewusst spaeter) umgebaut.
|
||||||
@@ -99,6 +92,15 @@ Wartet auf ein externes Ereignis oder eine Abhaengigkeit.
|
|||||||
- **Redundanten Docker-Tailscale-Stack entfernt (2026-06-06):** Befund: Host hatte zwei `tailscaled` — die funktionale native Plugin-Instanz `kallilabcore` (echtes TUN `tailscale1`, Subnet-Router, State im Flash-Backup) und den redundanten userspace-only Docker-Stack `kallilab-core` (`host-services/tailscale/`, routet nichts, nichts haengt dran). Sauber per GitOps abgebaut: Operator hat Komodo-Stack `tailscale` gestoppt+destroyed; danach `git rm host-services/tailscale/`, Glance-Widget entfernt, Architektur-/Service-Catalog-/DR-Bootstrap-/CLAUDE-/Restore-Matrix-/Netzwerk-Doku auf "natives Plugin" nachgezogen. Read-only verifiziert: Container weg, nur noch der native `tailscaled`, Subnet-Route + Operator-Zugriff intakt. Rest: tote Node-Eintraege in der Admin-Konsole entfernen (eigener Todo).
|
- **Redundanten Docker-Tailscale-Stack entfernt (2026-06-06):** Befund: Host hatte zwei `tailscaled` — die funktionale native Plugin-Instanz `kallilabcore` (echtes TUN `tailscale1`, Subnet-Router, State im Flash-Backup) und den redundanten userspace-only Docker-Stack `kallilab-core` (`host-services/tailscale/`, routet nichts, nichts haengt dran). Sauber per GitOps abgebaut: Operator hat Komodo-Stack `tailscale` gestoppt+destroyed; danach `git rm host-services/tailscale/`, Glance-Widget entfernt, Architektur-/Service-Catalog-/DR-Bootstrap-/CLAUDE-/Restore-Matrix-/Netzwerk-Doku auf "natives Plugin" nachgezogen. Read-only verifiziert: Container weg, nur noch der native `tailscaled`, Subnet-Route + Operator-Zugriff intakt. Rest: tote Node-Eintraege in der Admin-Konsole entfernen (eigener Todo).
|
||||||
|
|
||||||
- DR-Workstation Bare-Metal-Kit abgeschlossen: WSL2 Ubuntu 24.04 auf `baerchen`, Borg 1.2.8, GitHub-Read-DR-Key und Hetzner-DR-Key in WSL, `~/dr-smoke.sh` vorhanden. Finaler Smoke 2026-06-06 erfolgreich: GitHub HEAD `3a263a4...`, Hetzner Storage Box Repos sichtbar, Borg-Repo `hetzner_borg_appdata_critical` gelesen, Repository ID `5dd9b949...`, encrypted `Yes (repokey)`, `DR-Smoke OK (2026-06-06 10:05:30)`. Passphrase wurde nur interaktiv eingegeben und nicht gespeichert.
|
- DR-Workstation Bare-Metal-Kit abgeschlossen: WSL2 Ubuntu 24.04 auf `baerchen`, Borg 1.2.8, GitHub-Read-DR-Key und Hetzner-DR-Key in WSL, `~/dr-smoke.sh` vorhanden. Finaler Smoke 2026-06-06 erfolgreich: GitHub HEAD `3a263a4...`, Hetzner Storage Box Repos sichtbar, Borg-Repo `hetzner_borg_appdata_critical` gelesen, Repository ID `5dd9b949...`, encrypted `Yes (repokey)`, `DR-Smoke OK (2026-06-06 10:05:30)`. Passphrase wurde nur interaktiv eingegeben und nicht gespeichert.
|
||||||
|
- Restore-Frische-Negativtest validiert: `ops/restore-tests/negative-freshness-alert-test.sh` erstellt und am 2026-06-06 auf Unraid erfolgreich ausgefuehrt. Ergebnis: synthetischer leerer Dump-Pfad erzeugte erwartungsgemaess 10 Criticals, Test-Alert nach `homelab-alerts` gesendet, Report `/mnt/user/backups/restore-reports/freshness-negative-2026-06-06-130320.md`, produktive Dumps unangetastet.
|
||||||
|
- Gast-/IoT-Netz aktiviert und validiert: FRITZ!Box-Gastzugang `Fritzi Gastzugang` aktiv, Heimnetz-Zugriff aus dem Gastnetz blockiert. LAN- und Host-Preflight gruen; iPhone-Smoke aus dem Gast-WLAN bestaetigt, dass `192.168.178.58:8082`, `:8181`, `:222`, `https://192.168.178.58` und `192.168.178.1` nicht erreichbar sind. Runbook: `docs/GUEST_IOT_NETWORK.md`.
|
||||||
|
- `baerchen` Veeam-Recovery-Test ohne echten Restore abgeschlossen: Recovery-USB `VEEAMRE` bootet, SMB-Ziel `\\kallilabcore\backups\windows-images\baerchen` ist in der Recovery Environment erreichbar, Restore Point wird angezeigt, Test vor echtem Restore abgebrochen. Runbook: `ops/windows-reinstall/docs/windows-image-backup-baseline.md`.
|
||||||
|
- **Operator-Entscheidungen 2026-06-06 abgeschlossen** (Liste damit ohne offene Entscheidungen):
|
||||||
|
- **BitLocker `baerchen`: bewusst deaktiviert.** Recovery laeuft ueber Veeam-Image; kein BitLocker-Key-Management. Restrisiko physischer Diebstahl bewusst akzeptiert.
|
||||||
|
- **Veeam Storage Encryption: bewusst unverschluesselt.** Erster Full-Lauf bleibt; Image liegt auf dem lokalen SMB-Share `\\kallilabcore\backups`. Neu bewerten bei Off-host-Auslagerung des Images.
|
||||||
|
- **Stromverbrauch: bewusst ohne Messung.** Kein Messgeraet; Werte bleiben dauerhaft offen, kein Beschaffungs-Todo mehr.
|
||||||
|
- **Authelia Rest-2FA: KOMPLETT erledigt 2026-06-06.** Catch-all `*.kaleschke.info` -> `two_factor` in Repo **und** Host-Config (chirurgische Einzelzeilen-Aenderung mit Backup, OIDC-Beszel-Client + Secret unangetastet), `docker restart authelia` -> healthy + "Startup complete", Operator-2FA-Login auf einer vorher-1FA-Domain verifiziert. Nebenbei vorbestehenden Drift gefunden+bereinigt (Host-Config war vom 25. Mai, borg/code nie gemerged); Repo-Baseline an Host-Endzustand angeglichen, damit `authelia-diff.sh` clean wird sobald der Host-Mirror nachzieht. Rollback-`.bak` auf dem Host vorhanden.
|
||||||
|
- **Authelia OIDC: angehen** (neuer aktiver Block) — **Gast-/IoT-Netz: einrichten/planen** (neuer aktiver Block) — **Nextcloud 2FA: geparkt** bis OIDC die App-Login-Ebene erreicht.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+127
-4
@@ -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: Node-Eintraege `kallilab-core` (jetzt down) und
|
- **Tailnet-Konsole aufraeumen: ERLEDIGT 2026-06-06** — Node-Eintraege `kallilab-core`
|
||||||
alter Offline-`baerchen` entfernen (trivial, nur tote Geraeteeintraege).
|
und alter Offline-`baerchen` aus der Admin-Konsole entfernt.
|
||||||
- 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
|
||||||
@@ -217,6 +217,7 @@ Bewusst **nicht** freigegeben:
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `80/tcp` | Cloudflare-DNS-Challenge ersetzt HTTP-01; Traefik macht HTTP->HTTPS-Redirect nur LAN-seitig; WAN-`80` waere zusaetzliche Angriffsflaeche ohne Funktionsnutzen. **2026-05-28 in FRITZ!Box-UI entfernt**, Validierung: Mobilfunk-Test ergibt Timeout auf `http://vault.kaleschke.info`, `https://...` weiter erreichbar. |
|
| `80/tcp` | Cloudflare-DNS-Challenge ersetzt HTTP-01; Traefik macht HTTP->HTTPS-Redirect nur LAN-seitig; WAN-`80` waere zusaetzliche Angriffsflaeche ohne Funktionsnutzen. **2026-05-28 in FRITZ!Box-UI entfernt**, Validierung: Mobilfunk-Test ergibt Timeout auf `http://vault.kaleschke.info`, `https://...` weiter erreichbar. |
|
||||||
| `222/tcp` (Gitea SSH) | bewusst Tailscale-only: Operator-Pfad ist Tailscale, GitHub-Mirror deckt DR-Bootstrap ab, Gitea-Bundles sind off-host. Externe SSH-Brute-Force-Vektoren vermeiden. |
|
| `222/tcp` (Gitea SSH) | bewusst Tailscale-only: Operator-Pfad ist Tailscale, GitHub-Mirror deckt DR-Bootstrap ab, Gitea-Bundles sind off-host. Externe SSH-Brute-Force-Vektoren vermeiden. |
|
||||||
|
| `32400/tcp` (Plex) | Plex wird extern ausschliesslich ueber `https://plex.kaleschke.info` via Traefik/443 erreicht. Kein direkter WAN-Port fuer Plex, Plex Remote Access bleibt aus. |
|
||||||
|
|
||||||
### UPnP / Selbstständige Portfreigaben
|
### UPnP / Selbstständige Portfreigaben
|
||||||
|
|
||||||
@@ -245,6 +246,7 @@ Historischer UI-Befund vor Bereinigung vom 2026-05-27 (`Internet -> Freigaben ->
|
|||||||
| 443/tcp | Traefik | HTTPS | WAN-Freigabe in FRITZ!Box erwartet |
|
| 443/tcp | Traefik | HTTPS | WAN-Freigabe in FRITZ!Box erwartet |
|
||||||
| 222/tcp | Gitea SSH | Git SSH | nur LAN/Tailscale; keine WAN-Freigabe |
|
| 222/tcp | Gitea SSH | Git SSH | nur LAN/Tailscale; keine WAN-Freigabe |
|
||||||
| 53/tcp+udp | AdGuard | DNS | LAN-only, dokumentierte Ausnahme |
|
| 53/tcp+udp | AdGuard | DNS | LAN-only, dokumentierte Ausnahme |
|
||||||
|
| 32400/tcp | Plex | Medienserver / Plex Web lokal | LAN/Tailscale direkt; extern nur via Traefik `https://plex.kaleschke.info`, keine WAN-Freigabe fuer 32400 |
|
||||||
| 8082/tcp | AdGuard Admin | Admin UI | Bind nur `100.80.98.33:8082` (Tailscale), nicht im LAN exponiert |
|
| 8082/tcp | AdGuard Admin | Admin UI | Bind nur `100.80.98.33:8082` (Tailscale), nicht im LAN exponiert |
|
||||||
| 8181/tcp | InfluxDB 3 Core | Home Assistant / Ecowitt Writer | 2026-05-31 effektiv nur `127.0.0.1:8181`, nicht LAN-exponiert |
|
| 8181/tcp | InfluxDB 3 Core | Home Assistant / Ecowitt Writer | 2026-05-31 effektiv nur `127.0.0.1:8181`, nicht LAN-exponiert |
|
||||||
|
|
||||||
@@ -261,7 +263,7 @@ docker ps --format "{{.Names}}: {{.Ports}}" | sort
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| LAN | 192.168.178.0/24 | Hauptnetz, Host `192.168.178.58`, FRITZ!Box meldet 35 aktive Geraete |
|
| LAN | 192.168.178.0/24 | Hauptnetz, Host `192.168.178.58`, FRITZ!Box meldet 35 aktive Geraete |
|
||||||
| WLAN 2,4 / 5 GHz | aktiv, SSID `Fritzi` | Standard-WLAN, im LAN-Adressbereich, kein eigener Adressraum |
|
| WLAN 2,4 / 5 GHz | aktiv, SSID `Fritzi` | Standard-WLAN, im LAN-Adressbereich, kein eigener Adressraum |
|
||||||
| Gast-WLAN | **inaktiv** (FRITZ!Box-UI) | Solange inaktiv: kein Gast-Pfad zu LAN-Diensten; AdGuard-Admin-Trennung primaer ueber Tailscale-Bind statt Netzsegmentierung |
|
| Gast-WLAN | aktiv, SSID `Fritzi Gastzugang` | FRITZ!Box-Gastnetz ist vom Heimnetz getrennt; Smoke 2026-06-06 vom iPhone bestaetigt keine Erreichbarkeit der getesteten LAN-/Admin-Ziele |
|
||||||
| IoT-Netz | nicht existent | Keine VLAN-Trennung dokumentiert |
|
| IoT-Netz | nicht existent | Keine VLAN-Trennung dokumentiert |
|
||||||
| Tailscale | aktiv | Operator-Zugang, Host-IP `100.80.98.33` |
|
| Tailscale | aktiv | Operator-Zugang, Host-IP `100.80.98.33` |
|
||||||
| VLANs | nicht in Nutzung | FRITZ!Box 7590 kann VLAN-Tagging an einzelnen LAN-Ports; aktuell nicht konfiguriert |
|
| VLANs | nicht in Nutzung | FRITZ!Box 7590 kann VLAN-Tagging an einzelnen LAN-Ports; aktuell nicht konfiguriert |
|
||||||
@@ -286,6 +288,126 @@ docker network inspect frontend_net | jq '.[0].Containers | keys'
|
|||||||
docker network inspect backend_net | jq '.[0].Internal'
|
docker network inspect backend_net | jq '.[0].Internal'
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## SSH-Konfiguration Host
|
||||||
|
|
||||||
|
Geprueft 2026-06-06 (read-only), **gehaertet 2026-06-07** via `ssh root@192.168.178.58`.
|
||||||
|
|
||||||
|
| Parameter | Ist-Wert (effektiv via `sshd -T`, Stand 2026-06-07) | Soll | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `Port` | `22` | 22 | ok |
|
||||||
|
| `PermitRootLogin` | `prohibit-password` | `prohibit-password` | **gehaertet 2026-06-07** |
|
||||||
|
| `PasswordAuthentication` | `no` | `no` | **gehaertet 2026-06-07** |
|
||||||
|
| `KbdInteractiveAuthentication` | `no` | `no` | **gehaertet 2026-06-07** (noetig wegen `UsePAM yes`) |
|
||||||
|
| `PubkeyAuthentication` | `yes` | `yes` | ok |
|
||||||
|
| `PermitEmptyPasswords` | `no` | `no` | ok |
|
||||||
|
| `AuthorizedKeysFile` | `.ssh/authorized_keys` | `.ssh/authorized_keys` | ok |
|
||||||
|
|
||||||
|
**Hinterlegte SSH-Keys (root):** 3 Keys vorhanden (persistiert unter `/boot/config/ssh/root/authorized_keys`):
|
||||||
|
- `root@Kallilabcore` (Host-eigener Key)
|
||||||
|
- `michi@Baerchen` (Operator-Workstation)
|
||||||
|
- `hetzner-storagebox-maintenance-2026-06-01` (Hetzner-Maintenance-Key)
|
||||||
|
|
||||||
|
**Durchgefuehrte Haertung (2026-06-07):** Root-Login ist jetzt key-only,
|
||||||
|
Passwort- und Keyboard-Interactive-Auth sind serverseitig abgeschaltet.
|
||||||
|
Verifiziert: frischer Key-Login `OK`; `ssh -o PreferredAuthentications=none`
|
||||||
|
meldet `Authentications that can continue: publickey`; reiner Passwort-Versuch
|
||||||
|
`Permission denied (publickey)`.
|
||||||
|
|
||||||
|
**Wichtig — Unraid-Persistenz:** `/etc/ssh/sshd_config` wird beim Boot aus dem
|
||||||
|
OS-Image regeneriert (`rc.sshd`: `cp -f /boot/config/ssh/* /etc/ssh/`, danach
|
||||||
|
`sshd_build`, das nur `Port`/`ListenAddress`/`AddressFamily` setzt). Die
|
||||||
|
Unraid-GUI (**Settings → Management Access → SSH**) bietet nur `Use SSH`/`SSH port`
|
||||||
|
an — **`PermitRootLogin`/`PasswordAuthentication` sind dort nicht einstellbar.**
|
||||||
|
Persistiert wird daher **upgrade-sicher** ueber einen idempotenten Hook:
|
||||||
|
|
||||||
|
- `/boot/config/ssh-harden.sh` — setzt die drei Direktiven idempotent (bestehende
|
||||||
|
aktive Zeile entfernen, genau einmal global vor dem ersten `Match`-Block einfuegen),
|
||||||
|
`sshd -t`-Validierung, Reload nur per `kill -HUP` des Host-`sshd` bei valider Config.
|
||||||
|
Idempotenz belegt: nach mehreren Laeufen je Direktive exakt 1 aktive Zeile, alte
|
||||||
|
`PermitRootLogin yes` entfernt.
|
||||||
|
- `/boot/config/go` — ruft `/bin/bash /boot/config/ssh-harden.sh` bei jedem Boot auf.
|
||||||
|
|
||||||
|
**Selbst-Verifikation (Syslog, rein informativ, keine Reparatur):** Das Skript
|
||||||
|
schreibt nach jedem Lauf die effektiven Auth-Werte (`sshd -T`) nach syslog, z. B.
|
||||||
|
`ssh-harden: VERIFY permitrootlogin prohibit-password pubkeyauthentication yes
|
||||||
|
passwordauthentication no kbdinteractiveauthentication no`. Damit ist nach jedem
|
||||||
|
Boot/Upgrade nachweisbar, ob die Haertung gegriffen hat.
|
||||||
|
|
||||||
|
**Post-Upgrade-/Reboot-Check** (manuell, einmal nach jedem Unraid-Upgrade):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# A) Effektive Werte direkt abfragen (Soll: prohibit-password / no / no / yes)
|
||||||
|
ssh root@192.168.178.58 "sshd -T | grep -Ei 'permitroot|passwordauth|kbdinteractive|pubkey'"
|
||||||
|
# B) Oder die automatische VERIFY-Zeile im Syslog lesen (Unraid nutzt rsyslog -> /var/log/syslog, nicht logread)
|
||||||
|
ssh root@192.168.178.58 "grep 'ssh-harden' /var/log/syslog | tail -3"
|
||||||
|
```
|
||||||
|
|
||||||
|
Dieser Weg editiert die **jeweils aktuelle** von Unraid generierte Config nach und
|
||||||
|
ueberlebt damit Unraid-Upgrades; findet er die Stock-Zeile nicht (z. B. weil eine
|
||||||
|
neue Version schon `prohibit-password` ausliefert), macht der `sed` nichts und
|
||||||
|
bricht den Boot nicht (fail-safe Richtung offen, nicht ausgesperrt). Bewusst
|
||||||
|
**nicht** der oft empfohlene Weg einer kompletten `/boot/config/ssh/sshd_config`
|
||||||
|
auf Flash — der wuerde die Stock-Config einfrieren und beim Upgrade neue Defaults
|
||||||
|
verschlucken.
|
||||||
|
|
||||||
|
**Rollback:** `go`-Block + `/boot/config/ssh-harden.sh` entfernen, dann
|
||||||
|
`cp /boot/config/ssh-harden.sshd_config.bak-20260607 /etc/ssh/sshd_config` und
|
||||||
|
`kill -HUP $(cat /var/run/sshd.pid)`. Notzugang ueber Unraid-Konsole/GUI bleibt.
|
||||||
|
|
||||||
|
**Abgrenzung:** Ein zweiter `sshd` (`-D -e`) laeuft in einem Docker-Container
|
||||||
|
(s6-overlay, moby-Namespace) und bindet **nicht** den Host-`:22`; eigene Config
|
||||||
|
im Container, von dieser Haertung unberuehrt.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Post-Upgrade Posture-Recheck — Unraid 7.3.1 (2026-06-07)
|
||||||
|
|
||||||
|
Nach dem Major-Upgrade **7.2.4 → 7.3.1** read-only die Host-Listener-Landschaft
|
||||||
|
(`ss -tlnp`) gegen die dokumentierten Annahmen geprueft.
|
||||||
|
|
||||||
|
**Dokumentierte Ausnahmen verifiziert (alle weiterhin gueltig):**
|
||||||
|
|
||||||
|
| Dienst | Soll | Ist nach 7.3.1 | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| InfluxDB 3 | nur `127.0.0.1:8181` | `127.0.0.1:8181` | ✅ |
|
||||||
|
| AdGuard-Admin | nur Tailscale `100.80.98.33:8082` | `100.80.98.33:8082` | ✅ |
|
||||||
|
| Gitea-SSH `222` | LAN/Tailscale, keine WAN-Freigabe | `0.0.0.0:222` (LAN/TS), WAN am Router zu | ✅ |
|
||||||
|
| Traefik `80/443` | einziger Owner | docker-proxy (Traefik) allein | ✅ |
|
||||||
|
| libvirt `:53` | darf nicht existieren | **weg** (Fix vom 2026-06-07 haelt) | ✅ |
|
||||||
|
|
||||||
|
**Docker-Socket (`/var/run/docker.sock`) — C-3-Kontext:**
|
||||||
|
|
||||||
|
| Container | Mount | Bewertung |
|
||||||
|
|---|---|---|
|
||||||
|
| komodo-periphery | **RW** | dokumentierte Ausnahme (Periphery startet/stoppt Container) |
|
||||||
|
| traefik | ro | C-3: Direkt-Mount (ro), nicht ueber Socket-Proxy — offener Audit-Punkt, kein Regress |
|
||||||
|
| glances / monitoring-promtail / glance-docker-socket-proxy | ro | unkritisch |
|
||||||
|
|
||||||
|
Keine neue RW-Socket-Exposure durch das Upgrade.
|
||||||
|
|
||||||
|
**Vorfall-Notiz AdGuard/DNS (Boot-Race, behoben 2026-06-07):** Das Upgrade hatte das
|
||||||
|
ungenutzte **libvirt-Default-Netz** auf Autostart gebracht; dessen `dnsmasq` belegte
|
||||||
|
beim Boot Port `53` **vor** AdGuard → AdGuards erster Start scheiterte am Bind und
|
||||||
|
liess den Container ohne Netz-Anbindung (`Networks={}`, keine Ports) zurueck. Fix:
|
||||||
|
`virsh net-autostart default --disable` + `virsh net-destroy default` (kein VM
|
||||||
|
betroffen, Liste leer) + AdGuard-Container aus der Compose `--force-recreate`
|
||||||
|
(re-attach `dns_net`, `:53` neu veroeffentlicht). DNS danach verifiziert aufloesend.
|
||||||
|
`libvirtd` laeuft weiter nur auf `127.0.0.1:16509`.
|
||||||
|
|
||||||
|
**Empfehlung (Dauerfix):** Da keine VMs genutzt werden, **Unraid VM Manager → Enable
|
||||||
|
VMs = No** — dann startet `libvirtd` gar nicht und der `:53`-Konflikt kann prinzipiell
|
||||||
|
nicht wiederkehren. Bis dahin verhindert der abgeschaltete Autostart die Wiederkehr.
|
||||||
|
|
||||||
|
**Beobachtungen (kein Regress, Inventar):** SMB (`:445/:139`) und Plex (`*:32400`)
|
||||||
|
lauschen auch auf der Tailscale-IP; durch die seit 2026-06-06 tag-restriktive
|
||||||
|
Tailnet-ACL akzeptabel.
|
||||||
|
|
||||||
|
**SSH-Haertung nach Upgrade:** key-only root unveraendert aktiv und verifiziert
|
||||||
|
(`prohibit-password`/`password no`/`kbd no`), go-Hook genau 1× gefeuert — siehe
|
||||||
|
Abschnitt „SSH-Konfiguration Host".
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Offene Entscheidungen
|
## Offene Entscheidungen
|
||||||
|
|
||||||
| Thema | Status | Naechster Schritt |
|
| Thema | Status | Naechster Schritt |
|
||||||
@@ -294,7 +416,8 @@ docker network inspect backend_net | jq '.[0].Internal'
|
|||||||
| FRITZ!Box-Portfreigaben mit Repo-Soll abgleichen | **erledigt 2026-06-01** | Bereinigt: `80/tcp` entfernt (Cloudflare-DNS-Challenge ersetzt HTTP-01; Mobilfunk-Test bestaetigt Timeout auf `http://`, `https://` weiter ok). `222/tcp` bleibt bewusst nicht eingerichtet (Tailscale-only-Linie). UPnP-Selbstfreigaben sind aus. Aktiver Soll-Stand: ausschliesslich `443/tcp -> 192.168.178.58`. |
|
| FRITZ!Box-Portfreigaben mit Repo-Soll abgleichen | **erledigt 2026-06-01** | Bereinigt: `80/tcp` entfernt (Cloudflare-DNS-Challenge ersetzt HTTP-01; Mobilfunk-Test bestaetigt Timeout auf `http://`, `https://` weiter ok). `222/tcp` bleibt bewusst nicht eingerichtet (Tailscale-only-Linie). UPnP-Selbstfreigaben sind aus. Aktiver Soll-Stand: ausschliesslich `443/tcp -> 192.168.178.58`. |
|
||||||
| FRITZ!Box-Dienste aus dem Internet | **erledigt 2026-06-01** | `Internet -> Freigaben -> FRITZ!Box-Dienste`: HTTPS-Zugriff auf die FRITZ!Box aus dem Internet aus; FTP/FTPS auf Speichermedien aus. |
|
| FRITZ!Box-Dienste aus dem Internet | **erledigt 2026-06-01** | `Internet -> Freigaben -> FRITZ!Box-Dienste`: HTTPS-Zugriff auf die FRITZ!Box aus dem Internet aus; FTP/FTPS auf Speichermedien aus. |
|
||||||
| FRITZ!OS Update und Konfig-Backup | **erledigt 2026-06-01** | TR-064 meldet `154.08.25`; Konfig-Export liegt extern/off-system in Vaultwarden, Kennwort und Datei bleiben ausserhalb des Repos. |
|
| FRITZ!OS Update und Konfig-Backup | **erledigt 2026-06-01** | TR-064 meldet `154.08.25`; Konfig-Export liegt extern/off-system in Vaultwarden, Kennwort und Datei bleiben ausserhalb des Repos. |
|
||||||
| Gast-/IoT-Zugriff auf Admin-Ports | **Entscheidungspunkt: kein Gast-/IoT-Netz aktivieren, solange nicht gebraucht** | Aktuell entschaerft, weil Gast-WLAN inaktiv ist und kein IoT-VLAN existiert. Risiko entsteht erst bei Aktivierung. Harte Vorbedingung fuer eine spaetere Aktivierung: **vor** dem Einschalten von Gast-WLAN/IoT muessen `192.168.178.58:8082` (AdGuard-Admin, ohnehin Tailscale-gebunden), `192.168.178.58:8181` (InfluxDB, bereits `127.0.0.1`-bound) und alle weiteren LAN-Admin-Ports per FRITZ!Box-Netzwerkfilter/Kindersicherung gegen das Gastsegment gesperrt sein. Bis dahin bewusst kein Gastnetz. |
|
| Gast-/IoT-Zugriff auf Admin-Ports | **validiert 2026-06-06** | Runbook `docs/GUEST_IOT_NETWORK.md` und Checks `ops/maintenance/check-guest-iot-isolation.ps1` sowie `ops/maintenance/check-guest-iot-preflight.sh` vorhanden. LAN-Preflight von `baerchen` gruen: `192.168.178.58:8082` und `:8181` blockiert. Host-Preflight auf Unraid gruen, Report `/mnt/user/backups/restore-reports/guest-iot-preflight-2026-06-06-131316.md`. Gast-WLAN-Smoke per iPhone: `192.168.178.58:8082`, `:8181`, `:222`, `https://192.168.178.58` und `192.168.178.1` nicht erreichbar. |
|
||||||
| IPv6 Exposure | technisch und per UI entschaerft | Public DNS liefert keine AAAA-Records fuer `*.kaleschke.info`; Host hat keine globale Provider-IPv6. TR-064 meldet IPv6-Firewall aktiv und Pinholes grundsaetzlich erlaubt; FRITZ!Box-UI zeigt keine aktiven IPv6-Freigaben, keine Admin-/SSH-Freigaben. |
|
| IPv6 Exposure | technisch und per UI entschaerft | Public DNS liefert keine AAAA-Records fuer `*.kaleschke.info`; Host hat keine globale Provider-IPv6. TR-064 meldet IPv6-Firewall aktiv und Pinholes grundsaetzlich erlaubt; FRITZ!Box-UI zeigt keine aktiven IPv6-Freigaben, keine Admin-/SSH-Freigaben. |
|
||||||
| WAN-Ausfallschutz | **geparkt: spaeter evaluieren** (Operator-Entscheidung 2026-06-05) | Mobilfunk-Stick-Failover an FRITZ!Box bleibt vorerst inaktiv. Folgen sind bewusst akzeptiert: Internet-Ausfall = ACME/DDNS pausieren, lokale Apps laufen weiter. Review-Trigger: haeufigere oder laengere DSL-Ausfaelle, oder wenn externer Remote-Zugang (statt nur lokalem Betrieb) geschaeftskritisch wird. Erst dann Mobilfunk-Failover technisch bewerten. |
|
| WAN-Ausfallschutz | **geparkt: spaeter evaluieren** (Operator-Entscheidung 2026-06-05) | Mobilfunk-Stick-Failover an FRITZ!Box bleibt vorerst inaktiv. Folgen sind bewusst akzeptiert: Internet-Ausfall = ACME/DDNS pausieren, lokale Apps laufen weiter. Review-Trigger: haeufigere oder laengere DSL-Ausfaelle, oder wenn externer Remote-Zugang (statt nur lokalem Betrieb) geschaeftskritisch wird. Erst dann Mobilfunk-Failover technisch bewerten. |
|
||||||
| Home Assistant InfluxDB Bind | validiert 2026-05-31 | `docker-proxy` bindet `127.0.0.1:8181`; keine LAN-Exposure. Wenn Home Assistant nicht lokal auf dem Host schreibt, braucht das eine bewusste Bind-Aenderung. |
|
| Home Assistant InfluxDB Bind | validiert 2026-05-31 | `docker-proxy` bindet `127.0.0.1:8181`; keine LAN-Exposure. Wenn Home Assistant nicht lokal auf dem Host schreibt, braucht das eine bewusste Bind-Aenderung. |
|
||||||
|
| SSH-Haertung Host | **erledigt 2026-06-07** | Root-Login key-only: `PermitRootLogin prohibit-password`, `PasswordAuthentication no`, `KbdInteractiveAuthentication no`. Live gesetzt + verifiziert (Key-Login ok, Passwort-Auth abgelehnt). Persistenz upgrade-sicher ueber `/boot/config/ssh-harden.sh` (idempotent, `sshd -t` vor Reload) aufgerufen aus `/boot/config/go`. GUI bietet diese Optionen nicht. Details im Abschnitt „SSH-Konfiguration Host". |
|
||||||
|
|||||||
@@ -31,8 +31,10 @@ Diese Datei trennt aktive Betriebsdokumentation von historischer Arbeitsdoku. Ne
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `STORAGE_LAYOUT.md` | verbindliche Storage-/Share-/Pfad-Regeln |
|
| `STORAGE_LAYOUT.md` | verbindliche Storage-/Share-/Pfad-Regeln |
|
||||||
| `SECRETS_MAP.md` | Secret-Namen, Speicherorte und Einbindungsarten ohne Werte |
|
| `SECRETS_MAP.md` | Secret-Namen, Speicherorte und Einbindungsarten ohne Werte |
|
||||||
|
| `AUTHELIA_OIDC_PLAN.md` | Plan & Runbook fuer app-uebergreifendes SSO via Authelia OIDC |
|
||||||
| `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART-, USV- und Power-Baseline |
|
| `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART-, USV- und Power-Baseline |
|
||||||
| `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen |
|
| `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen |
|
||||||
|
| `GUEST_IOT_NETWORK.md` | Sicherer Ablauf fuer FRITZ!Box-Gastnetz / IoT-Isolation |
|
||||||
| `EXTERNAL_DEPENDENCIES.md` | Provider, Konten und externe Abhaengigkeiten |
|
| `EXTERNAL_DEPENDENCIES.md` | Provider, Konten und externe Abhaengigkeiten |
|
||||||
| `EXTERNAL_OPERATOR_RUNBOOK.md` | Hetzner-/Borg-/FRITZ!Box-Betreibercheck |
|
| `EXTERNAL_OPERATOR_RUNBOOK.md` | Hetzner-/Borg-/FRITZ!Box-Betreibercheck |
|
||||||
| `CAPACITY_AND_LIFECYCLE.md` | Kapazitaet, Wachstum und Upgrade-Trigger |
|
| `CAPACITY_AND_LIFECYCLE.md` | Kapazitaet, Wachstum und Upgrade-Trigger |
|
||||||
|
|||||||
+9
-1
@@ -93,7 +93,15 @@ Script: bash /mnt/user/services/homelab-infra/ops/renovate/run-renovate.sh
|
|||||||
| Schedule | `extends ["schedule:weekly"]` | Renovate-Engine prueft, aber PRs/Updates folgen Wochen-Profilen wo sinnvoll |
|
| Schedule | `extends ["schedule:weekly"]` | Renovate-Engine prueft, aber PRs/Updates folgen Wochen-Profilen wo sinnvoll |
|
||||||
| Dependency Dashboard | aktiv | Gitea-Issue, die alle ausstehenden Updates auflistet |
|
| Dependency Dashboard | aktiv | Gitea-Issue, die alle ausstehenden Updates auflistet |
|
||||||
| Onboarding-PR | `onboarding: false` | Keine `Configure Renovate`-Onboarding-PR; wir nutzen die Repo-`renovate.json` direkt |
|
| Onboarding-PR | `onboarding: false` | Keine `Configure Renovate`-Onboarding-PR; wir nutzen die Repo-`renovate.json` direkt |
|
||||||
| Ignore-Pfade | `_archive`, `ops/grafana-influxdb`, `ops/loki` | Renovate scant alte/abgeloeste Stacks nicht |
|
| Ignore-Pfade | `_archive`, `ops/grafana-influxdb`, `ops/loki`, `ops/komodo` | Renovate scant alte/abgeloeste Stacks nicht; `ops/komodo` ist bewusst raus (siehe unten) |
|
||||||
|
|
||||||
|
## Ausnahme: komodo-Stack ist inline-verwaltet, nicht git-deployed
|
||||||
|
|
||||||
|
Der `komodo`-Stack (Komodo-Core/Mongo/Periphery, Datei `ops/komodo/docker-compose.yml`) wird **nicht aus diesem Repo deployed**. In Komodo ist der Stack als **inline `file_contents`** (UI-defined) gespeichert (`repo` leer, `files_on_host=false`, `has_inline_file_contents=true`) und hat bewusst `webhook_enabled=false`, damit Komodo sich nicht selbst per Webhook recreated (Bootstrap-/Henne-Ei-Fall).
|
||||||
|
|
||||||
|
Konsequenz: Ein Renovate-PR auf `ops/komodo/docker-compose.yml` wirkt zur Laufzeit **nicht** (Komodo deployt aus seiner Inline-Definition) und erzeugt nur Git↔Komodo-Scheinsicherheit. Deshalb steht `ops/komodo/**` in `ignorePaths`. Die Repo-Datei bleibt als Doku/Spiegel und traegt den aktuell real laufenden Digest.
|
||||||
|
|
||||||
|
Befund-Datum 2026-06-10: Renovate-PR #13 (mongo-8.0.23 Digest-Refresh) wurde gemergt, wirkte aber nicht; der Digest wurde im Repo auf den laufenden Stand zurueckgesetzt und der Pfad ausgenommen. Echte Updates des komodo-Stacks laufen bis auf Weiteres manuell ueber Komodo (Inline-Compose anpassen) bzw. spaeter via Migration auf git-backed (eigener Aenderungsblock).
|
||||||
|
|
||||||
## Aktueller Betriebsstand
|
## Aktueller Betriebsstand
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ Details gilt immer die betroffene Compose-Datei oder das jeweilige Runbook.
|
|||||||
| `docs/GITOPS_DRIFT_RUNBOOK.md` | Git/Gitea/Komodo/Docker/Host-Drift |
|
| `docs/GITOPS_DRIFT_RUNBOOK.md` | Git/Gitea/Komodo/Docker/Host-Drift |
|
||||||
| `docs/AUDIT_2026-05-25_TODO.md` | aktuelle Restliste |
|
| `docs/AUDIT_2026-05-25_TODO.md` | aktuelle Restliste |
|
||||||
| `docs/DR_WORKSTATION_SETUP.md` | Schritt-fuer-Schritt-Runbook fuer den DR-Gaming-PC (WSL2 + Borg-Client + SSH-Keys) |
|
| `docs/DR_WORKSTATION_SETUP.md` | Schritt-fuer-Schritt-Runbook fuer den DR-Gaming-PC (WSL2 + Borg-Client + SSH-Keys) |
|
||||||
|
| `docs/runbooks/komodo-bulk-deploy-dns.md` | Bulk-Deploy-Pulls scheitern an DNS, wenn AdGuard im selben Batch recreated wird |
|
||||||
|
|
||||||
## Wichtige Skripte
|
## Wichtige Skripte
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ Sie ist die fachliche Ergaenzung zu `docs/DISASTER_RECOVERY.md`.
|
|||||||
|
|
||||||
| System | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
| System | Fuehrende Quelle | Datei-Restore | Dump / DB | Secrets / ENV | Abhaengigkeiten | Smoke-Test |
|
||||||
|---|---|---|---|---|---|---|
|
|---|---|---|---|---|---|---|
|
||||||
| `baerchen` Windows 11 | Veeam Agent Image auf Unraid-SMB | `/mnt/user/backups/windows-images/baerchen/` bzw. `\\kallilabcore\backups\windows-images\baerchen` | Veeam Restore Points im Zielordner; erster Full-Lauf 2026-06-05, GUI-Groesse 53,8 GB, Dauer 0:11:31, MetaCheck 0 Fehler/0 Warnungen | SMB-User `micha`; Veeam Job Encryption Password nur noetig, falls Storage Encryption spaeter aktiviert wird; BitLocker-Recovery-Key erst noetig, wenn BitLocker spaeter aktiviert wird | Veeam Recovery USB `VEEAMRE`, SMB auf `kallilabcore`, AdGuard/DNS oder direkte IP | Von `VEEAMRE` booten, SMB-Ziel oeffnen, Restore Point anzeigen, vor echtem Restore abbrechen; Runbook `ops/windows-reinstall/docs/windows-image-backup-baseline.md` |
|
| `baerchen` Windows 11 | Veeam Agent Image auf Unraid-SMB | `/mnt/user/backups/windows-images/baerchen/` bzw. `\\kallilabcore\backups\windows-images\baerchen` | Veeam Restore Points im Zielordner; erster Full-Lauf 2026-06-05, GUI-Groesse 53,8 GB, Dauer 0:11:31, MetaCheck 0 Fehler/0 Warnungen | SMB-User `micha`; Veeam Job Encryption Password nur noetig, falls Storage Encryption spaeter aktiviert wird; BitLocker-Recovery-Key erst noetig, wenn BitLocker spaeter aktiviert wird | Veeam Recovery USB `VEEAMRE`, SMB auf `kallilabcore`, AdGuard/DNS oder direkte IP | Recovery-Test am 2026-06-06 erfolgreich: USB-Boot, SMB-Ziel erreichbar, Restore Point sichtbar, vor echtem Restore abgebrochen; Runbook `ops/windows-reinstall/docs/windows-image-backup-baseline.md` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -159,7 +159,7 @@ Stand 2026-06-06. Pro Dienst auf einen Blick: Wurde der Restore schon einmal rea
|
|||||||
| ntfy | 2 | - | rebuildbar, kein Test noetig | - |
|
| ntfy | 2 | - | rebuildbar, kein Test noetig | - |
|
||||||
| Borg UI | 3 | - | rebuildbar | - |
|
| Borg UI | 3 | - | rebuildbar | - |
|
||||||
| Filebrowser | 3 | - | rebuildbar | - |
|
| Filebrowser | 3 | - | rebuildbar | - |
|
||||||
| baerchen Windows Image | Workstation | 2026-06-05 | Full-Backup geschrieben; Recovery USB + SMB-Mount noch offen | Recovery-USB-Test |
|
| baerchen Windows Image | Workstation | 2026-06-06 | Full-Backup geschrieben; Recovery-USB-Boot, SMB-Mount und Restore-Point-Sichtpruefung erfolgreich; vor echtem Restore abgebrochen | nach Image-Aenderungen oder quartalsweise |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -53,6 +53,8 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | aktiv |
|
| InfluxDB 3 Core | Admin Token JSON | `/mnt/user/appdata/secrets/influxdb3_admin_token.json` -> Docker Secret `/run/secrets/influxdb3_admin_token` | aktiv |
|
||||||
| Monitoring Grafana | Admin Password | `/mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt` -> Docker Secret `/run/secrets/monitoring_grafana_admin_password` -> `GF_SECURITY_ADMIN_PASSWORD__FILE` | aktiv |
|
| Monitoring Grafana | Admin Password | `/mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt` -> Docker Secret `/run/secrets/monitoring_grafana_admin_password` -> `GF_SECURITY_ADMIN_PASSWORD__FILE` | aktiv |
|
||||||
| Monitoring Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/monitoring_grafana_influxdb_token` | aktiv |
|
| Monitoring Grafana -> InfluxDB | Datasource Token | `/mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt` -> Docker Secret `/run/secrets/monitoring_grafana_influxdb_token` | aktiv |
|
||||||
|
| Grafana OIDC (Authelia) | Client Secret | `/mnt/user/appdata/secrets/grafana_oidc_client_secret` (Klartext, chmod 600) -> Docker Secret `/run/secrets/grafana_oidc_client_secret` -> `GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET__FILE`. Zugehoeriger pbkdf2-Hash liegt im Authelia-Host-Config-Client `grafana` (kein Wert im Repo) | aktiv (2026-06-06) |
|
||||||
|
| Mealie OIDC (Authelia) | Client Secret | Stack-ENV `${MEALIE_OIDC_CLIENT_SECRET}` in `/mnt/user/services/stacks/mealie/apps/mealie/.env` (Komodo-Stack-ENV); pbkdf2-Hash im Authelia-Host-Config-Client `mealie` (kein Wert im Repo) | aktiv (2026-06-06) |
|
||||||
| Renovate Bot | Gitea Service-Account PAT | `/mnt/user/appdata/secrets/renovate_token.txt` -> Host-Datei (chmod 600), gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_TOKEN` weitergegeben | aktiv nach Operator-Setup (siehe `docs/RENOVATE.md`) |
|
| Renovate Bot | Gitea Service-Account PAT | `/mnt/user/appdata/secrets/renovate_token.txt` -> Host-Datei (chmod 600), gelesen von `ops/renovate/run-renovate.sh` und an Renovate-Container als `RENOVATE_TOKEN` weitergegeben | aktiv nach Operator-Setup (siehe `docs/RENOVATE.md`) |
|
||||||
| n8n | Encryption Key fuer interne Credential-Verschluesselung | `/mnt/user/appdata/secrets/n8n_encryption_key.txt` (chmod 600) -> Komodo Stack ENV `${N8N_ENCRYPTION_KEY}`; kein `_FILE`-Support im Upstream-Image | aktiv |
|
| n8n | Encryption Key fuer interne Credential-Verschluesselung | `/mnt/user/appdata/secrets/n8n_encryption_key.txt` (chmod 600) -> Komodo Stack ENV `${N8N_ENCRYPTION_KEY}`; kein `_FILE`-Support im Upstream-Image | aktiv |
|
||||||
| n8n | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv |
|
| n8n | GMX IMAP Login (Mail-Trigger Workflow) | n8n Credentials Store (Typ `imap`), nur in `/mnt/user/appdata/n8n/data` mit `N8N_ENCRYPTION_KEY` verschluesselt | aktiv |
|
||||||
@@ -60,7 +62,7 @@ Dieses Dokument listet sensible Daten, deren Ablageorte und die vorgesehene Einb
|
|||||||
| n8n | Gitea PAT fuer `n8n-bot` (Issue-Erstellung Workflow) | n8n Credentials Store (Typ `httpHeaderAuth`, Header `Authorization: token ...`); separater Bot-User mit Scope `write:issue` auf `Micha/mails` | aktiv |
|
| n8n | Gitea PAT fuer `n8n-bot` (Issue-Erstellung Workflow) | n8n Credentials Store (Typ `httpHeaderAuth`, Header `Authorization: token ...`); separater Bot-User mit Scope `write:issue` auf `Micha/mails` | aktiv |
|
||||||
| baerchen Veeam | Veeam Job Encryption Password | Vaultwarden Secure Note `Veeam baerchen backup encryption password`; kein Datei-Secret im Repo | geplant, nur noetig falls Veeam Storage Encryption aktiviert wird |
|
| baerchen Veeam | Veeam Job Encryption Password | Vaultwarden Secure Note `Veeam baerchen backup encryption password`; kein Datei-Secret im Repo | geplant, nur noetig falls Veeam Storage Encryption aktiviert wird |
|
||||||
| baerchen SMB Backup Target | SMB Credential fuer User `micha` | bestehender Unraid-/Vaultwarden-Zugang fuer Share `backups`; wird im Veeam-Job gespeichert, Wert nie dokumentieren | aktiv |
|
| baerchen SMB Backup Target | SMB Credential fuer User `micha` | bestehender Unraid-/Vaultwarden-Zugang fuer Share `backups`; wird im Veeam-Job gespeichert, Wert nie dokumentieren | aktiv |
|
||||||
| baerchen BitLocker | BitLocker Recovery Key C: | geplant: `D:\30_Finanzen\BitLocker-RecoveryKey-baerchen-<DATUM>.txt` + Vaultwarden Secure Note + physischer Ausdruck; aktuell BitLocker noch nicht aktiv | geplant |
|
| baerchen BitLocker | BitLocker Recovery Key C: | **bewusst deaktiviert (Entscheidung 2026-06-06):** kein BitLocker, kein Recovery-Key noetig. Falls spaeter aktiviert: Key nach `D:\30_Finanzen\BitLocker-RecoveryKey-baerchen-<DATUM>.txt` + Vaultwarden Secure Note + physischer Ausdruck | nicht aktiv (bewusst) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Secret-Werte sind nicht enthalten. Es werden nur Secret-Namen, Env-Key-Namen und
|
|||||||
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
| `nextcloud` | Datei-/Cloud-Dienst | `apps/nextcloud/docker-compose.yml` | `https://cloud.kaleschke.info` | eigene PostgreSQL, eigene Redis, Traefik | `/mnt/user/appdata/nextcloud/html`, `/mnt/user/documents/nextcloud-data` | Tier 2, `nextcloud.dump` + Share | ja | native App-Auth ohne zentrale ForwardAuth; WebDAV/CardDAV beachten |
|
||||||
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
| `nextcloud-postgres` | Nextcloud-Datenbank | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/postgres18`, archivierter Rollback-Altstand `/mnt/user/appdata/_archive/pg18-immich-rollback-volumes-20260602/nextcloud-postgres17`, `nextcloud_postgres_password.txt` | `nextcloud.dump`, raw DB nicht primaerer Restore-Weg | nein | interne DB; PostgreSQL 18 |
|
||||||
| `nextcloud-redis` | Nextcloud Cache/Locking | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/redis` | Teil von Nextcloud-Restore | nein | interne Redis 8.8 |
|
| `nextcloud-redis` | Nextcloud Cache/Locking | `apps/nextcloud/docker-compose.yml` | intern | `nextcloud_internal` | `/mnt/user/appdata/nextcloud/redis` | Teil von Nextcloud-Restore | nein | interne Redis 8.8 |
|
||||||
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml` | Plex native, **LAN/Tailscale-only**, Remote Access deaktiviert | Host-Netz | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | nein | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme. Server geclaimt von `Xeridos` (Reclaim 2026-05-28 nach Preferences-Reset vom 18.05.). Smart-TVs greifen ueber WLAN-LAN per mDNS/Plex-GDM direkt zu. `PublishServerOnPlexOnlineKey=0` (Remote Access aus), `RelayEnabled` ebenfalls aus. |
|
| `plex` | Medienserver mit LAN-/Client-Discovery | `host-services/plex/docker-compose.yml`, `traefik/dynamic/plex.yml` | `https://plex.kaleschke.info`, LAN `http://192.168.178.58:32400/web`, Remote Access deaktiviert | Host-Netz, Traefik File provider | `/mnt/user/appdata/plex/config`, `/mnt/user/appdata/plex/transcode`, `/mnt/user/media`, `/mnt/user/photos` | Tier 2, Appdata + Medienpfade im Borg-/Share-Scope | ja, native Plex-Auth | Repo-Compose-Stack; `network_mode: host` bleibt dokumentierte Discovery-Ausnahme. Traefik routet via File-Provider-Ausnahme auf `http://192.168.178.58:32400`, weil Docker-Labels Host-Netz-Container aus Traefik heraus auf `127.0.0.1` routen wuerden. Keine FRITZ!Box-Freigabe fuer `32400`. Keine Authelia-ForwardAuth, weil Plex Web/App-Clients native Plex-Auth und eigene Flows nutzen. Server geclaimt von `Xeridos`; Smart-TVs greifen weiter ueber WLAN-LAN per mDNS/Plex-GDM direkt zu. `PublishServerOnPlexOnlineKey=0` (Plex Remote Access aus), `RelayEnabled` ebenfalls aus. |
|
||||||
| `ntfy` | Push-Benachrichtigungen | `apps/ntfy/docker-compose.yml` | `https://ntfy.kaleschke.info` | Traefik, upstream mobile push | `/mnt/user/appdata/ntfy` | Tier 2 | ja | `NTFY_BEHIND_PROXY=true`; Problem-Alerts gehen gebuendelt an `homelab-alerts`, optionale Erfolgsmeldungen an `homelab-info` |
|
| `ntfy` | Push-Benachrichtigungen | `apps/ntfy/docker-compose.yml` | `https://ntfy.kaleschke.info` | Traefik, upstream mobile push | `/mnt/user/appdata/ntfy` | Tier 2 | ja | `NTFY_BEHIND_PROXY=true`; Problem-Alerts gehen gebuendelt an `homelab-alerts`, optionale Erfolgsmeldungen an `homelab-info` |
|
||||||
| `bentopdf` | PDF-Tooling / Ersatz fuer Stirling-PDF | `apps/bentopdf/docker-compose.yml` | `https://pdf.kaleschke.info` | Traefik + Authelia | keine kritische Persistenz im Compose | Tier 3, rebuildbar | ja + Authelia | COOP/COEP per Middleware. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv als situatives Tool, auch wenn aktuell keine Traefik-Zugriffe in der Woche. Resource-Footprint vernachlaessigbar (~4 MB RAM). |
|
| `bentopdf` | PDF-Tooling / Ersatz fuer Stirling-PDF | `apps/bentopdf/docker-compose.yml` | `https://pdf.kaleschke.info` | Traefik + Authelia | keine kritische Persistenz im Compose | Tier 3, rebuildbar | ja + Authelia | COOP/COEP per Middleware. **Behalten-Entscheidung 2026-05-28:** Container bleibt aktiv als situatives Tool, auch wenn aktuell keine Traefik-Zugriffe in der Woche. Resource-Footprint vernachlaessigbar (~4 MB RAM). |
|
||||||
| `super-productivity` | Persoenliche Produktivitaets-/Task-PWA (Operator), konsumiert Gitea-Issues aus `Micha/mails` | `apps/super-productivity/docker-compose.yml` | `https://sp.kaleschke.info` | Traefik + Authelia, Gitea `Micha/mails` (Polling vom Client) | statisches Frontend, kein Server-State; Browser-IndexedDB plus optionaler WebDAV-Sync gegen Nextcloud | Tier 3, rebuildbar | ja + Authelia | Reine Static-PWA; SP synchronisiert client-seitig ueber Gitea-API (Scope `assigned`, Repo `Micha/mails`, User `Micha`). |
|
| `super-productivity` | Persoenliche Produktivitaets-/Task-PWA (Operator), konsumiert Gitea-Issues aus `Micha/mails` | `apps/super-productivity/docker-compose.yml` | `https://sp.kaleschke.info` | Traefik + Authelia, Gitea `Micha/mails` (Polling vom Client) | statisches Frontend, kein Server-State; Browser-IndexedDB plus optionaler WebDAV-Sync gegen Nextcloud | Tier 3, rebuildbar | ja + Authelia | Reine Static-PWA; SP synchronisiert client-seitig ueber Gitea-API (Scope `assigned`, Repo `Micha/mails`, User `Micha`). |
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
# Homelab-Optimierung — Assessment 2026-06-10
|
||||||
|
|
||||||
|
Read-only-Analyse des Repos (Stand `master`, lokale Arbeitskopie 2026-06-10).
|
||||||
|
Keine produktiven Änderungen durchgeführt. Alle Empfehlungen sind Vorschläge
|
||||||
|
mit Rollback-Plan; nichts wurde deployed.
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Das KalliLab-CORE-Homelab ist für ein Ein-Host-Setup ungewöhnlich reif:
|
||||||
|
GitOps mit Gitea+Komodo, sauberes Netzmodell (frontend/backend/app-intern),
|
||||||
|
Authelia mit 2FA-Catch-all, belegte Restore-Drills für alle Tier-1/2-Dienste,
|
||||||
|
Off-site-Borg nach Hetzner, DR-Workstation-Kit, Monitoring mit Prometheus/
|
||||||
|
Loki/Grafana/Alertmanager→ntfy. Die Doku-Disziplin ist das eigentliche Asset.
|
||||||
|
|
||||||
|
Die größten realen Lücken liegen nicht in der Architektur, sondern in der
|
||||||
|
**Container-Betriebsebene**: 20 von 30 Stacks haben keinen Healthcheck, kein
|
||||||
|
einziger Container hat Memory-/CPU-Limits, und mehrere Images laufen auf
|
||||||
|
mutablen Tags (`release`, `latest`, `:2`), bei denen Renovate-Digest-Bumps
|
||||||
|
faktisch unkontrollierte Versionssprünge sind — am kritischsten bei Immich.
|
||||||
|
Dazu kommen zwei strukturelle Risiken: **AdGuard ist DNS-SPOF ohne Fallback**
|
||||||
|
(hat bereits einen Teil-Deploy-Ausfall verursacht) und **Borg-Backups sind
|
||||||
|
vom Host aus löschbar** (append-only bewusst abgelehnt, aber die kostenlose
|
||||||
|
Kompensation — Hetzner-Storage-Box-Snapshots — ist nicht aktiviert).
|
||||||
|
|
||||||
|
## Gesamtbewertung
|
||||||
|
|
||||||
|
| Bereich | Note | Begründung |
|
||||||
|
|---|---|---|
|
||||||
|
| Architektur | **sehr gut** | klares Netzmodell, dokumentierte Ausnahmen, ein Ingress, Compose-first konsequent |
|
||||||
|
| Netzwerk/DNS/Proxy | **gut, ein SPOF** | Traefik v3 labelbasiert sauber; AdGuard+Unbound ohne zweiten Resolver — bekannter Vorfall (Bulk-Deploy-DNS-Ausfall, `docs/runbooks/komodo-bulk-deploy-dns.md`) |
|
||||||
|
| Container-Betrieb | **mittel** | 10/30 Stacks mit Healthcheck, 0 Ressourcen-Limits, mutable Tags hinter Digests versteckt |
|
||||||
|
| Storage/Backups | **sehr gut** | Borg→Hetzner, Dumps, H:/-Nearline, Restore-Drills mit Reports belegt; offen: Backup-Löschschutz |
|
||||||
|
| Security/Secrets | **gut** | `_FILE`/Stack-ENV konsequent, 2FA-Catch-all, WAN nur 443/tcp; `no-new-privileges` nur in 10/30 Stacks trotz P8-Pflichtregel |
|
||||||
|
| Monitoring/Alerting | **gut** | Prometheus/Blackbox/Loki/ntfy-Kette steht; Monitoring-Stack selbst hat keine Healthchecks und überwacht sich nicht selbst |
|
||||||
|
| Automatisierung/IaC | **sehr gut** | Komodo-Webhooks, Renovate, Posture-Check, Critical-Events-Watcher; manuelle Sync-Ausnahmen (traefik/dynamic, Authelia-Config) sind dokumentiert, aber fehleranfällig |
|
||||||
|
| Ausfallsicherheit | **bewusst begrenzt** | Ein Host, keine USV (geparkt Q3/2026), kein WAN-Failover — als akzeptiertes Risiko dokumentiert, das ist legitim |
|
||||||
|
| Strom/Kosten | **keine Daten** | keine Verbrauchsmessung im Repo sichtbar — siehe offene Fragen |
|
||||||
|
|
||||||
|
## Top 10 Verbesserungen nach Mehrwert
|
||||||
|
|
||||||
|
### 1. Immich vom `release`-Tag auf Versions-Tag pinnen
|
||||||
|
- **Beobachtung:** `apps/immich/docker-compose.yml:4` nutzt `immich-server:release@sha256:...` (ebenso ML). Renovate aktualisiert Digests — beim `release`-Tag ist ein "Digest-Update" in Wahrheit ein Major-/Minor-Versionssprung, ohne dass es im PR-Titel sichtbar wird. Immich ist berüchtigt für Breaking Changes zwischen Minors.
|
||||||
|
- **Warum relevant:** Ein gemergter "harmloser" Digest-PR kann Immich unangekündigt auf eine inkompatible Version heben (DB-Migrationen, ML-Modell-Wechsel).
|
||||||
|
- **Änderung:** Tag auf die konkret laufende Version umstellen (z. B. `immich-server:v2.x.y@sha256:<aktueller Digest>`), gleiche Vorgehensweise wie bei Mealie/Paperless. Laufende Version ermitteln: `docker exec immich_server cat /usr/src/app/package.json | grep version` oder Immich-UI → Version.
|
||||||
|
- **Verifikation:** Renovate erzeugt danach Versions-PRs statt stiller Digest-PRs; `docker inspect immich_server --format '{{.Config.Image}}'` zeigt den Versionstag.
|
||||||
|
- **Rollback:** Commit revert; Digest bleibt identisch, kein Redeploy-Zwang.
|
||||||
|
- **Nebenwirkungen:** keine zur Laufzeit (Digest unverändert). | Nutzen: **hoch** | Risiko: niedrig | Aufwand: klein | sofort
|
||||||
|
- Gleiches Muster prüfen für: `komodo:2`, `ddns-updater:latest`, `scrutiny:latest-omnibus`, `glances:latest-full` sowie tag-lose digest-only Images (`mail-archiver`, `borg-ui`, `ntfy` — Version im Compose unsichtbar).
|
||||||
|
|
||||||
|
### 2. Hetzner-Storage-Box-Snapshots als Ransomware-/Fehlbedienungsschutz aktivieren
|
||||||
|
- **Beobachtung:** Borg `append-only` wurde am 2026-06-01 bewusst verworfen (forced-command brach Key-Auth). Damit kann jeder mit dem Borg-Key (Host, borg-ui-Container mit `/local/secrets`-Mount) Archive **löschen** — ein kompromittierter Host vernichtet auch das Off-site-Backup.
|
||||||
|
- **Warum relevant:** Das ist die einzige verbliebene Lücke in einer sonst sehr guten Backup-Kette.
|
||||||
|
- **Änderung:** In der Hetzner-Robot-Konsole automatische Snapshots der Storage Box aktivieren (z. B. täglich, 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 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: niedrig–mittel | 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?
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# Runbook: Komodo Bulk-Deploy schlaegt mit DNS `connection refused` fehl
|
||||||
|
|
||||||
|
Stand: 2026-06-10 · Typ: Runbook / ADR-light · Status: Sofortmassnahme empfohlen, noch nicht umgesetzt
|
||||||
|
|
||||||
|
## Symptom
|
||||||
|
|
||||||
|
Ein Bulk-Merge (z. B. Renovate-Sammel-PR) loest gleichzeitig viele Komodo-Stack-Webhooks aus. Komodo startet parallele `DeployStack`. Nur ein Teil der Stacks deployt, der Rest bleibt auf dem alten Image. In der Deploy-Stufe **Compose Pull** stehen Fehler wie:
|
||||||
|
|
||||||
|
```
|
||||||
|
Get "https://registry-1.docker.io/v2/": dial tcp: lookup registry-1.docker.io
|
||||||
|
on 192.168.178.58:53: read udp ...->192.168.178.58:53: read: connection refused
|
||||||
|
```
|
||||||
|
|
||||||
|
Manuelles Re-Deploy der betroffenen Stacks danach funktioniert (AdGuard ist dann wieder oben).
|
||||||
|
|
||||||
|
## Ursache
|
||||||
|
|
||||||
|
Der Host nutzt **AdGuard Home als einzigen Resolver** (`/etc/resolv.conf` = nur `nameserver 192.168.178.58`, keine `/etc/docker/daemon.json`). AdGuard laeuft selbst als Container auf dem Host und bindet `0.0.0.0:53`. Wird der `adguard`-Stack im selben Batch neu deployt, faellt Port 53 fuer Sekunden aus. Alle parallelen `docker compose pull` der anderen Stacks koennen `registry-1.docker.io` dann nicht aufloesen -> `connection refused` -> Deploy `success=false`.
|
||||||
|
|
||||||
|
Es ist **kein** Webhook-, Auth- oder Docker-Hub-Rate-Limit-Problem: Webhooks authentifizieren sauber, `webhook_enabled=true`, Fehlerbild ist `connection refused` auf den eigenen DNS-Port direkt nach AdGuard-Recreate. Fuer den Pull-Pfad zaehlt der Docker-Daemon/Go-Resolver (iteriert ueber die `resolv.conf`-Server und springt bei Socket-Fehlern zum naechsten), nicht der glibc-Client.
|
||||||
|
|
||||||
|
## Sofortmassnahme (Schicht 1)
|
||||||
|
|
||||||
|
Unraid -> Settings -> Network Settings -> `eth0`:
|
||||||
|
|
||||||
|
- DNS server 1: `192.168.178.58` (AdGuard, bleibt)
|
||||||
|
- **DNS server 2: `192.168.178.1`** (FritzBox) -> Apply
|
||||||
|
|
||||||
|
Damit ueberleben Registry-Pulls einen kurzen AdGuard-Ausfall via Resolver-Failover. Im Normalbetrieb wird weiter DNS1 (AdGuard) genutzt, der Filter bleibt aktiv.
|
||||||
|
|
||||||
|
Pruefen / Bedingungen:
|
||||||
|
|
||||||
|
- **Kein `options rotate`** in `/etc/resolv.conf` (sonst dauerhafter Filter-Bypass). Aktuell nicht gesetzt; nach Apply erneut pruefen.
|
||||||
|
- Router muss oeffentliche Namen **selbst** aufloesen und nicht intern an AdGuard zurueckleiten.
|
||||||
|
- Hinweis zur Verifikation: Ein `nslookup registry-1.docker.io 192.168.178.1` bei laufendem AdGuard ist ein gutes Signal, aber **kein letzter Beweis**. Wasserdicht: AdGuard kurz stoppen und `dig @192.168.178.1 registry-1.docker.io`, oder FritzBox-Upstream / AdGuard-Querylog pruefen.
|
||||||
|
|
||||||
|
Rollback: DNS server 2 leeren + Apply.
|
||||||
|
|
||||||
|
## Betriebsregel (Schicht 2)
|
||||||
|
|
||||||
|
- **AdGuard und Unbound nicht gemeinsam mit abhaengigen Stacks im Bulk deployen.** DNS-Infrastruktur immer separat / einzeln deployen, nicht waehrend 20+ parallele Pulls laufen.
|
||||||
|
- Renovate-PRs gestaffelt mergen (eine Etappe pro Deploy) statt Sammel-Merge. Deckt dieses Problem fuer den Normalbetrieb bereits ab.
|
||||||
|
|
||||||
|
## Spaeter optional
|
||||||
|
|
||||||
|
- Komodo-Deploys serialisieren: statt vieler paralleler Stack-Webhooks eine **Procedure** (sequenzielle Stages) oder **Resource Sync** mit `after`-Ordering. Trifft die Ursache direkter, ist aber ein groesserer Umbau und **kein Renovate-Blocker**.
|
||||||
|
- Host-DNS vom AdGuard-Container entkoppeln (AdGuard eigene IP via macvlan, Host-Resolver auf Router/Unbound), damit `:53` am Host nicht exklusiv am Container-Lifecycle haengt.
|
||||||
|
|
||||||
|
## Verworfen
|
||||||
|
|
||||||
|
- `/etc/docker/daemon.json` mit `"dns": [...]`: wirkt nur fuer Container-DNS, nicht fuer Daemon-eigene Image-Pulls.
|
||||||
|
- AdGuard `network_mode: host`: beim Recreate ist der DNS-Prozess trotzdem weg; macht aus dem Single Point of Failure keinen HA-Resolver.
|
||||||
|
|
||||||
|
## Referenzen
|
||||||
|
|
||||||
|
- Diagnose-Zugriff: SSH `root@192.168.178.58`; Komodo-Mongo (`docker exec komodo-mongo`, DB `komodo`, Collections `Stack`/`Update`); Gitea SQLite `/data/gitea/gitea.db` (Tabelle `webhook`, `repo_id=3`).
|
||||||
|
- Verwandt: `docs/WORKFLOW.md` (DNS-Regeln fuer Container), `docs/GITOPS_DRIFT_RUNBOOK.md`.
|
||||||
|
</content>
|
||||||
@@ -66,15 +66,18 @@ services:
|
|||||||
image: prom/blackbox-exporter:v0.28.0@sha256:e753ff9f3fc458d02cca5eddab5a77e1c175eee484a8925ac7d524f04366c2fc
|
image: prom/blackbox-exporter:v0.28.0@sha256:e753ff9f3fc458d02cca5eddab5a77e1c175eee484a8925ac7d524f04366c2fc
|
||||||
container_name: monitoring-blackbox-exporter
|
container_name: monitoring-blackbox-exporter
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
# Use AdGuard so *.kaleschke.info resolves to the internal Traefik IP.
|
||||||
|
# External resolvers (1.1.1.1/8.8.8.8) return the public WAN IP, which
|
||||||
|
# causes hairpin-NAT timeouts when probing from inside the Docker network.
|
||||||
dns:
|
dns:
|
||||||
- 1.1.1.1
|
- 172.23.0.3
|
||||||
- 8.8.8.8
|
|
||||||
command:
|
command:
|
||||||
- --config.file=/etc/blackbox_exporter/blackbox.yml
|
- --config.file=/etc/blackbox_exporter/blackbox.yml
|
||||||
volumes:
|
volumes:
|
||||||
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro
|
- ./blackbox/blackbox.yml:/etc/blackbox_exporter/blackbox.yml:ro
|
||||||
networks:
|
networks:
|
||||||
- monitoring_net
|
- monitoring_net
|
||||||
|
- dns_net
|
||||||
expose:
|
expose:
|
||||||
- "9115"
|
- "9115"
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -129,6 +132,20 @@ services:
|
|||||||
GF_USERS_ALLOW_SIGN_UP: "false"
|
GF_USERS_ALLOW_SIGN_UP: "false"
|
||||||
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
GF_AUTH_ANONYMOUS_ENABLED: "false"
|
||||||
GF_PLUGINS_PREINSTALL_DISABLED: "true"
|
GF_PLUGINS_PREINSTALL_DISABLED: "true"
|
||||||
|
# --- Authelia OIDC SSO (2026-06-06) ---
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
|
||||||
|
GF_AUTH_GENERIC_OAUTH_NAME: Authelia
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_ID: grafana
|
||||||
|
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET__FILE: /run/secrets/grafana_oidc_client_secret
|
||||||
|
GF_AUTH_GENERIC_OAUTH_SCOPES: "openid profile email groups"
|
||||||
|
GF_AUTH_GENERIC_OAUTH_AUTH_URL: https://auth.kaleschke.info/api/oidc/authorization
|
||||||
|
GF_AUTH_GENERIC_OAUTH_TOKEN_URL: https://auth.kaleschke.info/api/oidc/token
|
||||||
|
GF_AUTH_GENERIC_OAUTH_API_URL: https://auth.kaleschke.info/api/oidc/userinfo
|
||||||
|
GF_AUTH_GENERIC_OAUTH_USE_PKCE: "true"
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP: "true"
|
||||||
|
# Proof: alle OIDC-Logins als Admin; spaeter ueber groups verfeinern
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: "'Admin'"
|
||||||
|
GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_STRICT: "false"
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/sh
|
- /bin/sh
|
||||||
- -c
|
- -c
|
||||||
@@ -145,6 +162,7 @@ services:
|
|||||||
secrets:
|
secrets:
|
||||||
- monitoring_grafana_admin_password
|
- monitoring_grafana_admin_password
|
||||||
- monitoring_grafana_influxdb_token
|
- monitoring_grafana_influxdb_token
|
||||||
|
- grafana_oidc_client_secret
|
||||||
expose:
|
expose:
|
||||||
- "3000"
|
- "3000"
|
||||||
security_opt:
|
security_opt:
|
||||||
@@ -160,7 +178,8 @@ services:
|
|||||||
- traefik.http.routers.monitoring-grafana.entrypoints=websecure
|
- traefik.http.routers.monitoring-grafana.entrypoints=websecure
|
||||||
- traefik.http.routers.monitoring-grafana.tls=true
|
- traefik.http.routers.monitoring-grafana.tls=true
|
||||||
- traefik.http.routers.monitoring-grafana.tls.certresolver=le
|
- traefik.http.routers.monitoring-grafana.tls.certresolver=le
|
||||||
- traefik.http.routers.monitoring-grafana.middlewares=authelia@file,secure-headers@file
|
# ForwardAuth bewusst entfernt 2026-06-06: Grafana macht jetzt eigenes OIDC-SSO gegen Authelia
|
||||||
|
- traefik.http.routers.monitoring-grafana.middlewares=secure-headers@file
|
||||||
- traefik.http.services.monitoring-grafana.loadbalancer.server.port=3000
|
- traefik.http.services.monitoring-grafana.loadbalancer.server.port=3000
|
||||||
|
|
||||||
grafana-dashboard-importer:
|
grafana-dashboard-importer:
|
||||||
@@ -351,6 +370,8 @@ networks:
|
|||||||
driver: bridge
|
driver: bridge
|
||||||
frontend_net:
|
frontend_net:
|
||||||
external: true
|
external: true
|
||||||
|
dns_net:
|
||||||
|
external: true
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
prometheus_data:
|
prometheus_data:
|
||||||
@@ -364,5 +385,7 @@ secrets:
|
|||||||
file: /mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt
|
file: /mnt/user/appdata/secrets/monitoring_grafana_admin_password.txt
|
||||||
monitoring_grafana_influxdb_token:
|
monitoring_grafana_influxdb_token:
|
||||||
file: /mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt
|
file: /mnt/user/appdata/secrets/monitoring_grafana_influxdb_token.txt
|
||||||
|
grafana_oidc_client_secret:
|
||||||
|
file: /mnt/user/appdata/secrets/grafana_oidc_client_secret
|
||||||
influxdb3_admin_token:
|
influxdb3_admin_token:
|
||||||
file: /mnt/user/appdata/secrets/influxdb3_admin_token.json
|
file: /mnt/user/appdata/secrets/influxdb3_admin_token.json
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
borg-ui:
|
borg-ui:
|
||||||
image: ainullcode/borg-ui@sha256:acb0fbe83dc4a3843abc06f814c5f1061a0701b2cfc574da2e851d17a34ab745
|
image: ainullcode/borg-ui@sha256:0922157e8f77a1b2bd23cd09366a458ea6de07fd9306aa1485f9cfe623eca17f
|
||||||
container_name: borg-ui
|
container_name: borg-ui
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
code-server:
|
code-server:
|
||||||
image: lscr.io/linuxserver/code-server:4.123.0@sha256:9dd4555720db04eb92d92cc84e7a34f0862bada5679889446a3004c45b5fa59b
|
image: lscr.io/linuxserver/code-server:4.123.0@sha256:cb261a7f87674b445e0fd66d87d55900c1b823d276c727ab0d168a75e69e9992
|
||||||
container_name: code-server
|
container_name: code-server
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
filebrowser:
|
filebrowser:
|
||||||
image: filebrowser/filebrowser:v2.63.13@sha256:e79c381fdbf549a48adc6268c74b920b70cab53663995a2e8142964eedea10c7
|
image: filebrowser/filebrowser:v2.63.14@sha256:1ec9b0c68297550c92f4a93feed432850c2993b261706cc3cc2e808f94a95e76
|
||||||
container_name: filebrowser
|
container_name: filebrowser
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -2,6 +2,11 @@ services:
|
|||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
# MongoDB – Datenbank fuer Komodo Core
|
# MongoDB – Datenbank fuer Komodo Core
|
||||||
# Netz: komodo_net (internal: true) – niemals frontend_net
|
# Netz: komodo_net (internal: true) – niemals frontend_net
|
||||||
|
# ACHTUNG: Dieser Stack wird NICHT aus diesem Repo deployed. Der komodo-Stack
|
||||||
|
# ist in Komodo inline (file_contents) verwaltet (Bootstrap-/Self-Stack).
|
||||||
|
# Diese Datei ist nur Doku/Spiegel; Aenderungen hier wirken NICHT zur Laufzeit.
|
||||||
|
# ops/komodo/** ist in renovate.json ignorePaths. Siehe docs/RENOVATE.md.
|
||||||
|
# Digest = aktuell real laufender Stand (kein Renovate-Auto-Update).
|
||||||
# ──────────────────────────────────────────────────────────────────
|
# ──────────────────────────────────────────────────────────────────
|
||||||
komodo-mongo:
|
komodo-mongo:
|
||||||
image: mongo:8.0.23@sha256:44aa79ae28ff80b56fe58681b66cda9336706df408a5175a6c04988aa54610d3
|
image: mongo:8.0.23@sha256:44aa79ae28ff80b56fe58681b66cda9336706df408a5175a6c04988aa54610d3
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
param(
|
||||||
|
[string]$HostLanIp = "192.168.178.58",
|
||||||
|
[string]$FritzBoxIp = "192.168.178.1",
|
||||||
|
[ValidateSet("LanPreflight", "Guest")]
|
||||||
|
[string]$Mode = "LanPreflight",
|
||||||
|
[string]$ReportPath = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
function Test-TcpPort {
|
||||||
|
param(
|
||||||
|
[string]$RemoteHost,
|
||||||
|
[int]$Port,
|
||||||
|
[int]$TimeoutMs = 1500
|
||||||
|
)
|
||||||
|
|
||||||
|
$client = [System.Net.Sockets.TcpClient]::new()
|
||||||
|
try {
|
||||||
|
$async = $client.BeginConnect($RemoteHost, $Port, $null, $null)
|
||||||
|
$ok = $async.AsyncWaitHandle.WaitOne($TimeoutMs, $false)
|
||||||
|
if (-not $ok) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
$client.EndConnect($async)
|
||||||
|
return $true
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
} finally {
|
||||||
|
$client.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Add-Result {
|
||||||
|
param(
|
||||||
|
[System.Collections.Generic.List[object]]$Results,
|
||||||
|
[string]$Name,
|
||||||
|
[string]$Target,
|
||||||
|
[bool]$Reachable,
|
||||||
|
[string]$ExpectedGuest,
|
||||||
|
[string]$Risk
|
||||||
|
)
|
||||||
|
|
||||||
|
$Results.Add([pscustomobject]@{
|
||||||
|
Name = $Name
|
||||||
|
Target = $Target
|
||||||
|
Reachable = $Reachable
|
||||||
|
ExpectedFromGuest = $ExpectedGuest
|
||||||
|
RiskIfReachableFromGuest = $Risk
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$adapters = Get-NetIPConfiguration |
|
||||||
|
Where-Object { $_.IPv4Address -and $_.NetAdapter.Status -eq "Up" } |
|
||||||
|
Select-Object InterfaceAlias,
|
||||||
|
@{Name="IPv4";Expression={$_.IPv4Address.IPAddress -join ", "}},
|
||||||
|
@{Name="Gateway";Expression={$_.IPv4DefaultGateway.NextHop -join ", "}},
|
||||||
|
@{Name="DnsServer";Expression={$_.DNSServer.ServerAddresses -join ", "}}
|
||||||
|
|
||||||
|
$results = [System.Collections.Generic.List[object]]::new()
|
||||||
|
|
||||||
|
Add-Result $results "Unraid HTTP/LAN" "${HostLanIp}:80" (Test-TcpPort $HostLanIp 80) "blocked" "Guest can reach LAN web entrypoint directly"
|
||||||
|
Add-Result $results "Unraid HTTPS/LAN" "${HostLanIp}:443" (Test-TcpPort $HostLanIp 443) "blocked" "Guest can reach LAN HTTPS entrypoint directly"
|
||||||
|
Add-Result $results "Gitea SSH/LAN" "${HostLanIp}:222" (Test-TcpPort $HostLanIp 222) "blocked" "Guest can reach Git SSH"
|
||||||
|
Add-Result $results "AdGuard Admin/LAN" "${HostLanIp}:8082" (Test-TcpPort $HostLanIp 8082) "blocked" "Guest can reach AdGuard admin UI"
|
||||||
|
Add-Result $results "InfluxDB LAN" "${HostLanIp}:8181" (Test-TcpPort $HostLanIp 8181) "blocked" "Guest can reach InfluxDB writer endpoint"
|
||||||
|
Add-Result $results "FRITZ!Box LAN UI" "${FritzBoxIp}:80" (Test-TcpPort $FritzBoxIp 80) "blocked-or-guest-gateway-only" "Guest can reach main router UI"
|
||||||
|
|
||||||
|
$risk = if ($Mode -eq "Guest") {
|
||||||
|
$results | Where-Object {
|
||||||
|
$_.ExpectedFromGuest -like "blocked*" -and $_.Reachable
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$results | Where-Object {
|
||||||
|
$_.Name -in @("AdGuard Admin/LAN", "InfluxDB LAN") -and $_.Reachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||||
|
$lines = [System.Collections.Generic.List[string]]::new()
|
||||||
|
$lines.Add("# Guest/IoT Isolation Check")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("Timestamp: $timestamp")
|
||||||
|
$lines.Add("Mode: $Mode")
|
||||||
|
$lines.Add("Host LAN IP: $HostLanIp")
|
||||||
|
$lines.Add("FRITZ!Box IP: $FritzBoxIp")
|
||||||
|
$lines.Add("Risk count: $($risk.Count)")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("## Active Network Adapters")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("| Interface | IPv4 | Gateway | DNS |")
|
||||||
|
$lines.Add("|---|---|---|---|")
|
||||||
|
foreach ($adapter in $adapters) {
|
||||||
|
$lines.Add("| $($adapter.InterfaceAlias) | $($adapter.IPv4) | $($adapter.Gateway) | $($adapter.DnsServer) |")
|
||||||
|
}
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("## Port Tests")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("| Name | Target | Reachable | Expected from guest Wi-Fi | Risk if reachable from guest |")
|
||||||
|
$lines.Add("|---|---|---:|---|---|")
|
||||||
|
foreach ($result in $results) {
|
||||||
|
$lines.Add("| $($result.Name) | $($result.Target) | $($result.Reachable) | $($result.ExpectedFromGuest) | $($result.RiskIfReachableFromGuest) |")
|
||||||
|
}
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("## Interpretation")
|
||||||
|
$lines.Add("")
|
||||||
|
$lines.Add("- `LanPreflight`: reachable `80/443/222` can be normal; `8082` and `8181` should still be blocked.")
|
||||||
|
$lines.Add("- `Guest`: all listed LAN targets should be blocked. Public domains may still work via the internet path.")
|
||||||
|
$lines.Add("- A non-zero risk count means the selected mode failed.")
|
||||||
|
|
||||||
|
$text = $lines -join [Environment]::NewLine
|
||||||
|
|
||||||
|
if ($ReportPath) {
|
||||||
|
$parent = Split-Path -Parent $ReportPath
|
||||||
|
if ($parent) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
||||||
|
}
|
||||||
|
Set-Content -Path $ReportPath -Value $text -Encoding UTF8
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output $text
|
||||||
|
|
||||||
|
if ($risk.Count -gt 0) {
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
exit 0
|
||||||
Executable
+90
@@ -0,0 +1,90 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
HOST_LAN_IP="${HOST_LAN_IP:-192.168.178.58}"
|
||||||
|
TAILSCALE_IP="${TAILSCALE_IP:-100.80.98.33}"
|
||||||
|
FRITZBOX_TR064_URL="${FRITZBOX_TR064_URL:-http://192.168.178.1:49000/tr64desc.xml}"
|
||||||
|
REPORT_ROOT="${REPORT_ROOT:-/mnt/user/backups/restore-reports}"
|
||||||
|
STAMP="$(date +%F-%H%M%S)"
|
||||||
|
REPORT_FILE="$REPORT_ROOT/guest-iot-preflight-$STAMP.md"
|
||||||
|
|
||||||
|
mkdir -p "$REPORT_ROOT"
|
||||||
|
|
||||||
|
tcp_check() {
|
||||||
|
local host="$1"
|
||||||
|
local port="$2"
|
||||||
|
timeout 2 bash -c "cat < /dev/null > /dev/tcp/$host/$port" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
result_row() {
|
||||||
|
local name="$1"
|
||||||
|
local target="$2"
|
||||||
|
local expectation="$3"
|
||||||
|
local status="$4"
|
||||||
|
printf '| %s | `%s` | %s | %s |\n' "$name" "$target" "$status" "$expectation"
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Guest/IoT Preflight"
|
||||||
|
echo
|
||||||
|
echo "Timestamp: $(date '+%F %T')"
|
||||||
|
echo "Scope: host-side read-only checks before enabling FRITZ!Box guest/IoT network"
|
||||||
|
echo
|
||||||
|
echo "## FRITZ!Box TR-064"
|
||||||
|
echo
|
||||||
|
if curl -fsS --max-time 5 "$FRITZBOX_TR064_URL" >/tmp/guest-iot-fritzbox-tr064.xml 2>/dev/null; then
|
||||||
|
model="$(grep -o '<friendlyName>[^<]*' /tmp/guest-iot-fritzbox-tr064.xml | head -n1 | sed 's/<friendlyName>//')"
|
||||||
|
echo "- TR-064 descriptor reachable: yes"
|
||||||
|
echo "- Model: ${model:-unknown}"
|
||||||
|
else
|
||||||
|
echo "- TR-064 descriptor reachable: no"
|
||||||
|
fi
|
||||||
|
rm -f /tmp/guest-iot-fritzbox-tr064.xml
|
||||||
|
echo
|
||||||
|
echo "## Host listeners"
|
||||||
|
echo
|
||||||
|
echo '```text'
|
||||||
|
ss -ltnp | sort -k4 | grep -E ':(53|80|443|222|8082|8181)[[:space:]]' || true
|
||||||
|
echo '```'
|
||||||
|
echo
|
||||||
|
echo "## Port reachability from host namespace"
|
||||||
|
echo
|
||||||
|
echo "| Check | Target | Status | Expectation |"
|
||||||
|
echo "|---|---|---|---|"
|
||||||
|
|
||||||
|
for port in 80 443 222 53; do
|
||||||
|
if tcp_check "$HOST_LAN_IP" "$port"; then
|
||||||
|
result_row "LAN service" "$HOST_LAN_IP:$port" "may be reachable from normal LAN; must be blocked from guest Wi-Fi" "reachable"
|
||||||
|
else
|
||||||
|
result_row "LAN service" "$HOST_LAN_IP:$port" "not reachable from host namespace or UDP-only" "blocked"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if tcp_check "$HOST_LAN_IP" 8082; then
|
||||||
|
result_row "AdGuard Admin via LAN IP" "$HOST_LAN_IP:8082" "should be blocked" "reachable"
|
||||||
|
else
|
||||||
|
result_row "AdGuard Admin via LAN IP" "$HOST_LAN_IP:8082" "should be blocked" "blocked"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if tcp_check "$TAILSCALE_IP" 8082; then
|
||||||
|
result_row "AdGuard Admin via Tailscale IP" "$TAILSCALE_IP:8082" "operator path should work" "reachable"
|
||||||
|
else
|
||||||
|
result_row "AdGuard Admin via Tailscale IP" "$TAILSCALE_IP:8082" "operator path should work" "blocked"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if tcp_check "$HOST_LAN_IP" 8181; then
|
||||||
|
result_row "InfluxDB via LAN IP" "$HOST_LAN_IP:8181" "should be blocked unless HA LAN writer is reintroduced" "reachable"
|
||||||
|
else
|
||||||
|
result_row "InfluxDB via LAN IP" "$HOST_LAN_IP:8181" "should be blocked unless HA LAN writer is reintroduced" "blocked"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
echo "## Next operator step"
|
||||||
|
echo
|
||||||
|
echo "Enable FRITZ!Box guest Wi-Fi only after confirming LAN isolation is active. Then connect a phone/laptop to guest Wi-Fi and run:"
|
||||||
|
echo
|
||||||
|
echo '```powershell'
|
||||||
|
echo 'G:\Gitea_Clone\homelab-infra\ops\maintenance\check-guest-iot-isolation.ps1 -Mode Guest'
|
||||||
|
echo '```'
|
||||||
|
} | tee "$REPORT_FILE"
|
||||||
|
|
||||||
|
echo "Guest/IoT preflight report: $REPORT_FILE"
|
||||||
@@ -52,6 +52,7 @@ Ziel:
|
|||||||
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
- `check-restore-freshness.ps1`: woechentlicher Frische-Check fuer Dumps und Reports
|
||||||
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
- `run-restore-checks.ps1`: einfacher Dispatcher fuer Restore-Jobs
|
||||||
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
- `check-restore-freshness.sh`: hosttauglicher Frische-Check
|
||||||
|
- `negative-freshness-alert-test.sh`: sicherer Negativtest fuer den Frische-Alarmweg; nutzt synthetische leere Testpfade unter `/mnt/user/backups/restore-lab/freshness-negative`, veraendert keine produktiven Dumps und sendet bei erkanntem Fehler einen Test-Alert nach `homelab-alerts`
|
||||||
- `run-restore-checks.sh`: hosttauglicher Dispatcher
|
- `run-restore-checks.sh`: hosttauglicher Dispatcher
|
||||||
- `common.sh`: gemeinsame Host-Helferfunktionen
|
- `common.sh`: gemeinsame Host-Helferfunktionen
|
||||||
- `automation-plan.md`: Host-Job- und Automatisierungsmodell
|
- `automation-plan.md`: Host-Job- und Automatisierungsmodell
|
||||||
@@ -100,6 +101,7 @@ Aktuell ist das erste validierte Muster vorhanden.
|
|||||||
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
- Bash-Dispatcher und Bash-Restore-Jobs am 2026-05-07 erfolgreich hostseitig verifiziert
|
||||||
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
- Restore-Lab und Report-Pfade auf dem Host angelegt
|
||||||
- `ntfy`-Wrapper ist fuer Host-Jobs verfuegbar
|
- `ntfy`-Wrapper ist fuer Host-Jobs verfuegbar
|
||||||
|
- Frische-Negativtest ist als sicherer Host-Job verfuegbar und am 2026-06-06 auf Unraid validiert: `ops/restore-tests/run-restore-checks.sh freshness-negative`. Ergebnis: synthetischer leerer Dump-Pfad erzeugte 10 Criticals, Test-Alert ging nach `homelab-alerts`, produktive Dump-Pfade blieben unangetastet. Report: `/mnt/user/backups/restore-reports/freshness-negative-2026-06-06-130320.md`.
|
||||||
- Nextcloud-Restore-Test: Scaffold existiert, aber **blockiert**. Nextcloud 33 fuehrt zur Laufzeit `chmod()` auf Dateien unter `/var/www/html` aus (`OC_Util.php:486`). Auf Unraids FUSE/shfs User-Shares ist `chmod` strukturell nicht moeglich, was zu permanenter 503 fuehrt. Loesungsoptionen: (a) Restore-Lab auf ein Cache-Drive statt User Share legen, (b) Docker-Volumes statt Bind-Mounts verwenden, (c) tmpfs-Mount fuer html/ + `rsync` der Borg-Daten hinein. Bis dahin ist Nextcloud als Backlog-Item dokumentiert.
|
- Nextcloud-Restore-Test: Scaffold existiert, aber **blockiert**. Nextcloud 33 fuehrt zur Laufzeit `chmod()` auf Dateien unter `/var/www/html` aus (`OC_Util.php:486`). Auf Unraids FUSE/shfs User-Shares ist `chmod` strukturell nicht moeglich, was zu permanenter 503 fuehrt. Loesungsoptionen: (a) Restore-Lab auf ein Cache-Drive statt User Share legen, (b) Docker-Volumes statt Bind-Mounts verwenden, (c) tmpfs-Mount fuer html/ + `rsync` der Borg-Daten hinein. Bis dahin ist Nextcloud als Backlog-Item dokumentiert.
|
||||||
- Komodo-Mongo-Daten-Restore am 2026-06-03 erfolgreich: 86904 Dokumente (inkl. 32 Stacks), Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`
|
- Komodo-Mongo-Daten-Restore am 2026-06-03 erfolgreich: 86904 Dokumente (inkl. 32 Stacks), Report `/mnt/user/backups/restore-reports/komodo-mongo-restore-2026-06-03.md`
|
||||||
- naechste grosse Kandidaten sind Mailarchiver und Mealie; Nextcloud bleibt blockiert (shfs-chmod)
|
- naechste grosse Kandidaten sind Mailarchiver und Mealie; Nextcloud bleibt blockiert (shfs-chmod)
|
||||||
|
|||||||
+74
@@ -0,0 +1,74 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
LAB_ROOT="${LAB_ROOT:-/mnt/user/backups/restore-lab/freshness-negative}"
|
||||||
|
REPORT_ROOT="${REPORT_ROOT:-/mnt/user/backups/restore-reports}"
|
||||||
|
ALERT_TOPIC="${ALERT_TOPIC:-homelab-alerts}"
|
||||||
|
INFO_TOPIC="${INFO_TOPIC:-homelab-info}"
|
||||||
|
SEND_NTFY="${SEND_NTFY:-1}"
|
||||||
|
|
||||||
|
stamp="$(date +%F-%H%M%S)"
|
||||||
|
test_root="$LAB_ROOT/$stamp"
|
||||||
|
dump_root="$test_root/dumps"
|
||||||
|
test_report_root="$test_root/reports"
|
||||||
|
report_file="$REPORT_ROOT/freshness-negative-$stamp.md"
|
||||||
|
raw_log="$test_root/check-output.md"
|
||||||
|
|
||||||
|
mkdir -p "$dump_root" "$test_report_root" "$REPORT_ROOT"
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$test_root"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
set +e
|
||||||
|
DUMP_ROOT="$dump_root" \
|
||||||
|
REPORT_ROOT="$test_report_root" \
|
||||||
|
MAX_DUMP_AGE_HOURS=26 \
|
||||||
|
MAX_REPORT_AGE_DAYS=45 \
|
||||||
|
"$SCRIPT_DIR/check-restore-freshness.sh" >"$raw_log" 2>&1
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
critical_count="$(awk -F': ' '/^Critical:/ {print $2; exit}' "$raw_log" | tr -d '[:space:]')"
|
||||||
|
critical_count="${critical_count:-0}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "# Restore Freshness Negative Alert Test"
|
||||||
|
echo
|
||||||
|
echo "Timestamp: $(date '+%F %T')"
|
||||||
|
echo "Result: $([ "$rc" -ne 0 ] && [ "$critical_count" -gt 0 ] && echo "ok" || echo "failed")"
|
||||||
|
echo "Check exit code: $rc"
|
||||||
|
echo "Critical count: $critical_count"
|
||||||
|
echo "Synthetic dump root: $dump_root"
|
||||||
|
echo "Synthetic report root: $test_report_root"
|
||||||
|
echo "Production dump root touched: no"
|
||||||
|
echo
|
||||||
|
echo "## Check Output"
|
||||||
|
echo
|
||||||
|
cat "$raw_log"
|
||||||
|
} >"$report_file"
|
||||||
|
|
||||||
|
if [ "$rc" -ne 0 ] && [ "$critical_count" -gt 0 ]; then
|
||||||
|
if [ "$SEND_NTFY" = "1" ]; then
|
||||||
|
bash "$SCRIPT_DIR/send-ntfy.sh" \
|
||||||
|
"$ALERT_TOPIC" \
|
||||||
|
"TEST: Restore freshness alert path ok" \
|
||||||
|
"Negativtest erfolgreich: check-restore-freshness.sh meldete ${critical_count} Criticals gegen synthetischen leeren Testpfad. Produktive Dumps wurden nicht veraendert. Report: $report_file" \
|
||||||
|
high
|
||||||
|
fi
|
||||||
|
echo "Negative freshness alert test ok. Report: $report_file"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$SEND_NTFY" = "1" ]; then
|
||||||
|
bash "$SCRIPT_DIR/send-ntfy.sh" \
|
||||||
|
"$ALERT_TOPIC" \
|
||||||
|
"TEST FAILED: Restore freshness alert path" \
|
||||||
|
"Negativtest fehlgeschlagen: erwarteter Critical-Zustand wurde nicht erkannt. Report: $report_file" \
|
||||||
|
high || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Negative freshness alert test failed. Report: $report_file" >&2
|
||||||
|
exit 1
|
||||||
@@ -10,6 +10,9 @@ case "$MODE" in
|
|||||||
freshness)
|
freshness)
|
||||||
exec "$SCRIPT_DIR/check-restore-freshness.sh"
|
exec "$SCRIPT_DIR/check-restore-freshness.sh"
|
||||||
;;
|
;;
|
||||||
|
freshness-negative)
|
||||||
|
exec "$SCRIPT_DIR/negative-freshness-alert-test.sh"
|
||||||
|
;;
|
||||||
vaultwarden)
|
vaultwarden)
|
||||||
if [ "$WHATIF" = "--what-if" ]; then
|
if [ "$WHATIF" = "--what-if" ]; then
|
||||||
exec "$SCRIPT_DIR/vaultwarden-restore-test.sh" --what-if
|
exec "$SCRIPT_DIR/vaultwarden-restore-test.sh" --what-if
|
||||||
@@ -95,7 +98,7 @@ case "$MODE" in
|
|||||||
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
|
exec "$SCRIPT_DIR/shared-pg-cluster-restore-test.sh"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo "Usage: $0 {freshness|vaultwarden|gitea|paperless|immich|authelia|adguard|redis|nextcloud|komodo-bootstrap|komodo-mongo-restore|traefik|mailarchiver|mealie|shared-pg-cluster} [--what-if]" >&2
|
echo "Usage: $0 {freshness|freshness-negative|vaultwarden|gitea|paperless|immich|authelia|adguard|redis|nextcloud|komodo-bootstrap|komodo-mongo-restore|traefik|mailarchiver|mealie|shared-pg-cluster} [--what-if]" >&2
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
scrutiny:
|
scrutiny:
|
||||||
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:bf5a07f583998b1b0a690d4b06be0378baa6f6f0140b4c815bd4fa55268df223
|
image: ghcr.io/starosdev/scrutiny:latest-omnibus@sha256:228483f16a6236d2fa9b2fbfca2e76dc861e648fbc6ae6e680d23e5d00211a5d
|
||||||
container_name: scrutiny
|
container_name: scrutiny
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
privileged: true
|
privileged: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
speedtest-tracker:
|
speedtest-tracker:
|
||||||
image: lscr.io/linuxserver/speedtest-tracker:1.14.3@sha256:79c00631575dec6d91c10ed904c211224f00813013a305c2284324e195a538bb
|
image: lscr.io/linuxserver/speedtest-tracker:1.14.3@sha256:c3750c40948a9360000ce62d694da92e85584b4ab6d3d9a9d1432d76fa5e0726
|
||||||
container_name: speedtest-tracker
|
container_name: speedtest-tracker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -125,8 +125,8 @@ und nicht als Alltags-USB verwenden.
|
|||||||
|
|
||||||
## Restore-Test ohne echten Restore
|
## Restore-Test ohne echten Restore
|
||||||
|
|
||||||
Der Test ist noch offen und muss nach dem ersten erfolgreichen Backup
|
Der Test wurde am 2026-06-06 erfolgreich abgeschlossen
|
||||||
durchgefuehrt werden:
|
(Operator-Bestaetigung):
|
||||||
|
|
||||||
1. Vom USB-Stick booten.
|
1. Vom USB-Stick booten.
|
||||||
2. Veeam Recovery Environment starten.
|
2. Veeam Recovery Environment starten.
|
||||||
@@ -137,8 +137,9 @@ durchgefuehrt werden:
|
|||||||
7. Vor Auswahl eines echten Ziel-Datentraegers abbrechen.
|
7. Vor Auswahl eines echten Ziel-Datentraegers abbrechen.
|
||||||
8. Windows normal von der internen SSD booten.
|
8. Windows normal von der internen SSD booten.
|
||||||
|
|
||||||
Erfolgskriterium: Recovery-Umgebung sieht das SMB-Ziel und den Restore Point,
|
Ergebnis: Recovery-Umgebung bootet, SMB-Ziel
|
||||||
ohne dass ein Restore gestartet wurde.
|
`\\kallilabcore\backups\windows-images\baerchen` ist erreichbar, Restore Point
|
||||||
|
wird angezeigt, und der Test wurde vor einem echten Restore abgebrochen.
|
||||||
|
|
||||||
## Erster Full-Lauf 2026-06-05
|
## Erster Full-Lauf 2026-06-05
|
||||||
|
|
||||||
@@ -157,8 +158,9 @@ Beleg aus `C:\ProgramData\Veeam\Endpoint\baerchen-c-image\Job.baerchen-c-image.B
|
|||||||
- Repository-Speicher danach: ca. 5.99 TB total, ca. 3.99 TB frei
|
- Repository-Speicher danach: ca. 5.99 TB total, ca. 3.99 TB frei
|
||||||
- Job-Konfiguration im Log: `StorageEncryptionEnabled=False`
|
- Job-Konfiguration im Log: `StorageEncryptionEnabled=False`
|
||||||
|
|
||||||
Damit ist der erste Image-Lauf technisch erfolgreich geschrieben. Noch offen
|
Damit ist der erste Image-Lauf technisch erfolgreich geschrieben und der
|
||||||
bleibt der Recovery-USB-Boot-/SMB-Test.
|
Recovery-USB-/SMB-/Restore-Point-Test wurde am 2026-06-06 erfolgreich
|
||||||
|
abgeschlossen.
|
||||||
|
|
||||||
Hilfsskript fuer die Windows-Seite:
|
Hilfsskript fuer die Windows-Seite:
|
||||||
|
|
||||||
@@ -173,7 +175,6 @@ Sitzung zusaetzlich per `net use` authentifiziert werden muss.
|
|||||||
|
|
||||||
## Offene Punkte
|
## Offene Punkte
|
||||||
|
|
||||||
- Recovery-Stick-Boot und SMB-Mount testen.
|
|
||||||
- Entscheiden, ob Veeam Storage Encryption nachtraeglich aktiviert werden soll.
|
- Entscheiden, ob Veeam Storage Encryption nachtraeglich aktiviert werden soll.
|
||||||
Wenn ja: Passwort in Vaultwarden anlegen, Job umstellen und ein neues Full-
|
Wenn ja: Passwort in Vaultwarden anlegen, Job umstellen und ein neues Full-
|
||||||
Backup erzeugen.
|
Backup erzeugen.
|
||||||
|
|||||||
+15
-1
@@ -38,6 +38,19 @@
|
|||||||
"automerge": false,
|
"automerge": false,
|
||||||
"labels": ["dependencies", "minor-patch"]
|
"labels": ["dependencies", "minor-patch"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Kritische Kerninfra (Traefik=Public-Entrypoint, Unbound=DNS, n8n, Nextcloud): nicht im Sammel-PR, eigene einzeln reviewbare PRs, kein Auto-Merge",
|
||||||
|
"matchManagers": ["docker-compose", "dockerfile"],
|
||||||
|
"matchPackageNames": [
|
||||||
|
"traefik",
|
||||||
|
"shaanmajid/unbound",
|
||||||
|
"docker.n8n.io/n8nio/n8n",
|
||||||
|
"nextcloud"
|
||||||
|
],
|
||||||
|
"groupName": null,
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["dependencies", "core-critical"]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Stateful Tier-1 (Postgres, Mongo, Redis): keine Auto-Group, einzelne PRs, kein Auto-Merge",
|
"description": "Stateful Tier-1 (Postgres, Mongo, Redis): keine Auto-Group, einzelne PRs, kein Auto-Merge",
|
||||||
"matchPackageNames": [
|
"matchPackageNames": [
|
||||||
@@ -99,6 +112,7 @@
|
|||||||
"ignorePaths": [
|
"ignorePaths": [
|
||||||
"**/_archive/**",
|
"**/_archive/**",
|
||||||
"ops/grafana-influxdb/**",
|
"ops/grafana-influxdb/**",
|
||||||
"ops/loki/**"
|
"ops/loki/**",
|
||||||
|
"ops/komodo/**"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,18 +41,21 @@ access_control:
|
|||||||
- git.kaleschke.info
|
- git.kaleschke.info
|
||||||
policy: bypass
|
policy: bypass
|
||||||
|
|
||||||
# Admin-Dienste - 2FA erforderlich (Operator-UIs mit Host-/Backup-Zugriff)
|
# Expliziter 2FA-Block (Tier-1-Operator-UIs). Redundant, seit die Catch-all
|
||||||
|
# unten ohnehin alles auf two_factor zwingt - bleibt nur zur Lesbarkeit.
|
||||||
|
# borg/code brauchen hier keinen Eintrag mehr: sie sind via Catch-all 2FA.
|
||||||
- domain:
|
- domain:
|
||||||
- files.kaleschke.info
|
- files.kaleschke.info
|
||||||
- scrutiny.kaleschke.info
|
- scrutiny.kaleschke.info
|
||||||
- borg.kaleschke.info
|
|
||||||
- code.kaleschke.info
|
|
||||||
policy: two_factor
|
policy: two_factor
|
||||||
|
|
||||||
# Alles andere mit Authelia-Middleware - 1FA.
|
# Alles andere mit Authelia-Middleware - 2FA (Haertung 2026-06-06).
|
||||||
|
# Vorher one_factor; alle restlichen Admin-UIs (monitoring, glances, glance,
|
||||||
|
# speedtest, pdf, mail, paperless-gpt, hermes, sp, borg, code ...) sind damit two_factor.
|
||||||
|
# Voraussetzung: Operator-Account hat Authelia-TOTP enrolled (Tier-1-UIs nutzen es bereits).
|
||||||
# Komodo hat bewusst keine ForwardAuth-Middleware und wird hier nicht ausgewertet.
|
# Komodo hat bewusst keine ForwardAuth-Middleware und wird hier nicht ausgewertet.
|
||||||
- domain: "*.kaleschke.info"
|
- domain: "*.kaleschke.info"
|
||||||
policy: one_factor
|
policy: two_factor
|
||||||
|
|
||||||
session:
|
session:
|
||||||
name: authelia_session
|
name: authelia_session
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ SINCE="${SINCE:-24h}"
|
|||||||
MAX_LOG_LINES="${MAX_LOG_LINES:-80}"
|
MAX_LOG_LINES="${MAX_LOG_LINES:-80}"
|
||||||
CERT_MAX_ROWS="${CERT_MAX_ROWS:-12}"
|
CERT_MAX_ROWS="${CERT_MAX_ROWS:-12}"
|
||||||
IMAGE_AGE_WARN_DAYS="${IMAGE_AGE_WARN_DAYS:-180}"
|
IMAGE_AGE_WARN_DAYS="${IMAGE_AGE_WARN_DAYS:-180}"
|
||||||
|
IMAGE_AGE_ALLOW_FILE="${IMAGE_AGE_ALLOW_FILE:-/mnt/user/services/homelab-infra/services/posture-check/image-age-allow.patterns}"
|
||||||
LOG_VOLUME_TOP_N="${LOG_VOLUME_TOP_N:-10}"
|
LOG_VOLUME_TOP_N="${LOG_VOLUME_TOP_N:-10}"
|
||||||
DISK_USAGE_WARN_PCT="${DISK_USAGE_WARN_PCT:-85}"
|
DISK_USAGE_WARN_PCT="${DISK_USAGE_WARN_PCT:-85}"
|
||||||
CERT_WARN_DAYS="${CERT_WARN_DAYS:-21}"
|
CERT_WARN_DAYS="${CERT_WARN_DAYS:-21}"
|
||||||
@@ -28,6 +29,7 @@ TRAEFIK_ACME_PATH="${TRAEFIK_ACME_PATH:-/mnt/user/appdata/traefik/letsencrypt/ac
|
|||||||
NOISE_PATTERNS_FILE="${NOISE_PATTERNS_FILE:-/mnt/user/services/homelab-infra/services/posture-check/log-noise.patterns}"
|
NOISE_PATTERNS_FILE="${NOISE_PATTERNS_FILE:-/mnt/user/services/homelab-infra/services/posture-check/log-noise.patterns}"
|
||||||
NORMALIZE_NOISE_SCRIPT="${NORMALIZE_NOISE_SCRIPT:-/mnt/user/services/homelab-infra/services/posture-check/lib/normalize-noise-patterns.sh}"
|
NORMALIZE_NOISE_SCRIPT="${NORMALIZE_NOISE_SCRIPT:-/mnt/user/services/homelab-infra/services/posture-check/lib/normalize-noise-patterns.sh}"
|
||||||
NOISE_ESCALATION_THRESHOLD="${NOISE_ESCALATION_THRESHOLD:-500}"
|
NOISE_ESCALATION_THRESHOLD="${NOISE_ESCALATION_THRESHOLD:-500}"
|
||||||
|
NOISE_ESCALATION_EXEMPT_FILE="${NOISE_ESCALATION_EXEMPT_FILE:-/mnt/user/services/homelab-infra/services/posture-check/noise-escalation-exempt.patterns}"
|
||||||
NOISE_BREAKDOWN_TOP_N="${NOISE_BREAKDOWN_TOP_N:-10}"
|
NOISE_BREAKDOWN_TOP_N="${NOISE_BREAKDOWN_TOP_N:-10}"
|
||||||
POSTURE_CHECK_FILE="${POSTURE_CHECK_FILE:-/mnt/user/services/posture-check/last.json}"
|
POSTURE_CHECK_FILE="${POSTURE_CHECK_FILE:-/mnt/user/services/posture-check/last.json}"
|
||||||
LOCK_FILE="${LOCK_FILE:-/tmp/homelab-daily-report.lock}"
|
LOCK_FILE="${LOCK_FILE:-/tmp/homelab-daily-report.lock}"
|
||||||
@@ -459,6 +461,10 @@ with open("/acme.json", "r", encoding="utf-8") as handle:
|
|||||||
data = json.load(handle)
|
data = json.load(handle)
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
|
# Deduplicate: for each unique set of domains keep only the longest-lived cert.
|
||||||
|
# Traefik stores both the old and the newly-issued cert in acme.json during
|
||||||
|
# the renewal window, which would otherwise produce a false warning.
|
||||||
|
best = {} # frozenset(domains) -> (days, expire_date_iso, names)
|
||||||
for resolver in data.values():
|
for resolver in data.values():
|
||||||
for cert in resolver.get("Certificates", []):
|
for cert in resolver.get("Certificates", []):
|
||||||
domain = cert.get("domain", {}).get("main") or "-"
|
domain = cert.get("domain", {}).get("main") or "-"
|
||||||
@@ -474,7 +480,11 @@ for resolver in data.values():
|
|||||||
not_after = datetime.strptime(decoded["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
not_after = datetime.strptime(decoded["notAfter"], "%b %d %H:%M:%S %Y %Z").replace(tzinfo=timezone.utc)
|
||||||
days = (not_after - now).days
|
days = (not_after - now).days
|
||||||
names = ", ".join([domain, *sans])
|
names = ", ".join([domain, *sans])
|
||||||
print(f"{days}\t{not_after.date().isoformat()}\t{names}")
|
key = frozenset([domain, *sans])
|
||||||
|
if key not in best or days > best[key][0]:
|
||||||
|
best[key] = (days, not_after.date().isoformat(), names)
|
||||||
|
for days, expires, names in best.values():
|
||||||
|
print(f"{days}\t{expires}\t{names}")
|
||||||
PY
|
PY
|
||||||
then
|
then
|
||||||
if [ ! -s "$cert_file" ]; then
|
if [ ! -s "$cert_file" ]; then
|
||||||
@@ -573,13 +583,36 @@ collect_image_freshness() {
|
|||||||
|
|
||||||
local image_file="$TMP_DIR/images.tsv"
|
local image_file="$TMP_DIR/images.tsv"
|
||||||
local image_warnings=0
|
local image_warnings=0
|
||||||
|
local image_allowed=0
|
||||||
local now_epoch
|
local now_epoch
|
||||||
: > "$image_file"
|
: > "$image_file"
|
||||||
now_epoch="$(date +%s)"
|
now_epoch="$(date +%s)"
|
||||||
|
|
||||||
|
# Parse the image-age allowlist: container deliberately pinned to a stable or
|
||||||
|
# upstream-recommended image. Each entry carries a recheck date; once that
|
||||||
|
# date has passed the suppression lapses, so a pin gets re-reviewed instead
|
||||||
|
# of silently aging forever.
|
||||||
|
local allow_file="$TMP_DIR/image-allow.tsv"
|
||||||
|
: > "$allow_file"
|
||||||
|
if [ -f "$IMAGE_AGE_ALLOW_FILE" ]; then
|
||||||
|
while IFS= read -r line; do
|
||||||
|
line="${line%%#*}"
|
||||||
|
line="$(printf '%s' "$line" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||||
|
[ -n "$line" ] || continue
|
||||||
|
local a_name a_date a_epoch
|
||||||
|
a_name="$(printf '%s' "$line" | awk '{ print $1 }')"
|
||||||
|
a_date="$(printf '%s' "$line" | awk '{ print $2 }')"
|
||||||
|
[ -n "$a_name" ] && [ -n "$a_date" ] || continue
|
||||||
|
a_epoch="$(date -d "$a_date" +%s 2>/dev/null || echo 0)"
|
||||||
|
if [ "$a_epoch" -ge "$now_epoch" ]; then
|
||||||
|
printf '%s\t%s\n' "$a_name" "$a_date" >> "$allow_file"
|
||||||
|
fi
|
||||||
|
done < "$IMAGE_AGE_ALLOW_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
while IFS= read -r name; do
|
while IFS= read -r name; do
|
||||||
[ -n "$name" ] || continue
|
[ -n "$name" ] || continue
|
||||||
local image_id created_iso created_epoch age_days image_tag
|
local image_id created_iso created_epoch age_days image_tag note recheck
|
||||||
image_id="$(docker inspect --format '{{.Image}}' "$name" 2>/dev/null || true)"
|
image_id="$(docker inspect --format '{{.Image}}' "$name" 2>/dev/null || true)"
|
||||||
[ -n "$image_id" ] || continue
|
[ -n "$image_id" ] || continue
|
||||||
created_iso="$(docker image inspect --format '{{.Created}}' "$image_id" 2>/dev/null || true)"
|
created_iso="$(docker image inspect --format '{{.Created}}' "$image_id" 2>/dev/null || true)"
|
||||||
@@ -588,33 +621,46 @@ collect_image_freshness() {
|
|||||||
created_epoch="$(date -d "$created_iso" +%s 2>/dev/null || echo 0)"
|
created_epoch="$(date -d "$created_iso" +%s 2>/dev/null || echo 0)"
|
||||||
[ "$created_epoch" -gt 0 ] || continue
|
[ "$created_epoch" -gt 0 ] || continue
|
||||||
age_days=$(( (now_epoch - created_epoch) / 86400 ))
|
age_days=$(( (now_epoch - created_epoch) / 86400 ))
|
||||||
printf '%d\t%s\t%s\n' "$age_days" "$name" "$image_tag" >> "$image_file"
|
note=""
|
||||||
if [ "$age_days" -ge "$IMAGE_AGE_WARN_DAYS" ]; then
|
if [ "$age_days" -ge "$IMAGE_AGE_WARN_DAYS" ]; then
|
||||||
|
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 Image >= $IMAGE_AGE_WARN_DAYS Tage: $image_warnings"
|
append "- Container mit ueberaltertem Image (gewarnt): $image_warnings"
|
||||||
|
append "- Davon bewusst gepinnt (von Warnung ausgenommen): $image_allowed"
|
||||||
|
append "- Allowlist-Quelle: \`$IMAGE_AGE_ALLOW_FILE\`"
|
||||||
append ""
|
append ""
|
||||||
append "### Aelteste Images (Top 10)"
|
append "### Aelteste Images (Top 10)"
|
||||||
append ""
|
append ""
|
||||||
append "| Alter Tage | Container | Image |"
|
append "| Alter Tage | Container | Image | Hinweis |"
|
||||||
append "|---:|---|---|"
|
append "|---:|---|---|---|"
|
||||||
sort -nr "$image_file" | head -n 10 | while IFS="$(printf '\t')" read -r age name img; do
|
sort -nr "$image_file" | head -n 10 | while IFS="$(printf '\t')" read -r age name img note; do
|
||||||
append "| $age | $name | $img |"
|
append "| $age | $name | $img | ${note:-} |"
|
||||||
done
|
done
|
||||||
append ""
|
append ""
|
||||||
if [ "$image_warnings" -eq 0 ]; then
|
if [ "$image_warnings" -eq 0 ] && [ "$image_allowed" -eq 0 ]; then
|
||||||
append "Bewertung: Keine Container mit ueberalterten Images. CVE-Hygiene aus dieser Sicht ok."
|
append "Bewertung: Keine Container mit ueberalterten Images. CVE-Hygiene aus dieser Sicht ok."
|
||||||
|
elif [ "$image_warnings" -eq 0 ]; then
|
||||||
|
append "Bewertung: Keine ungeprueft ueberalterten Images. $image_allowed Container sind bewusst gepinnt und mit Recheck-Datum dokumentiert."
|
||||||
else
|
else
|
||||||
append "Bewertung: $image_warnings Container nutzen Images aelter als $IMAGE_AGE_WARN_DAYS Tage. Update-Pipeline und CVE-Status pruefen."
|
append "Bewertung: $image_warnings Container nutzen ueberalterte Images (nicht in der Allowlist). Update-Pipeline und CVE-Status pruefen."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
append ""
|
append ""
|
||||||
@@ -655,6 +701,31 @@ collect_container_events() {
|
|||||||
collect_container_state() {
|
collect_container_state() {
|
||||||
append "## Container-Zustand"
|
append "## Container-Zustand"
|
||||||
append ""
|
append ""
|
||||||
|
|
||||||
|
append "### Unhealthy Container"
|
||||||
|
local unhealthy_file="$TMP_DIR/unhealthy.log"
|
||||||
|
docker ps --filter health=unhealthy --format '{{.Names}}' > "$unhealthy_file"
|
||||||
|
if [ ! -s "$unhealthy_file" ]; then
|
||||||
|
append "- Keine."
|
||||||
|
else
|
||||||
|
append "| Container | FailingStreak | Letzter Healthcheck |"
|
||||||
|
append "|---|---:|---|"
|
||||||
|
while IFS= read -r name; do
|
||||||
|
[ -n "$name" ] || continue
|
||||||
|
local streak hc
|
||||||
|
streak="$(docker inspect "$name" --format '{{.State.Health.FailingStreak}}' 2>/dev/null || echo '?')"
|
||||||
|
# Letzten nicht-leeren Health-Log-Eintrag holen, einzeilig machen und
|
||||||
|
# Pipe-Zeichen escapen, damit die Markdown-Tabelle nicht bricht.
|
||||||
|
hc="$(docker inspect "$name" --format '{{range .State.Health.Log}}exit={{.ExitCode}} out={{.Output}}~~~{{end}}' 2>/dev/null \
|
||||||
|
| tr '\n' ' ' \
|
||||||
|
| awk -F '~~~' '{ for (i = NF - 1; i >= 1; i--) { if ($i != "") { print $i; break } } }' \
|
||||||
|
| sed -E 's/[[:space:]]+/ /g; s/\|/\\|/g' \
|
||||||
|
| cut -c1-160)"
|
||||||
|
append "| \`$name\` | ${streak:-?} | ${hc:-(kein Output)} |"
|
||||||
|
done < "$unhealthy_file"
|
||||||
|
fi
|
||||||
|
append ""
|
||||||
|
|
||||||
append "### Nicht laufende Container"
|
append "### Nicht laufende Container"
|
||||||
local stopped_file="$TMP_DIR/stopped.log"
|
local stopped_file="$TMP_DIR/stopped.log"
|
||||||
docker ps -a --filter status=exited --filter status=dead --filter status=created --format '{{.Names}}\t{{.Status}}' > "$stopped_file"
|
docker ps -a --filter status=exited --filter status=dead --filter status=created --format '{{.Names}}\t{{.Status}}' > "$stopped_file"
|
||||||
@@ -810,12 +881,35 @@ collect_log_highlights() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Threshold escalation: how many patterns produced more than the threshold?
|
# Escalation-exempt patterns: known noise that is also permanently very loud
|
||||||
local noise_threshold_exceeded=0
|
# (e.g. Unraid mdadm parse spam). Without this, such a pattern would keep the
|
||||||
|
# report stuck at >= WARNUNG forever and devalue the OK/WARNUNG/KRITISCH
|
||||||
|
# signal. Exempt patterns are still counted/shown as noise, but do NOT count
|
||||||
|
# toward noise_threshold_exceeded. New/unexpected loud patterns still escalate.
|
||||||
|
local noise_exempt="$TMP_DIR/noise-escalation-exempt.normalized"
|
||||||
|
: > "$noise_exempt"
|
||||||
|
if [ -f "$NOISE_ESCALATION_EXEMPT_FILE" ]; then
|
||||||
|
grep -Ev '^[[:space:]]*(#|$)' "$NOISE_ESCALATION_EXEMPT_FILE" 2>/dev/null \
|
||||||
|
| sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//' \
|
||||||
|
| grep -v '^$' > "$noise_exempt" || : > "$noise_exempt"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Threshold escalation: how many NON-exempt patterns exceeded the threshold?
|
||||||
|
local noise_threshold_exceeded=0 noise_threshold_exempt=0
|
||||||
if [ -s "$noise_by_pattern" ]; then
|
if [ -s "$noise_by_pattern" ]; then
|
||||||
noise_threshold_exceeded="$(awk -v t="$NOISE_ESCALATION_THRESHOLD" '$1 > t { n++ } END { print n + 0 }' "$noise_by_pattern")"
|
noise_threshold_exceeded="$(awk -F '\t' -v t="$NOISE_ESCALATION_THRESHOLD" '
|
||||||
|
NR == FNR { exempt[$0] = 1; next }
|
||||||
|
$1 > t && !($2 in exempt) { n++ }
|
||||||
|
END { print n + 0 }
|
||||||
|
' "$noise_exempt" "$noise_by_pattern")"
|
||||||
|
noise_threshold_exempt="$(awk -F '\t' -v t="$NOISE_ESCALATION_THRESHOLD" '
|
||||||
|
NR == FNR { exempt[$0] = 1; next }
|
||||||
|
$1 > t && ($2 in exempt) { n++ }
|
||||||
|
END { print n + 0 }
|
||||||
|
' "$noise_exempt" "$noise_by_pattern")"
|
||||||
fi
|
fi
|
||||||
set_summary "noise_threshold_exceeded" "$noise_threshold_exceeded"
|
set_summary "noise_threshold_exceeded" "$noise_threshold_exceeded"
|
||||||
|
set_summary "noise_threshold_exempt" "$noise_threshold_exempt"
|
||||||
|
|
||||||
local hit_count attention_count known_noise_count
|
local hit_count attention_count known_noise_count
|
||||||
hit_count="$(count_lines < "$hits")"
|
hit_count="$(count_lines < "$hits")"
|
||||||
@@ -836,6 +930,9 @@ collect_log_highlights() {
|
|||||||
if [ "$noise_threshold_exceeded" -gt 0 ]; then
|
if [ "$noise_threshold_exceeded" -gt 0 ]; then
|
||||||
append "- WARNUNG: $noise_threshold_exceeded Pattern ueberschreit(en) die Schwelle - bitte pruefen ob noch wirklich Noise."
|
append "- WARNUNG: $noise_threshold_exceeded Pattern ueberschreit(en) die Schwelle - bitte pruefen ob noch wirklich Noise."
|
||||||
fi
|
fi
|
||||||
|
if [ "${noise_threshold_exempt:-0}" -gt 0 ]; then
|
||||||
|
append "- Hinweis: $noise_threshold_exempt laute(s) Pattern ist/sind als bewusst eskalations-befreit markiert (siehe \`$NOISE_ESCALATION_EXEMPT_FILE\`) und loesen keine WARNUNG aus."
|
||||||
|
fi
|
||||||
append ""
|
append ""
|
||||||
|
|
||||||
if [ "$attention_count" -eq 0 ]; then
|
if [ "$attention_count" -eq 0 ]; then
|
||||||
@@ -885,22 +982,32 @@ collect_log_highlights() {
|
|||||||
if [ -s "$noise_by_pattern" ]; then
|
if [ -s "$noise_by_pattern" ]; then
|
||||||
append "#### Pattern mit den meisten Treffern"
|
append "#### Pattern mit den meisten Treffern"
|
||||||
append ""
|
append ""
|
||||||
append "| Pattern | Anzahl |"
|
append "| Pattern | Anzahl | Hinweis |"
|
||||||
append "|---|---:|"
|
append "|---|---:|---|"
|
||||||
head -n "$NOISE_BREAKDOWN_TOP_N" "$noise_by_pattern" \
|
head -n "$NOISE_BREAKDOWN_TOP_N" "$noise_by_pattern" \
|
||||||
| while IFS="$(printf '\t')" read -r cnt pat; do
|
| while IFS="$(printf '\t')" read -r cnt pat; do
|
||||||
local short="$pat"
|
local short="$pat" note=""
|
||||||
|
# Mark patterns that are deliberately exempt from escalation.
|
||||||
|
if [ -s "$noise_exempt" ] && grep -Fxq -- "$pat" "$noise_exempt"; then
|
||||||
|
if [ "$cnt" -gt "$NOISE_ESCALATION_THRESHOLD" ]; then
|
||||||
|
note="eskalations-befreit"
|
||||||
|
fi
|
||||||
|
elif [ "$cnt" -gt "$NOISE_ESCALATION_THRESHOLD" ]; then
|
||||||
|
note="ueber Schwelle"
|
||||||
|
fi
|
||||||
if [ "${#short}" -gt 80 ]; then
|
if [ "${#short}" -gt 80 ]; then
|
||||||
short="${short:0:77}..."
|
short="${short:0:77}..."
|
||||||
fi
|
fi
|
||||||
# Escape pipe characters that would break the markdown table.
|
# Escape pipe characters that would break the markdown table.
|
||||||
short="${short//|/\\|}"
|
short="${short//|/\\|}"
|
||||||
append "| \`$short\` | $cnt |"
|
append "| \`$short\` | $cnt | $note |"
|
||||||
done
|
done
|
||||||
append ""
|
append ""
|
||||||
fi
|
fi
|
||||||
if [ "$noise_threshold_exceeded" -gt 0 ]; then
|
if [ "$noise_threshold_exceeded" -gt 0 ]; then
|
||||||
append "Bewertung: $noise_threshold_exceeded Pattern ueberschreit(en) die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD). Bitte pruefen, ob die als Noise eingeordneten Meldungen noch fachlich Noise sind oder ob sich ein echter Vorfall darunter versteckt."
|
append "Bewertung: $noise_threshold_exceeded nicht-befreite(s) Pattern ueberschreit(en) die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD). Bitte pruefen, ob die als Noise eingeordneten Meldungen noch fachlich Noise sind oder ob sich ein echter Vorfall darunter versteckt."
|
||||||
|
elif [ "${noise_threshold_exempt:-0}" -gt 0 ]; then
|
||||||
|
append "Bewertung: Kein nicht-befreites Pattern ueberschreitet die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD). $noise_threshold_exempt lautes Pattern ist bewusst eskalations-befreit und mit Begruendung dokumentiert."
|
||||||
else
|
else
|
||||||
append "Bewertung: Kein Pattern ueberschreitet die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD)."
|
append "Bewertung: Kein Pattern ueberschreitet die Eskalations-Schwelle ($NOISE_ESCALATION_THRESHOLD)."
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ set -euo pipefail
|
|||||||
TEXTFILE_DIR="${TEXTFILE_DIR:-/mnt/user/services/posture-check/textfile}"
|
TEXTFILE_DIR="${TEXTFILE_DIR:-/mnt/user/services/posture-check/textfile}"
|
||||||
OUTPUT_FILE="${OUTPUT_FILE:-$TEXTFILE_DIR/homelab.prom}"
|
OUTPUT_FILE="${OUTPUT_FILE:-$TEXTFILE_DIR/homelab.prom}"
|
||||||
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
BORG_CONTAINER="${BORG_CONTAINER:-borg-ui}"
|
||||||
CRITICAL_CONTAINERS="${CRITICAL_CONTAINERS:-traefik authelia postgresql17 gitea komodo-core komodo-mongo komodo-periphery vaultwarden borg-ui ntfy adguard unbound Tailscale-Docker monitoring-alertmanager monitoring-alertmanager-ntfy-bridge monitoring-blackbox-exporter monitoring-cadvisor monitoring-grafana monitoring-loki monitoring-node-exporter monitoring-promtail immich_server immich_postgres immich_redis paperless-ngx nextcloud nextcloud-postgres nextcloud-redis mealie mealie-postgres}"
|
CRITICAL_CONTAINERS="${CRITICAL_CONTAINERS:-traefik authelia postgresql17 gitea komodo-core komodo-mongo komodo-periphery vaultwarden borg-ui ntfy adguard unbound monitoring-alertmanager monitoring-alertmanager-ntfy-bridge monitoring-blackbox-exporter monitoring-cadvisor monitoring-grafana monitoring-loki monitoring-node-exporter monitoring-promtail immich_server immich_postgres immich_redis paperless-ngx nextcloud nextcloud-postgres nextcloud-redis mealie mealie-postgres}"
|
||||||
|
# Hinweis: Tailscale laeuft als natives Unraid-Plugin (kein Docker-Container) und
|
||||||
|
# wird daher hier bewusst NICHT als kritischer Container gefuehrt (Stand 2026-06-06).
|
||||||
|
|
||||||
mkdir -p "$TEXTFILE_DIR"
|
mkdir -p "$TEXTFILE_DIR"
|
||||||
tmp="$(mktemp "$TEXTFILE_DIR/homelab.prom.XXXXXX")"
|
tmp="$(mktemp "$TEXTFILE_DIR/homelab.prom.XXXXXX")"
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# image-age-allow.patterns - Daily Operations Report
|
||||||
|
#
|
||||||
|
# Container, die bewusst auf einem aelteren, aber aktuellen/empfohlenen Image
|
||||||
|
# gepinnt sind, sollen nicht jeden Tag als "Image ueberaltert" warnen.
|
||||||
|
#
|
||||||
|
# Format pro Zeile:
|
||||||
|
# <container-name> <YYYY-MM-DD recheck> # Begruendung
|
||||||
|
#
|
||||||
|
# - Spalte 1: exakter Container-Name (docker ps {{.Names}}).
|
||||||
|
# - Spalte 2: Recheck-Datum. NACH diesem Datum greift die Ausnahme NICHT
|
||||||
|
# mehr und der Container taucht wieder als Warnung auf -> erzwingt eine
|
||||||
|
# menschliche Neubewertung statt stillen Alterns.
|
||||||
|
# - Alles nach '#' ist Kommentar. Leerzeilen werden ignoriert.
|
||||||
|
#
|
||||||
|
# Eine Ausnahme heisst NICHT "Image egal", sondern "am Datum X erneut pruefen,
|
||||||
|
# ob es noch die empfohlene/aktuelle Version ist".
|
||||||
|
#
|
||||||
|
# Last reviewed: 2026-06-10
|
||||||
|
|
||||||
|
# immich_postgres: exakt das von Immich offiziell empfohlene, per Digest
|
||||||
|
# gepinnte DB-Image (14-vectorchord0.4.3-pgvectors0.2.0). Immichs eigene
|
||||||
|
# docker-compose auf main pinnt am 2026-06-10 denselben Tag inkl. identischem
|
||||||
|
# Digest. Kein Update, solange Immich nichts Neueres empfiehlt.
|
||||||
|
# Re-check: ob Immich ein neueres Postgres-Image empfiehlt.
|
||||||
|
immich_postgres 2026-09-10
|
||||||
|
|
||||||
|
# monitoring-blackbox-exporter: v0.28.0 ist am 2026-06-10 die NEUESTE Release
|
||||||
|
# (Dez 2025). Das Image-Alter ist nur Build-Alter, keine veraltete Version.
|
||||||
|
# Re-check: ob eine blackbox_exporter-Version > v0.28.0 erschienen ist.
|
||||||
|
monitoring-blackbox-exporter 2026-09-10
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
# Removing a pattern: replace with a fresh attention example in the next
|
# Removing a pattern: replace with a fresh attention example in the next
|
||||||
# daily report and consult before reintroducing.
|
# daily report and consult before reintroducing.
|
||||||
#
|
#
|
||||||
# Last reviewed: 2026-05-21
|
# Last reviewed: 2026-06-10
|
||||||
|
|
||||||
# Loki internal query cancellations / scheduler chatter.
|
# Loki internal query cancellations / scheduler chatter.
|
||||||
# Why: Loki cancels internal queries continuously when downstream Promtails
|
# Why: Loki cancels internal queries continuously when downstream Promtails
|
||||||
@@ -72,3 +72,18 @@ authelia.*Request timeout occurred.*status_code=408
|
|||||||
# noise becomes overwhelming, add a *narrow* pattern restricted to
|
# noise becomes overwhelming, add a *narrow* pattern restricted to
|
||||||
# push contexts only (e.g. `vaultwarden.*push.*(ResolveError|...)`).
|
# push contexts only (e.g. `vaultwarden.*push.*(ResolveError|...)`).
|
||||||
vaultwarden.*(Token has expired|Invalid refresh token|Failed to decode.*refresh_token|POST /identity/connect/token => 401 Unauthorized)
|
vaultwarden.*(Token has expired|Invalid refresh token|Failed to decode.*refresh_token|POST /identity/connect/token => 401 Unauthorized)
|
||||||
|
|
||||||
|
# AdGuard: Fritz!Box sends malformed SOA queries for myfritz.net / myfritz.link.
|
||||||
|
# Why: AVM Fritz!Box devices send multi-question DNS SOA queries that violate
|
||||||
|
# RFC 1035 ("only 1 question allowed"). AdGuard rejects them with an error
|
||||||
|
# but they have no operational impact.
|
||||||
|
# Re-check: if the same error appears for non-AVM domains, or if rate spikes
|
||||||
|
# well above 1000/day without a Fritz!Box reboot explaining it.
|
||||||
|
adguard.*bad question section.*only 1 question allowed
|
||||||
|
|
||||||
|
# Grafana: usage-stats collector looks for the Amazon Prometheus plugin, which
|
||||||
|
# is not installed in this setup. The error is emitted once per stats cycle.
|
||||||
|
# Why: GF_PLUGINS_PREINSTALL_DISABLED=true keeps the plugin list minimal;
|
||||||
|
# this lookup is harmless and does not affect any dashboard.
|
||||||
|
# Re-check: only if Amazon Prometheus is added as a datasource.
|
||||||
|
monitoring-grafana.*grafana-amazonprometheus-datasource not found
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# noise-escalation-exempt.patterns - Daily Operations Report
|
||||||
|
#
|
||||||
|
# Pattern, die als Rauschen bekannt UND dauerhaft sehr laut sind, sollen die
|
||||||
|
# Eskalations-Schwelle (NOISE_ESCALATION_THRESHOLD) nicht in eine WARNUNG
|
||||||
|
# uebersetzen. Ohne diese Ausnahme haengt der Report-Status strukturell auf
|
||||||
|
# >= WARNUNG fest (z. B. mdadm-Noise auf Unraid feuert dauerhaft > 5000/Tag),
|
||||||
|
# was die OK/WARNUNG/KRITISCH-Ampel entwertet.
|
||||||
|
#
|
||||||
|
# Wirkung: Ein hier gelistetes Pattern wird weiterhin als Noise gezaehlt und
|
||||||
|
# in der Breakdown-Tabelle gezeigt (mit Markierung "eskalations-befreit"),
|
||||||
|
# zaehlt aber NICHT mehr zu noise_threshold_exceeded. Neue/unerwartete laute
|
||||||
|
# Patterns loesen weiterhin eine WARNUNG aus.
|
||||||
|
#
|
||||||
|
# Format:
|
||||||
|
# - Exakte Pattern-Zeile wie in log-noise.patterns (nach Normalisierung:
|
||||||
|
# getrimmt, ohne Kommentar). Muss zeichengenau dem Eintrag entsprechen.
|
||||||
|
# - Zeilen mit '#' sind Kommentare, Leerzeilen werden ignoriert.
|
||||||
|
#
|
||||||
|
# Eine Befreiung heisst NICHT "ignorieren", sondern "Volumen ist als Noise
|
||||||
|
# akzeptiert; nur die ESKALATION ist abgeschaltet".
|
||||||
|
#
|
||||||
|
# Last reviewed: 2026-06-10
|
||||||
|
|
||||||
|
# node-exporter kann /proc/mdstat auf Unraid nicht parsen (eigener Array-
|
||||||
|
# Treiber, kein Linux-mdadm). Dauerhaft > 5000/Tag, rein kosmetisch.
|
||||||
|
# Re-check: nur bei Migration auf echtes mdadm-RAID.
|
||||||
|
monitoring-node-exporter.*mdadm.*Cannot parse /host/proc/mdstat
|
||||||
|
|
||||||
|
# Fritz!Box sendet RFC-1035-widrige Multi-Question-SOA-Queries fuer
|
||||||
|
# myfritz.net/myfritz.link; AdGuard lehnt sie ab. ~1000+/Tag, kein Impact.
|
||||||
|
# Re-check: falls derselbe Fehler fuer Nicht-AVM-Domains auftaucht.
|
||||||
|
adguard.*bad question section.*only 1 question allowed
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.7@sha256:fcdef599e6259359833dd2e1d49f9e964f66825d69bd3dd468f51102ce013d03
|
image: traefik:v3.7@sha256:d6858791f9e74df44ca4014166647c41cdc2abd3bf2a71b832ca4e1c6a91b257
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
security_opt:
|
security_opt:
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# traefik/dynamic/plex.yml
|
||||||
|
#
|
||||||
|
# Plex runs with network_mode: host for LAN discovery/GDM. The Docker provider
|
||||||
|
# resolves host-network containers to 127.0.0.1 from inside Traefik, which causes
|
||||||
|
# Bad Gateway. Keep this one host-network route as a documented File provider
|
||||||
|
# exception and point Traefik at the Unraid LAN IP.
|
||||||
|
http:
|
||||||
|
routers:
|
||||||
|
plex-web:
|
||||||
|
rule: Host(`plex.kaleschke.info`) && (Path(`/`) || Path(`/web`))
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- plex-web-redirect
|
||||||
|
- secure-headers@file
|
||||||
|
tls:
|
||||||
|
certResolver: le
|
||||||
|
service: plex
|
||||||
|
|
||||||
|
plex:
|
||||||
|
rule: Host(`plex.kaleschke.info`)
|
||||||
|
entryPoints:
|
||||||
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- secure-headers@file
|
||||||
|
tls:
|
||||||
|
certResolver: le
|
||||||
|
service: plex
|
||||||
|
|
||||||
|
middlewares:
|
||||||
|
plex-web-redirect:
|
||||||
|
redirectRegex:
|
||||||
|
regex: "^https://plex\\.kaleschke\\.info(/web)?/?$"
|
||||||
|
replacement: "https://plex.kaleschke.info/web/index.html"
|
||||||
|
permanent: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
plex:
|
||||||
|
loadBalancer:
|
||||||
|
servers:
|
||||||
|
- url: http://192.168.178.58:32400
|
||||||
Reference in New Issue
Block a user