The exporter pruned first_seen on every scrape, keeping only what it had just seen. A report that failed to parse - a file being written, a brief I/O error - was skipped by a silent `continue`, so its findings never entered seen_keys and their first-seen timestamps were deleted for good. Nothing reported it, and "first seen" simply restarted at now. Pruning is now limited to targets whose report was actually read this round. Proven both ways against a throwaway results directory rather than by reasoning: make one report unreadable and its entry survives while read_errors counts 1; fix the other report but drop its finding and that entry is pruned as before. The distinction is the point - the old behaviour was not too aggressive, it was indiscriminate. Two numbers now leave the exporter: trivy_reports_total and trivy_report_read_errors, with alerts on both. They cover what TrivyScanStale cannot reach by construction - a target that never produced a report has no series for time() to compare against, so it stays quiet no matter how long it has been broken. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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), Pterodactyl-Host (157.90.155.206:9100+:8080). 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 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
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.- Pterodactyl-Host
157.90.155.206: node-exporter und cadvisor antworten aktuell nicht (Targets down). - Loki
table_managerist deprecated; Retention-Enforcement laeuft aktuell nicht ueber den Compactor -- bei Gelegenheit auf Compactor-Retention umstellen.