Files
sorbandClaude Opus 4.8 e9c13dcf22 feat(monitoring): scrape alertmanager, alert on failed delivery; fix stale README
Alertmanager was configured as an alerting target but never scraped, so its own
metrics were absent: a silently breaking alert chain could not report itself —
the same blind spot as a missing series, now at the end of the chain. Adds the
operating_alertmanager scrape job and AlertDeliveryFailing on
alertmanager_notifications_failed_total.

The README still claimed alert delivery was deliberately muted via a
room=security null receiver. That route is gone; alertmanager.yml routes
everything to the matrix receiver, so the backup alerts added yesterday do get
delivered. Documentation asserting the opposite is dangerous in both directions,
so it now states the current wiring and keeps the alert-storm history as
background.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 12:00:00 +00:00

148 lines
6.8 KiB
Markdown

# Monitoring-Stack (Operating-Host)
Prometheus + Loki + Grafana + Alloy + node-exporter fuer den Operating-Server
(`CFGMON`). Vollstaendig provisioniert: Datasources und Dashboards liegen als
Code in diesem Repo, ein `docker compose up -d` auf einem frischen Host stellt
den kompletten Stack wieder her.
## Deployment
```bash
cp .env.example .env # Werte setzen (nur beim allerersten Start relevant)
docker network create traefik # falls noch nicht vorhanden
docker compose up -d
```
### Config-Aenderungen: was wirklich ankommt
**Prometheus und Alertmanager mounten seit 2026-08-15 ihr Config-VERZEICHNIS**
(`./prometheus`, `./alertmanager`) statt einzelner Dateien. Damit ist die frueher
hier beschriebene Inode-Falle fuer sie beseitigt: `git pull` ersetzt Dateien per
Rename (neuer Inode); ein Einzeldatei-Mount zeigt danach weiter auf die **alte**
Datei, waehrend `SIGHUP` seelenruhig Erfolg meldet. Verzeichnis-Mounts loesen bei
jedem Zugriff ueber den Pfad auf.
Warum strukturell statt per Anleitung: genau diese Falle war hier bereits
ausfuehrlich dokumentiert -- samt richtigem Kommando und Pruefbefehl -- und hat am
2026-08-14 trotzdem zugeschlagen (geladen wurden die alten Alert-Regeln, Reload
meldete Erfolg). Eine Fussangel, die man nur durch Lesen umgeht, umgeht man
irgendwann nicht.
Nach einem `git pull`, der Configs anfasst:
```bash
docker compose up -d # Mount-/Service-Aenderungen -> Container wird neu erstellt
```
Reicht bei Prometheus/Alertmanager fuer den Inhalt bereits ein Reload, ist das in
Ordnung -- die Datei ist jetzt wirklich die neue.
**Noch als Einzeldatei gemountet** (gleiche Falle, dort weiterhin
`--force-recreate` noetig): `loki/loki-config.yaml`, `alloy/config.alloy` sowie die
Skripte `matrix-alerts.py`, `release-watch.py`, `cve-exporter.py`.
Ob eine Aenderung angekommen ist, sieht man nur **im Container**, nie auf der Platte:
```bash
docker exec prometheus md5sum /etc/prometheus/alerts.yml
md5sum prometheus/alerts.yml # muessen uebereinstimmen
```
Grafana liest **Provider-Definitionen** nur beim Start: ein neuer Ordner in
`provisioning/dashboards/dashboards.yml` braucht `docker compose restart grafana`.
Dashboard-JSONs innerhalb eines bestehenden Providers werden laufend nachgezogen.
## Struktur
| Pfad | Inhalt |
|---|---|
| `docker-compose.yml` | Stack-Definition, Images auf feste Versionen gepinnt |
| `prometheus/prometheus.yml` | Scrape-Konfiguration |
| `loki/loki-config.yaml` | Loki Single-Binary-Konfiguration, 90 Tage Retention |
| `alloy/config.alloy` | Docker-Log-Collection -> Loki, node-exporter -> Prometheus |
| `grafana/provisioning/` | Datasources (feste UIDs!) und Dashboard-Provider |
| `grafana/dashboards/<ordner>/*.json` | Dashboards, je Unterordner ein Grafana-Ordner |
| `cve/images.txt` | Liste der real deployten Images, Inventur aus dem Cluster |
| `cve/scan-loop.sh` | Trivy-Scan-Schleife (24h), schreibt JSON-Reports |
| `cve/cve-exporter.py` | Reports -> Prometheus-Metriken, mit First-Seen-State |
## Externe Abhaengigkeiten
- **Remote-Write eingehend** auf `:9090`: k3s-Cluster und Matrix-Server pushen
Metriken (`flux`, `kube_state_metrics`, `synapse`).
- **Log-Push eingehend** auf `:3100` (Loki) von externen Alloys.
- **Scrape ausgehend**: k3s-Host (`10.0.0.2:9100`), Pterodactyl-Host
(`157.90.155.206:9100` + `:8080`).
- `cadvisor` und `traefik` werden gescraped, laufen aber in **anderen** Stacks
(cadvisor: Portainer-Stack `thread-net-git`; beide haengen im externen
`traefik`-Netz).
## Dashboards aendern
Provisionierte Dashboards sind in der UI editierbar (`allowUiUpdates: true`),
aber die JSON-Datei im Repo ist die Quelle der Wahrheit: UI-Aenderungen
muessen exportiert und committet werden, sonst gehen sie beim naechsten
Datei-Update verloren.
## CVE-Pipeline (gitops#47)
`cve-scan` scannt alle 24h die Images aus `cve/images.txt` mit Trivy und legt
JSON-Reports in ein Volume; `cve-exporter` serviert sie als Metriken auf
`:9101`, Prometheus scraped sie als Job `cve_exporter`. Dashboard:
**Security / CVE-Uebersicht (Trivy)**.
Der Exporter merkt sich je `(CVE, Target)` den Erstfund in einem persistenten
State (`cve_exporter_state`-Volume) -- daher kommt die Spalte
"erstmals gesehen". Verschwindet ein Finding, faellt der Eintrag raus
(= "geschlossen").
Bei Stack-Aenderungen `cve/images.txt` nachziehen, sonst scannt die Pipeline
an der Realitaet vorbei.
### Alarm-Zustellung (frueher stummgeschaltet — seit gitops#51 wieder scharf)
**Stand 2026-08-15: Alarme werden zugestellt.** `alertmanager.yml` hat nur noch die
Default-Route auf den `matrix`-Receiver; die frueher hier beschriebene
`room="security"`-Route auf den Null-Receiver existiert nicht mehr. Alarme **ohne**
`room`-Label (u.a. die `axion-basics`- und `axion-backup`-Gruppen) laufen ueber
diese Default-Route.
Historie, damit der Grund der damaligen Stummschaltung nicht verlorengeht: die
CVE-Regeln erzeugten je eine Alarm-Instanz **pro CVE pro Image** (beim ersten Lauf
59 CRITICAL + 445 HIGH), `matrix-alerts.py` schickte eine Nachricht pro Alarm,
Synapse rate-limitete nach ~10 Nachrichten mit 429, und weil `save_state()` hinter
der Sende-Schleife stand, wiederholte Alertmanager die komplette Gruppe mit noch
leerer Deduplizierung. Behoben durch Aggregation auf `count by (target, severity)`
(eine Instanz je Image statt je CVE); Details im Dashboard `cve-overview`.
Vollstaendige Historie: gitops#51 / AAR 2026-08-01.
**Zustellfehler sind seit 2026-08-15 selbst ueberwacht:** Prometheus scrapt
Alertmanager (`operating_alertmanager`) und `AlertDeliveryFailing` schlaegt an,
wenn `alertmanager_notifications_failed_total` steigt. Vorher war Alertmanager zwar
Alarm-Ziel, aber kein Scrape-Target — eine reissende Alarmkette haette sich also
selbst nicht melden koennen.
### Kleinere offene Punkte der Pipeline
- `TrivyScanStale` kann ein Image, das **nie** erfolgreich gescannt wurde,
nicht melden: ohne ersten Report gibt es keine Serie, an der
`time() - trivy_last_scan_timestamp` haengen koennte. Ein dauerhaft
fehlschlagendes Image bleibt damit still. `TargetDown` deckt nur den toten
Exporter ab, nicht den einzelnen blinden Fleck.
- `coturn/coturn:latest` ist als einziges Image ungepinnt -- Scan-Ergebnisse
sind dadurch nicht reproduzierbar.
- Der Exporter prunt den First-Seen-State bei **jedem** Scrape anhand der
gerade gelesenen Reports. Ein transienter Lesefehler (`except: continue`)
loescht die Erstfund-Zeitstempel des betroffenen Targets dauerhaft.
## Offene Punkte / Sicherheit
- `9090`, `3100`, `9100` sind auf der oeffentlichen IP ohne Auth erreichbar
(Docker umgeht ufw). Per **Hetzner Cloud Firewall** auf die IPs der
bekannten Absender einschraenken.
- Pterodactyl-Host `157.90.155.206`: node-exporter und cadvisor antworten
aktuell nicht (Targets down).
- Loki `table_manager` ist deprecated; Retention-Enforcement laeuft aktuell
nicht ueber den Compactor -- bei Gelegenheit auf Compactor-Retention
umstellen.