2026-07-30 12:13:01 +02: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
|
|
|
|
|
|
|
|
|
|
```bash
|
|
|
|
|
cp .env.example .env # Werte setzen (nur beim allerersten Start relevant)
|
|
|
|
|
docker network create traefik # falls noch nicht vorhanden
|
|
|
|
|
docker compose up -d
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-01 14:01:14 +02:00
|
|
|
### 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.
|
|
|
|
|
|
2026-07-30 12:13:01 +02:00
|
|
|
## 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 |
|
2026-08-01 14:01:14 +02:00
|
|
|
| `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 |
|
2026-07-30 12:13:01 +02:00
|
|
|
|
|
|
|
|
## 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.
|
|
|
|
|
|
2026-08-01 14:01:14 +02:00
|
|
|
## 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.
|
|
|
|
|
|
2026-07-30 12:13:01 +02:00
|
|
|
## 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.
|