Files
threadnet-operating/monitoring
Thore Cimbal b989987d69 cve: four rules and three panels guard the derivation (#0106)
Gate 3 planned three rules; there are four. The fourth covers a case the others
miss entirely: every source answers cleanly but empty. Then nothing is missing,
because the desired set is empty, the freshness stamps are current, and nothing
is scanned at all. The Python suite already carries that case as "an empty set
is not the same as success", so the rule belongs with it.

These are the first rule unit tests in this stack. Each rule has a case where it
must fire and one where it must stay silent, because a rule that always fires
cannot be told from a correct one otherwise. Two sabotages confirm the tests
bite: an unreachable threshold on the source-freshness rule makes the expected
alert vanish, and removing the six hour grace period makes the missing-targets
rule fire at five hours where the test demands silence.

The grace period is not padding. A full round over roughly 65 images takes time,
so right after a deploy the gap is real rather than wrong.

The dashboard gains coverage and unscanned-image counters in the two free slots
of the top row, and a source-freshness bar at the bottom, so no existing panel
moves. That bar is the only place where a failed derivation can be told apart
from success.
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/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), 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 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.
  • 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.