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-14 12:00:00 +00:00
|
|
|
### Config-Aenderungen: was wirklich ankommt
|
2026-08-01 14:01:14 +02:00
|
|
|
|
2026-08-14 12:00:00 +00:00
|
|
|
**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.
|
2026-08-01 14:01:14 +02:00
|
|
|
|
2026-08-14 12:00:00 +00:00
|
|
|
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:
|
2026-08-01 14:01:14 +02:00
|
|
|
|
|
|
|
|
```bash
|
2026-08-14 12:00:00 +00:00
|
|
|
docker compose up -d # Mount-/Service-Aenderungen -> Container wird neu erstellt
|
2026-08-01 14:01:14 +02:00
|
|
|
```
|
|
|
|
|
|
2026-08-14 12:00:00 +00:00
|
|
|
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:
|
2026-08-01 14:01:14 +02:00
|
|
|
|
|
|
|
|
```bash
|
2026-08-14 12:00:00 +00:00
|
|
|
docker exec prometheus md5sum /etc/prometheus/alerts.yml
|
|
|
|
|
md5sum prometheus/alerts.yml # muessen uebereinstimmen
|
2026-08-01 14:01:14 +02:00
|
|
|
```
|
|
|
|
|
|
2026-08-14 12:00:00 +00:00
|
|
|
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.
|
2026-08-01 14:01:14 +02:00
|
|
|
|
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-21 12:00:00 +00:00
|
|
|
| `cve/targets.py` | Bildet die Soll-Menge der Scan-Ziele aus Cluster, Betriebs-Host und Registry |
|
|
|
|
|
| `cve/test_targets.py` | Zusicherungen dazu, je mit Gegenprobe (`python3 test_targets.py`) |
|
|
|
|
|
| `cve/test_scan_loop.sh` | Zusicherungen fuer die Scan-Schleife (`sh cve/test_scan_loop.sh`) |
|
2026-08-01 14:01:14 +02:00
|
|
|
| `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.
|
2026-08-20 12:00:00 +00:00
|
|
|
- **Scrape ausgehend**: k3s-Host (`10.0.0.2:9100`), Game-Host privat ueber den
|
2026-08-20 12:00:00 +00:00
|
|
|
vSwitch (`10.0.0.4:9100`, `:8081` cadvisor, `:9810` gameserver-exporter).
|
2026-08-20 12:00:00 +00:00
|
|
|
cadvisor liegt dort auf 8081, weil Host-Port 8080 von coolify-proxy belegt ist.
|
2026-08-20 12:00:00 +00:00
|
|
|
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.
|
2026-07-30 12:13:01 +02:00
|
|
|
- `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)
|
|
|
|
|
|
2026-08-21 12:00:00 +00:00
|
|
|
`cve-scan` scannt alle 24h die Ziele aus `/targets/targets.txt` mit Trivy und legt
|
2026-08-01 14:01:14 +02:00
|
|
|
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").
|
|
|
|
|
|
2026-08-21 12:00:00 +00:00
|
|
|
### Die Zielmenge wird abgeleitet, NICHT gepflegt (#0106, ADR-0026)
|
|
|
|
|
|
|
|
|
|
Bis zum 2026-08-21 stand die Zielliste in `cve/images.txt` und musste bei
|
|
|
|
|
Stack-Aenderungen von Hand nachgezogen werden. Sie wurde es nicht: Gemessen an
|
|
|
|
|
dem Tag deckte sie **27 von 51** laufenden Images ab (52 %). Ungescannt blieben
|
|
|
|
|
unter anderem der Web-Client, den jeder Nutzer laedt, **beide** Traefik-Schichten
|
|
|
|
|
und die Registry selbst; gleichzeitig lief ein Scan gegen ein Image, das
|
|
|
|
|
nirgends mehr laeuft.
|
|
|
|
|
|
|
|
|
|
**Es gibt daher nichts mehr nachzuziehen.** Der `cve-exporter` bildet die
|
|
|
|
|
Soll-Menge bei jeder Herleitung neu aus drei Quellen und schreibt sie nach
|
|
|
|
|
`/targets/targets.txt` (Volume `cve_targets`, fuer den Scanner nur lesbar):
|
|
|
|
|
|
|
|
|
|
| Quelle | woher |
|
|
|
|
|
|---|---|
|
2026-08-21 12:00:00 +00:00
|
|
|
| Cluster | `kube_pod_container_info` aus dem eigenen Prometheus — **`image_spec`, nicht `image`** (siehe Warnung unten) |
|
2026-08-21 12:00:00 +00:00
|
|
|
| Betriebs-Host | `container_last_seen{job="operating_cadvisor"}` — schliesst `gameserver_cadvisor` aus |
|
|
|
|
|
| Registry | rohana `/v2`, anonymer Token; die letzten drei Fassungen je Repo |
|
|
|
|
|
|
|
|
|
|
Ein Ziel, das aus der Menge faellt, verliert seinen Bericht — sonst meldete ein
|
|
|
|
|
ausgemustertes Image weiter.
|
|
|
|
|
|
2026-08-21 12:00:00 +00:00
|
|
|
⚠️ **Ein per Digest festgenagelter Container (`repo:tag@sha256:...`) traegt in
|
|
|
|
|
`kube_pod_container_info` unter `image` nur den nackten `sha256:...`.** Die
|
|
|
|
|
brauchbare Referenz steht allein in `image_spec`. Am 2026-08-21 kam mit dem
|
|
|
|
|
alloy-Chart 1.x genau so ein Beiwagen herein; ohne diesen Zusatz waere er still
|
|
|
|
|
aus der Zielmenge gefallen. Der nackte Digest wird ausserdem ausdruecklich
|
|
|
|
|
abgewiesen, sonst landete er als Repo `sha256` in `targets.txt` und zaehlte
|
|
|
|
|
dauerhaft als Deckungsluecke.
|
|
|
|
|
|
2026-08-21 12:00:00 +00:00
|
|
|
⚠️ **Faellt eine Quelle aus, wird die Soll-Menge kleiner — und die Deckung sieht
|
|
|
|
|
dadurch BESSER aus, nicht schlechter.** Deshalb traegt jede Quelle einen
|
|
|
|
|
Frische-Zeitstempel (`cve_target_source_stale`), auf dem eine Alarmregel steht.
|
|
|
|
|
Wer hier etwas aendert, darf diese Absicherung nicht wegkuerzen; ohne sie ist
|
|
|
|
|
ein Ausfall der Herleitung von Erfolg nicht zu unterscheiden.
|
|
|
|
|
|
|
|
|
|
Deckung im Blick: `cve_target_coverage_ratio`, `cve_targets_missing`,
|
|
|
|
|
`cve_targets_orphaned` — im Dashboard *Security / CVE-Uebersicht*.
|
2026-08-01 14:01:14 +02:00
|
|
|
|
2026-08-21 12:00:00 +00:00
|
|
|
### Entschieden oder offen (#0051)
|
|
|
|
|
|
|
|
|
|
Ein CRITICAL, zu dem es nichts zu tun gibt, ist erlaubt — aber nur **benannt,
|
|
|
|
|
begruendet und mit Pruefdatum**. Diese Entscheidungen stehen in
|
|
|
|
|
`cve/entscheidungen.json`, je Eintrag ein Ziel mit den **einzelnen**
|
|
|
|
|
CVE-Kennungen:
|
|
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{ "ziel": "coturn/coturn:4.17.2", "art": "hingenommen",
|
|
|
|
|
"grund": "4.17.2 ist die neueste veroeffentlichte Fassung ...",
|
|
|
|
|
"pruefen_am": "2026-11-21", "cves": ["CVE-...", "..."] }
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
`art` ist `hingenommen` (es gibt nichts zu tun) oder `geplant` (die Arbeit steht
|
|
|
|
|
aus). Daraus zaehlt der Exporter `cve_critical_offen`,
|
|
|
|
|
`cve_critical_entschieden`, `cve_entscheidungen_abgelaufen` und
|
|
|
|
|
`cve_entscheidungen_ohne_befund`; vier Alarmregeln stehen darauf.
|
|
|
|
|
|
|
|
|
|
⚠️ **Bewusst keine Trivy-Ignore-Datei.** Trivy koennte die Befunde einfach
|
|
|
|
|
weglassen — dann saehe „0 CRITICAL, weil behoben" genauso aus wie „0 CRITICAL,
|
|
|
|
|
weil weggeschaut". Hier bleibt jeder Befund in `trivy_vuln_info` sichtbar;
|
|
|
|
|
entschieden heisst **gezaehlt, nicht versteckt**.
|
|
|
|
|
|
|
|
|
|
⚠️ **Pauschalen je Image sind nicht ausdrueckbar** (der Lader weist einen
|
|
|
|
|
Eintrag ohne CVE-Kennungen ab). Sonst verschluckte eine einmal getroffene
|
|
|
|
|
Entscheidung auch jeden **neuen** Befund an demselben Image.
|
|
|
|
|
|
|
|
|
|
⚠️ **Eine unlesbare Datei laesst alles als offen zaehlen**, nicht als
|
|
|
|
|
entschieden — die Richtung, die auffaellt. `CveEntscheidungenUnlesbar` nennt
|
|
|
|
|
den Grund sofort, `CveCriticalOffen` erst nach 24h.
|
|
|
|
|
|
|
|
|
|
Pruefen: `python3 cve/test_entscheidungen.py` (15 Zusicherungen, ohne Netz) und
|
|
|
|
|
`docker run --rm --entrypoint promtool -v "$PWD/prometheus:/p:ro"
|
|
|
|
|
prom/prometheus:v3.14.0 test rules /p/alerts_test.yml`.
|
|
|
|
|
|
2026-08-14 12:00:00 +00:00
|
|
|
### Alarm-Zustellung (frueher stummgeschaltet — seit gitops#51 wieder scharf)
|
2026-08-01 14:01:14 +02:00
|
|
|
|
2026-08-14 12:00:00 +00:00
|
|
|
**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.
|
2026-08-01 14:01:14 +02:00
|
|
|
|
2026-08-14 12:00:00 +00:00
|
|
|
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.
|
2026-08-01 14:01:14 +02:00
|
|
|
|
2026-08-14 12:00:00 +00:00
|
|
|
**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.
|
2026-08-01 14:01:14 +02:00
|
|
|
|
|
|
|
|
### 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.
|
2026-08-20 12:00:00 +00:00
|
|
|
- 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.
|
2026-07-30 12:13:01 +02:00
|
|
|
- Loki `table_manager` ist deprecated; Retention-Enforcement laeuft aktuell
|
|
|
|
|
nicht ueber den Compactor -- bei Gelegenheit auf Compactor-Retention
|
|
|
|
|
umstellen.
|