# 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 ```bash cp .env.example .env # Werte setzen (nur beim allerersten Start relevant) docker network create traefik # falls noch nicht vorhanden docker compose up -d ``` ### ACHTUNG: Config-Aenderungen brauchen ein `--force-recreate` `docker compose up -d` allein aktiviert **keine** geaenderte Config-Datei. Grund: `prometheus.yml`, `alerts.yml` und `alertmanager.yml` sind als **einzelne Dateien** gemountet, und Docker haengt so einen Mount am Inode auf. `git pull` schreibt eine neue Datei und benennt sie um -- neuer Inode. Der Container zeigt danach weiter auf die alte Datei und sieht die Aenderung nie. `up -d` merkt davon nichts (Service-Definition unveraendert -> kein Neustart), und ein `SIGHUP`-Reload laedt bloss den alten Inhalt erneut. Nach jedem `git pull`, der eine dieser Dateien anfasst: ```bash docker compose up -d --force-recreate prometheus alertmanager ``` Ob es gewirkt hat, sieht man nur **im Container**, nicht auf der Platte: ```bash docker exec prometheus grep -c axion-cve /etc/prometheus/alerts.yml # 0 = alter Inode ``` Nicht betroffen sind Verzeichnis-Mounts (`grafana/provisioning/`, `grafana/dashboards/`) -- die loesen bei jedem Zugriff ueber den Pfad auf. Grafana liest **Provider-Definitionen** allerdings nur beim Start: ein neuer Ordner in `provisioning/dashboards/dashboards.yml` braucht einen `docker compose restart grafana`. Neue oder geaenderte Dashboard-JSONs innerhalb eines bestehenden Providers werden dagegen 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//*.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.