Files
threadnet-operating/monitoring
sorbandClaude Opus 4.8 4cb9bfb2bc fix(monitoring): mount prometheus/alertmanager config dirs, not single files
Deploying e0808ba surfaced the trap in practice: docker pins a single-file bind
mount to the inode, git pull replaces files by rename, so the container kept
serving the old alerts.yml while SIGHUP reported a successful reload — host and
container md5 differed and BackupCronJobMissing simply was not there.

The trap was already documented in this README, in detail, with the correct
command and a verification snippet, and it still bit. A footgun you avoid only by
reading gets stepped on eventually, so remove it structurally: directory mounts
resolve through the path on every access. Config paths are unchanged, so
--config.file keeps working. Remaining single-file mounts (loki, alloy, the
scripts) are named in the README as still needing --force-recreate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 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), Pterodactyl-Host (157.90.155.206:9100 + :8080).
  • 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.

Bekanntes Problem: Alarm-Zustellung ist stummgeschaltet

Die CVE-Regeln laufen, die Alarme werden aber bewusst nicht zugestellt. In alertmanager.yml routet room="security" derzeit auf den Null-Receiver.

Grund: TrivyCriticalVuln und TrivyHighVuln erzeugen eine Alarm-Instanz pro CVE pro Image. Beim ersten Scan-Durchlauf waren das bei 14 von 29 Images bereits 59 CRITICAL und 445 HIGH. Da group_by: [alertname, instance] alle in eine Gruppe legt und matrix-alerts.py eine Matrix-Nachricht pro Alarm schickt, wird daraus ein Nachrichten-Schwall. Verschaerfend: save_state() steht in do_POST hinter der Sende-Schleife -- bricht ein Send ab (Synapse rate-limitet nach ~10 Nachrichten mit 429), wird der State nicht gespeichert, der Receiver antwortet 502 und Alertmanager wiederholt die komplette Gruppe. Die Fingerprint-Deduplizierung ist beim Retry noch leer, also wiederholt sich das.

Zum Scharfschalten sind beide Punkte noetig:

  1. Zustellung buendeln -- entweder matrix-alerts.py auf eine Sammelnachricht pro Webhook-Batch umbauen (alle fuenf Pflichtfelder je CVE als eine Zeile), oder die Regeln auf count by (target, severity) aggregieren und die CVE-Details im Dashboard lassen.
  2. save_state() inkrementell nach jedem erfolgreichen Send, plus 429-Behandlung mit Retry-After.

Danach die room="security"-Route aus alertmanager.yml entfernen.

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.
  • Pterodactyl-Host 157.90.155.206: node-exporter und cadvisor antworten aktuell nicht (Targets down).
  • Loki table_manager ist deprecated; Retention-Enforcement laeuft aktuell nicht ueber den Compactor -- bei Gelegenheit auf Compactor-Retention umstellen.