Files
threadnet-operating/monitoring
Thore Cimbal 4758e35150 cve: only scan registry repositories that are actually running (#0051)
Decision by sorb: narrow the target set to what is operated rather than tidying
the registry. A repository now counts only while at least one of its tags is
running, which drops element-desktop-build and windows-vm and 61 of their 62
critical findings with them. Of those 62, exactly two had a fix available; they
are build artefacts nobody runs, so remediation was never the right answer.

The narrowing stays derived rather than maintained: the running set is the one
the derivation already builds, so there is no second list to keep in step
(ADR-0026).

Order now matters. The registry selection needs the running estate to tell a
rollback target from a build artefact, so it runs after the other two sources
and is skipped when they yield nothing.

That last part changes behaviour deliberately. A derivation where both estate
sources answer successfully but empty used to count as complete and merely
unusable; it now reports the registry as failed. If nothing is running at all,
that is an outage rather than a normal state, and it should say so instead of
hanging on a single boolean. The test carries the new contract with that
reasoning written next to it, plus a counter-case proving the registry does run
when the estate is known.
2026-08-21 12:00:00 +00:00
..

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, :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
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.

⚠️ 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.

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.