Files
threadnet-operating/monitoring/README.md
T

230 lines
11 KiB
Markdown
Raw Normal View History

# 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/targets.py` | Bildet die Soll-Menge der Scan-Ziele aus Cluster, Betriebs-Host und Registry |
| `cve/test_targets.py` | Zusicherungen dazu, je mit Gegenprobe (`python3 test_targets.py`) |
| `cve/test_scan_loop.sh` | Zusicherungen fuer die Scan-Schleife (`sh cve/test_scan_loop.sh`) |
| `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`), Game-Host privat ueber den
vSwitch (`10.0.0.4:9100`, `:8081` cadvisor, `:9810` gameserver-exporter).
cadvisor liegt dort auf 8081, weil Host-Port 8080 von coolify-proxy belegt ist.
Der Exporter auf `:9810` loest den alten auf `:9531` ab -- jener rief die
Client-API mit einem Application-Key ueber http auf und lieferte deshalb nie
eine einzige `pterodactyl_*`-Metrik.
- `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 Ziele aus `/targets/targets.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").
### Die Zielmenge wird abgeleitet, NICHT gepflegt (#0106, ADR-0026)
Bis zum 2026-08-21 stand die Zielliste in `cve/images.txt` und musste bei
Stack-Aenderungen von Hand nachgezogen werden. Sie wurde es nicht: Gemessen an
dem Tag deckte sie **27 von 51** laufenden Images ab (52 %). Ungescannt blieben
unter anderem der Web-Client, den jeder Nutzer laedt, **beide** Traefik-Schichten
und die Registry selbst; gleichzeitig lief ein Scan gegen ein Image, das
nirgends mehr laeuft.
**Es gibt daher nichts mehr nachzuziehen.** Der `cve-exporter` bildet die
Soll-Menge bei jeder Herleitung neu aus drei Quellen und schreibt sie nach
`/targets/targets.txt` (Volume `cve_targets`, fuer den Scanner nur lesbar):
| Quelle | woher |
|---|---|
| Cluster | `kube_pod_container_info` aus dem eigenen Prometheus — **`image_spec`, nicht `image`** (siehe Warnung unten) |
| Betriebs-Host | `container_last_seen{job="operating_cadvisor"}` — schliesst `gameserver_cadvisor` aus |
| Registry | rohana `/v2`, anonymer Token; die letzten drei Fassungen je Repo |
Ein Ziel, das aus der Menge faellt, verliert seinen Bericht — sonst meldete ein
ausgemustertes Image weiter.
⚠️ **Ein per Digest festgenagelter Container (`repo:tag@sha256:...`) traegt in
`kube_pod_container_info` unter `image` nur den nackten `sha256:...`.** Die
brauchbare Referenz steht allein in `image_spec`. Am 2026-08-21 kam mit dem
alloy-Chart 1.x genau so ein Beiwagen herein; ohne diesen Zusatz waere er still
aus der Zielmenge gefallen. Der nackte Digest wird ausserdem ausdruecklich
abgewiesen, sonst landete er als Repo `sha256` in `targets.txt` und zaehlte
dauerhaft als Deckungsluecke.
⚠️ **Faellt eine Quelle aus, wird die Soll-Menge kleiner — und die Deckung sieht
dadurch BESSER aus, nicht schlechter.** Deshalb traegt jede Quelle einen
Frische-Zeitstempel (`cve_target_source_stale`), auf dem eine Alarmregel steht.
Wer hier etwas aendert, darf diese Absicherung nicht wegkuerzen; ohne sie ist
ein Ausfall der Herleitung von Erfolg nicht zu unterscheiden.
Deckung im Blick: `cve_target_coverage_ratio`, `cve_targets_missing`,
`cve_targets_orphaned` — im Dashboard *Security / CVE-Uebersicht*.
### Entschieden oder offen (#0051)
Ein CRITICAL, zu dem es nichts zu tun gibt, ist erlaubt — aber nur **benannt,
begruendet und mit Pruefdatum**. Diese Entscheidungen stehen in
`cve/entscheidungen.json`, je Eintrag ein Ziel mit den **einzelnen**
CVE-Kennungen:
```json
{ "ziel": "coturn/coturn:4.17.2", "art": "hingenommen",
"grund": "4.17.2 ist die neueste veroeffentlichte Fassung ...",
"pruefen_am": "2026-11-21", "cves": ["CVE-...", "..."] }
```
`art` ist `hingenommen` (es gibt nichts zu tun) oder `geplant` (die Arbeit steht
aus). Daraus zaehlt der Exporter `cve_critical_offen`,
`cve_critical_entschieden`, `cve_entscheidungen_abgelaufen` und
`cve_entscheidungen_ohne_befund`; vier Alarmregeln stehen darauf.
⚠️ **Bewusst keine Trivy-Ignore-Datei.** Trivy koennte die Befunde einfach
weglassen — dann saehe „0 CRITICAL, weil behoben" genauso aus wie „0 CRITICAL,
weil weggeschaut". Hier bleibt jeder Befund in `trivy_vuln_info` sichtbar;
entschieden heisst **gezaehlt, nicht versteckt**.
⚠️ **Pauschalen je Image sind nicht ausdrueckbar** (der Lader weist einen
Eintrag ohne CVE-Kennungen ab). Sonst verschluckte eine einmal getroffene
Entscheidung auch jeden **neuen** Befund an demselben Image.
⚠️ **Eine unlesbare Datei laesst alles als offen zaehlen**, nicht als
entschieden — die Richtung, die auffaellt. `CveEntscheidungenUnlesbar` nennt
den Grund sofort, `CveCriticalOffen` erst nach 24h.
Pruefen: `python3 cve/test_entscheidungen.py` (15 Zusicherungen, ohne Netz) und
`docker run --rm --entrypoint promtool -v "$PWD/prometheus:/p:ro"
prom/prometheus:v3.14.0 test rules /p/alerts_test.yml`.
### 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.
- Game-Host: **erledigt**. Host-Seite am 2026-08-20 deployt und verifiziert, die
Ports liegen auf `10.0.0.4` (nicht `0.0.0.0` -- das umginge ufw). Der lokale
Prometheus/Grafana/Loki-Stack des Game-Hosts ist zurueckgebaut; er liefert nur
noch Sammler, promtail schickt nach `10.0.0.3:3100`.
Das Label-Konzept des Game-Hosts lebte in `metric_relabel_configs` und ist mit
dem Rueckbau hierher gewandert -- ohne es blieben Container nur unter ihrer UUID
sichtbar und je Serie rund 20 `container_label_*` uebrig.
- Loki `table_manager` ist deprecated; Retention-Enforcement laeuft aktuell
nicht ueber den Compactor -- bei Gelegenheit auf Compactor-Retention
umstellen.