The pipeline still holds the report for v1.151, so the six criticals in v1.158 never showed up in any query against it. They surfaced only from re-deriving the target set against the live cluster and folding in the pre-deploy scans by hand. Five sit without a fix in the debian base, one in the bundled tool's go runtime; the chart sets the version, so there is nothing to take. With that entry the count over all 54 desired targets is zero open.
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
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:
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:
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,:8081cadvisor,:9810gameserver-exporter). cadvisor liegt dort auf 8081, weil Host-Port 8080 von coolify-proxy belegt ist. Der Exporter auf:9810loest den alten auf:9531ab -- jener rief die Client-API mit einem Application-Key ueber http auf und lieferte deshalb nie eine einzigepterodactyl_*-Metrik. cadvisorundtraefikwerden gescraped, laufen aber in anderen Stacks (cadvisor: Portainer-Stackthread-net-git; beide haengen im externentraefik-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:
{ "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
TrivyScanStalekann ein Image, das nie erfolgreich gescannt wurde, nicht melden: ohne ersten Report gibt es keine Serie, an dertime() - trivy_last_scan_timestamphaengen koennte. Ein dauerhaft fehlschlagendes Image bleibt damit still.TargetDowndeckt nur den toten Exporter ab, nicht den einzelnen blinden Fleck.coturn/coturn:latestist 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,9100sind 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(nicht0.0.0.0-- das umginge ufw). Der lokale Prometheus/Grafana/Loki-Stack des Game-Hosts ist zurueckgebaut; er liefert nur noch Sammler, promtail schickt nach10.0.0.3:3100. Das Label-Konzept des Game-Hosts lebte inmetric_relabel_configsund ist mit dem Rueckbau hierher gewandert -- ohne es blieben Container nur unter ihrer UUID sichtbar und je Serie rund 20container_label_*uebrig. - Loki
table_managerist deprecated; Retention-Enforcement laeuft aktuell nicht ueber den Compactor -- bei Gelegenheit auf Compactor-Retention umstellen.