25 Commits
Author SHA1 Message Date
Thore CimbalandClaude Opus 5 9a106151f1 monitoring: a read error must not erase the CVE timeline (management #0082)
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>
2026-08-19 12:00:00 +00:00
Thore CimbalandClaude Opus 4.8 be6b5f0657 docs: add group-rules pointer (CLAUDE.md) and repo-specific AGENTS.md
Field test F-011 found four of five components carried no pointer file, so a
session landing here had no path to the group rules at all. CLAUDE.md is the
one-line pointer the check looks for; AGENTS.md links the canonical rules in the
management repo (with the Gitea mirror URL for readers outside the lab) and
otherwise carries only what is specific and easy to get wrong here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-15 12:00:00 +00:00
sorbandClaude Opus 4.8 e9c13dcf22 feat(monitoring): scrape alertmanager, alert on failed delivery; fix stale README
Alertmanager was configured as an alerting target but never scraped, so its own
metrics were absent: a silently breaking alert chain could not report itself —
the same blind spot as a missing series, now at the end of the chain. Adds the
operating_alertmanager scrape job and AlertDeliveryFailing on
alertmanager_notifications_failed_total.

The README still claimed alert delivery was deliberately muted via a
room=security null receiver. That route is gone; alertmanager.yml routes
everything to the matrix receiver, so the backup alerts added yesterday do get
delivered. Documentation asserting the opposite is dangerous in both directions,
so it now states the current wiring and keeps the alert-storm history as
background.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 12:00:00 +00:00
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
sorbandClaude Opus 4.8 e0808bad90 fix(alerts): a missing series must not silence the backup alerts
Verification on CFGMON showed RestoreDrillStale could never fire: restore-drill
has no last_schedule_time series until its first scheduled run (a manually
triggered job does not set it), and an expression over a missing series yields
nothing. BackupNotRunning shares the flaw — deleting a CronJob removes the very
series the alert reads, so it goes quiet instead of firing.

Fall back to kube_cronjob_created, but aggregate with max by(namespace, cronjob):
'or' matches including __name__, so a bare fallback would return BOTH series and
the never-updating created timestamp would fire permanently once the window
elapsed. Add BackupCronJobMissing so a vanished CronJob is itself the alert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 12:00:00 +00:00
sorbandClaude Opus 4.8 1bbff5e749 feat(alerts): alert on backup failure, stalled backups and stale restore drill
There was no rule covering backups at all: a failed nightly job would have gone
unnoticed, which is precisely the silent failure management #0030 is about.
BackupJobFailed catches a failed run, BackupNotRunning catches a CronJob that
stopped scheduling, and RestoreDrillStale fires when the monthly drill stops —
an unverified backup is an assumption again, so the absence of the check is
itself worth alerting on.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-14 12:00:00 +00:00
Thore CimbalandClaude Fable 5 32f89afebf BACKLOG.md durch ein README ersetzt
Das Repo hatte auf oberster Ebene kein README - das einzige Frontdoor-Dokument
war ein BACKLOG.md, das seit 2026-07-30 nur noch auf 'sorb/Backlogs' verwies.
Dieses Repo gibt es unter dem Namen nicht mehr (umbenannt zu 'management',
kanonisch auf git.lab statt Gitea), und ein dateibasiertes Backlog widerspricht
ADR-0005: alles Offene ist ein Issue. Der alte Gitea-Link funktioniert nur noch
ueber einen 301-Redirect.

Ein Wegweiser, der auf ein Modell zeigt, das abgeloest wurde, ist schlechter als
keiner - deshalb ersetzt statt geflickt. Das neue README beschreibt, was das
Repo ist, verweist auf monitoring/README.md als Betriebsanleitung und nennt die
einschlaegigen Issues mit ihren aktuellen Nummern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-08-02 14:46:04 +02:00
Thore CimbalandClaude Fable 5 ff87cb25fb monitoring: CVE-Alarme aggregiert pro Image + Receiver-Robustheit (gitops#51)
Entscheidung sorb 2026-08-01 (Option 1 aus #51): Alarme als
count by (target, severity) statt pro CVE (~58 Serien statt ~1200),
CVE-Details bleiben im Dashboard (trivy_vuln_info unveraendert).
Receiver: inkrementelles save_state nach jedem Alarm, 1s-Sende-Drossel
(Synapse rc_message), recent_resolved-Dedup gegen doppelte Fallback-Haken
bei Batch-Retries, Teilfehler -> 502 liefert nur den Rest nach.
Stumm-Route + Null-Receiver entfernt - Zustellung wieder scharf.
promtool/amtool/py_compile gruen. UNGETESTET bis Deploy (Uebergabe-Issue).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-08-01 16:11:25 +02:00
0bd77e28d8 monitoring: CVE-Alarme vorerst stumm, Deploy-Fallstricke dokumentiert (gitops#47)
Die CVE-Pipeline aus #47 ist deployt und sammelt Daten, die Alarm-Zustellung
ist aber bewusst abgeklemmt: room="security" routet auf einen Null-Receiver.

Grund: die Regeln erzeugen eine Alarm-Instanz pro CVE pro Image (bei 14 von 29
gescannten Images bereits 59 CRITICAL / 445 HIGH). group_by legt alle in eine
Gruppe, matrix-alerts.py schickt eine Nachricht pro Alarm -> Schwall. Dazu
steht save_state() hinter der Sende-Schleife: bricht ein Send ab (Synapse
rate-limitet nach ~10 mit 429), wird kein State gespeichert, der Receiver
antwortet 502 und Alertmanager wiederholt die ganze Gruppe -- mit leerer
Deduplizierung. Details und Weg zum Scharfschalten im README.

Ausserdem dokumentiert: Config-Aenderungen an prometheus.yml/alerts.yml/
alertmanager.yml werden von "docker compose up -d" NICHT aktiv. Die Dateien
sind einzeln gemountet, Docker haengt den Mount am Inode, git pull erzeugt
beim Umbenennen einen neuen -- der Container sieht weiter die alte Datei.
Es braucht --force-recreate; ein SIGHUP laedt nur den alten Inhalt erneut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 16:09:37 +02:00
Thore CimbalandClaude Fable 5 cdfadc0f26 monitoring: Security-Dashboard-Ordner provisioniert (gitops#47)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-08-01 13:42:32 +02:00
Thore CimbalandClaude Fable 5 b6007c50fd monitoring: CVE-Pipeline v1 (gitops#47) - Scanner, Exporter, Regeln, Routing, Dashboard
- cve-scan: Trivy-Loop ueber die 29 real deployten Images (Cluster-Inventur
  2026-08-01 + Prod-Web-Image); 24h-Intervall, Fehler einzelner Images
  blockieren nicht
- cve-exporter: Stdlib-Exporter mit first_seen-State (Zeitstrahl), Schema
  trivy_vuln_info/_count/_first_seen/_last_scan gemaess Pflichtfeldern
- 3 Alertregeln (CRITICAL sofort, HIGH mit 24h-Daempfung, Scan-Frische) -
  promtool SUCCESS 9 rules; alle mit room=security
- matrix-alerts: Label-basiertes Raum-Routing (MATRIX_ROOM_<NAME>), Edits
  landen im richtigen Raum via State
- Grafana-Dashboard cve-overview: Severity-Stats, CVE-Tabelle mit
  NVD-Link/Fix-Version/first-seen, Zeitstrahl, Verlauf
UNGETESTET bis zum Deploy auf CFGMON (compose up -d).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-08-01 13:42:07 +02:00
Thore CimbalandClaude Fable 5 f45e01219b monitoring: release-watch in eigenen CVE-/Release-Raum (gitops#47), SBOM-Abgrenzung dokumentiert
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-08-01 10:10:16 +02:00
Thore CimbalandClaude Fable 5 8c06329e33 monitoring: Release-/Advisory-Watch fuer Element-Upstreams (gitops#22)
Stdlib-Daemon im matrix-alerts-Muster: pollt die GitHub-Release-Atom-Feeds
von synapse/ess-helm/element-web/mas/element-call alle 6h und meldet neue
Eintraege als Notiz in den Alerts-Raum (Security-Verdacht mit 🚨 markiert).
Erstlauf setzt nur den State. UNGETESTET bis zum Deploy auf CFGMON.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-08-01 04:51:53 +02:00
6ffab68583 monitoring: State von matrix-alerts persistent machen
Der Receiver (ea33c3b) merkt sich die Firing-Nachricht pro Alarm, um sie beim
Resolved per Edit abzuhaken. Die State-Datei lag aber unter /tmp im Writable
Layer des Containers. Das ueberlebt ein "compose restart", nicht aber ein
"up -d", das den Container neu baut -- also genau jeden Deploy. Danach haetten
alle offenen Alarme ihre Event-Zuordnung verloren und sich ueber den Fallback
als separate Nachricht aufgeloest, statt die urspruengliche abzuhaken.

Jetzt: named volume matrix_alerts_data auf /state, Pfad per MATRIX_STATE_FILE.

Verifiziert: Testalarm eingekippt, State geschrieben, Container per
--force-recreate neu gebaut (Container-ID 7e013a99d892 -> b1f95380dede),
State unveraendert vorhanden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 04:51:08 +02:00
Thore CimbalandClaude Fable 5 ea33c3b7b0 monitoring: Resolved hakt die Firing-Nachricht per Edit ab statt neu zu posten
Ein Alarm = eine Nachricht: Firing-Event-IDs werden je
Alertmanager-Fingerprint gemerkt (State-Datei, ueberlebt Restarts),
Resolved ersetzt die Originalnachricht per m.replace mit
durchgestrichenem Text + Haken. Ohne bekannte Zuordnung Fallback auf
eigenstaendige Resolved-Nachricht. Re-Notifies desselben Alarms
erzeugen keine Doppelnachrichten mehr.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-08-01 03:24:52 +02:00
Thore CimbalandClaude Fable 5 5b09a12287 monitoring: echte Alerts-Raum-ID eingetragen, Bot eingerichtet (gitops#32)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-07-31 23:43:04 +02:00
Thore CimbalandClaude Fable 5 682adbdd54 monitoring: Alert-Zustellung auf eigenen @alerts-Bot + dedizierten Raum umgestellt
Entscheidung 2026-07-31 (gitops#32): eigener Bot statt maintenance-notify
(Token-Trennung MATRIX-Host vs. CFGMON), eigener Alerts-Raum im
Operating-Space statt Wartungsraum. Bot-Account existiert bereits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PKhFj1S3UdD6xL2fbWPeYj
2026-07-31 23:26:32 +02:00
Thore CimbalandClaude Fable 5 9594ec80ec monitoring: Alerting vorbereitet - Alertmanager, 6 Alert-Regeln, Matrix-Receiver (gitops#32)
Regeln decken die real erlebten Fehlerklassen ab: TargetDown (coturn-Klasse),
KubePodRestartLoop (36k-Restarts-Klasse), OOM-Kills, RAM-/Swap-/Disk-Druck.
Zustellung in den wartung-Raum ueber einen minimalen Stdlib-Webhook-Receiver
(gleiche Machart wie maintenance-notify, gitops Issue #24); Bot-Token kommt
beim Deploy per .env (Vorlage in .env.example).

UNGETESTET/DEPLOY-PENDING: promtool/amtool-Lint auf dem Mac an haengendem
Docker-Hub-Pull gescheitert - vor dem Deploy auf CFGMON ausfuehren (Images
liegen dort bereits):
  docker run --rm -v $PWD/monitoring/prometheus:/cfg:ro --entrypoint promtool prom/prometheus:v3.3.1 check rules /cfg/alerts.yml
  docker run --rm -v $PWD/monitoring/alertmanager:/cfg:ro --entrypoint amtool prom/alertmanager:v0.28.1 check-config /cfg/alertmanager.yml

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-31 18:30:40 +02:00
Claude b067c93677 BACKLOG.md: auf zentrales Backlogs-Repo verweisen
Die offenen Punkte betreffen inzwischen mehrere Hosts, dieses Repo
beschreibt aber einen Stack auf einem Host. Inhalte sind nach
sorb/Backlogs umgezogen und dort pro Host strukturiert; hier bleibt nur
der Verweis, damit die Liste nicht an zwei Stellen auseinanderlaeuft.

Die Historie der urspruenglichen Eintraege bleibt bis 25bb5dd erhalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:37:38 +02:00
Claude 25bb5dd44e BACKLOG.md: DNS-Bereinigung bei IONOS aufnehmen
IONOS legt pro Subdomain automatisch einen Mail-Satz (MX, SPF,
DKIM-CNAMEs, autodiscover) und ein www.-Paar an, auch fuer Hosts ohne
Mail. Betrifft selendis und matrix vollstaendig, rohana und game nur
beim www.-Paar.

Ersatzloses Loeschen waere schlechter: ohne SPF gibt es keine Aussage
mehr, und ohne MX weichen Absender per RFC 5321 auf A/AAAA aus -- Mail
an @rohana.axion1337.de landete dann auf Port 25 des Hosts. Richtig ist
Null-MX (RFC 7505) plus SPF -all plus DMARC p=reject.

Konkrete Record-Listen fuer rohana und selendis dokumentiert; der User
setzt diese beiden direkt um. game, matrix, ftp und die Apex-DMARC-
Policy bleiben offen -- bei matrix erst klaeren, ob der Server Mail mit
Absender @matrix.axion1337.de verschickt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:25:13 +02:00
Claude adb8cf3a11 BACKLOG.md: IPv6 und www.rohana ergaenzen
rohana, www.rohana und selendis haben jetzt AAAA-Records auf
2a01:4f8:c17:93eb::1. Let's Encrypt validiert damit bevorzugt ueber
IPv6, die Hetzner-Firewall braucht fuer 443 also eine Regel mit Quelle
::/0 zusaetzlich zu 0.0.0.0/0 -- sonst scheitert die Erneuerung Ende
September trotz offenem IPv4.

Ausliefern ueber IPv6 funktioniert bereits (rohana und selendis
antworten mit 200, Traefik lauscht auf [::]:443).

www.rohana matcht keinen Traefik-Router und liefert das Traefik-Default-
Cert aus. Bei einer Subdomain ist das www.-Praefix ueberfluessig;
Empfehlung ist Loeschen der beiden Records statt Router + Cert-SAN.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:17:17 +02:00
Claude 8c7a57dbf1 BACKLOG.md: offene Punkte festhalten
Zwei Punkte, die bewusst nicht sofort erledigt wurden:

1. Cert-Erneuerung ab Ende September 2026 braucht Port 443 aus dem
   offenen Internet (TLS-ALPN-01). Betrifft auch Gitea. Alternative:
   Umstellung auf DNS-01, dann ohne offenen Port.
2. Pterodactyl-Host 157.90.155.206 ist nicht erreichbar (kein Ping,
   Ports 8080/9100 dicht), daher 2 Prometheus-Targets down. Bestand
   schon vor dem Rework.

Zusaetzlich notiert: oeffentlich erreichbarer Remote-Write-Receiver auf
9090 ohne Auth, und dass die Grafana-Admin-Credentials aus .env nicht
fuer die HTTP-API gelten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:11:38 +02:00
Claude edac97e931 monitoring: Alloy-Storage persistieren
--storage.path=/var/lib/alloy/data war gesetzt, aber ohne Volume: die
Positions-Datei lag im Container-Layer und war bei jedem Recreate weg.
Alloy las danach alle Docker-Logdateien von vorn, worauf Loki alle
Eintraege aelter als 7 Tage mit HTTP 400 abwies
("timestamp too old", reject_old_samples). Sichtbar als Fehler-Burst bei
jedem Deploy; betroffen waren nur Alt-Logzeilen bis zurueck zu 2025,
keine aktuellen Daten.

Verifiziert: Positions-Datei liegt jetzt in monitoring_alloy_data und
ueberlebt --force-recreate (28 -> 31 Zeilen), zweiter Recreate erzeugt
0 Fehler. 7 Container liefern weiterhin Logs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:10:04 +02:00
Claude a400f8a4ba monitoring: Grafana-Certresolver auf letsencrypt korrigieren
Das Label nannte den Resolver "le", Traefik kennt ihn aber als
"letsencrypt" (--certificatesresolvers.letsencrypt.acme.*). Traefik
protokollierte daher "Router uses a nonexistent certificate resolver"
und lieferte fuer selendis.axion1337.de sein Default-Self-Signed-Cert
aus. Der Fehler stammt aus dem Altbestand in /opt/monitoring und wurde
bei der Bestandsaufnahme unveraendert uebernommen.

Verifiziert: Let's Encrypt-Cert (YR2) ausgestellt, gueltig bis
2026-10-28, https://selendis.axion1337.de/login antwortet mit 200.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 14:00:58 +02:00
sorbandClaude Opus 5 9deb205dce Merge branch 'rework/monitoring'
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-30 12:13:01 +02:00
16 changed files with 963 additions and 2 deletions
+28
View File
@@ -0,0 +1,28 @@
# AGENTS.md — threadnet-operating
> **Die Gruppenregeln sind kanonisch im `management`-Repo:**
> [`AGENTS.md`](https://git.lab/axion1337.chat/management/-/blob/main/AGENTS.md)
> — von außerhalb des Labs über den Gitea-Mirror lesbar:
> `https://rohana.axion1337.de/sorb/management`. Dort stehen Repo-Topologie und
> Mirror-Regeln, das Kanban-Framework (Status-Labels, WIP-Limit 2, ADR-Pflicht),
> Deploy-Übergabe und AAR-Verfahren, Secrets-Handhabung und die
> Karpathy-Leitlinien. Sie gelten für **jede** Session in diesem Repo.
> Hier steht nur, was zusätzlich für dieses Repository gilt.
## Was dieses Repo ist
Monitoring-Stack auf **CFGMON**: Prometheus, Loki, Grafana, Alloy, Alertmanager und der
CVE-Exporter — als Compose, **nicht** von Flux verwaltet. Deploy manuell auf dem Host
(`/opt/threadnet-operating/monitoring`).
## Was hier besonders zählt
- **Alarmregeln** liegen in `monitoring/prometheus/alerts.yml`; die Zustellung läuft über
`alertmanager/matrix-alerts.py` in den Matrix-Security-Raum. Eine Regel, die nie feuern
kann, ist schlimmer als keine — **fehlende Zeitreihen erzeugen Stille, keinen Alarm**
(deshalb die `or`-Fallbacks in `axion-backup`).
- **Config-Änderungen kommen nicht automatisch an.** Prometheus und Alertmanager mounten seit
2026-08-15 ihr Config-**Verzeichnis**; bei den übrigen Diensten (loki, alloy, die Skripte)
hängt der Einzeldatei-Mount weiter am Inode und braucht `--force-recreate` nach `git pull`.
Details und Prüfbefehl in `monitoring/README.md`.
- Ob eine Änderung wirklich greift, sieht man **nur im Container**, nie auf der Platte.
+1
View File
@@ -0,0 +1 @@
Read AGENTS.md — the canonical instruction file for this repository. All rules live there.
+35
View File
@@ -0,0 +1,35 @@
# threadnet-operating
Der Betriebs-/Monitoring-Stack für den Operating-Host **CFGMON**: Prometheus,
Loki, Grafana, Alloy, Alertmanager und der CVE-Exporter — vollständig als Code,
ein `docker compose up -d` stellt ihn auf einem frischen Host wieder her.
**→ [`monitoring/README.md`](monitoring/README.md)** ist die eigentliche
Betriebsanleitung (Deployment, Config-Fallen, Alerting, CVE-Pipeline).
## Wo was liegt
| Pfad | Inhalt |
|---|---|
| `monitoring/` | der Stack: Compose, Prometheus, Loki, Grafana, Alertmanager, Alloy |
| `monitoring/cve/` | CVE-Exporter (Trivy-Scan → Prometheus-Metriken), [ADR-0003](https://git.lab/axion1337.chat/management/-/blob/main/decisions/0003-cve-meldeweg-aggregiert.md) |
| `monitoring/grafana/` | Datasources und Dashboards als Code |
## Offene Punkte
Kein Backlog in diesem Repo. Offene Punkte sind **Issues** im
[management-Projekt](https://git.lab/axion1337.chat/management/-/issues)
([ADR-0005](https://git.lab/axion1337.chat/management/-/blob/main/decisions/0005-pm-framework-kanban.md)) —
sie betreffen meist mehrere Hosts, eine Liste je Repo würde auseinanderlaufen.
Für diesen Stack einschlägig sind unter anderem
[#8 Remote-Write und Loki ohne Auth](https://git.lab/axion1337.chat/management/-/issues/8),
[#9 Grafana-Credentials](https://git.lab/axion1337.chat/management/-/issues/9) und
[#10 Gitea-Backups off-host](https://git.lab/axion1337.chat/management/-/issues/10);
Bestand und Historie zum Host stehen in
[`hosts/cfgmon.md`](https://git.lab/axion1337.chat/management/-/blob/main/hosts/cfgmon.md).
**Kanonisch ist git.lab** ([ADR-0001](https://git.lab/axion1337.chat/management/-/blob/main/decisions/0001-gitlab-kanonisch-push-mirror.md),
[ADR-0002](https://git.lab/axion1337.chat/management/-/blob/main/decisions/0002-issues-und-management-ins-lab.md)).
Von außerhalb des Labs ist derselbe Stand über den Push-Mirror
[`sorb/management`](https://rohana.axion1337.de/sorb/management) lesbar — dorthin
aber **nie pushen**, der Mirror überschreibt.
+14
View File
@@ -2,3 +2,17 @@
# Werte in Single-Quotes, damit Sonderzeichen shell-safe sind.
GRAFANA_ADMIN_USER='admin'
GRAFANA_ADMIN_PASSWORD='changeme'
# Alerting (gitops#32) - Matrix-Zustellung durch den eigenen Bot @alerts in den
# dedizierten Alerts-Raum im Operating-Space (Entscheidung 2026-07-31: eigener Bot
# + eigener Raum, Token-Trennung vom maintenance-notify-Bot auf dem MATRIX-Host).
# Token erzeugen (Wert direkt hier eintragen, nicht in Chats/Logs):
# kubectl exec -n matrix deploy/matrix-stack-matrix-authentication-service -- \
# mas-cli manage issue-compatibility-token alerts
# Bot ist dem Raum bereits beigetreten (2026-07-31, inkl. Avatar + Testnachricht).
MATRIX_ALERT_HOMESERVER=https://matrix.axion1337.chat
MATRIX_ALERT_ROOM_ID=!qavWkXbhLPfGvqtifj:axion1337.chat
MATRIX_ALERT_TOKEN=changeme
# CVE-/Release-Raum fuer release-watch (gitops#47) - @alerts dort einladen!
MATRIX_RELEASE_ROOM_ID=!YRJvcEbVXtRlUIkNld:axion1337.chat
+93
View File
@@ -13,6 +13,45 @@ 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:
```bash
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:
```bash
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 |
@@ -23,6 +62,9 @@ docker compose up -d
| `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
@@ -42,6 +84,57 @@ 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
- `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
+16
View File
@@ -0,0 +1,16 @@
# Alertmanager (gitops#32): alles an den Matrix-Receiver (wartung-Raum).
route:
receiver: matrix
group_by: [alertname, instance]
group_wait: 1m
group_interval: 5m
repeat_interval: 4h
# CVE-Alarme laufen seit gitops#51 aggregiert (eine Instanz pro Image statt
# pro CVE) und wieder ueber den Matrix-Receiver — Historie des Alarm-Sturms
# und der Stummschaltung: gitops#51 / AAR 2026-08-01.
receivers:
- name: matrix
webhook_configs:
- url: http://matrix-alerts:8080/alert
send_resolved: true
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env python3
# Minimaler Alertmanager-Webhook -> Matrix-Raum (gitops#32). Gleiche Machart wie
# maintenance-notify (Issue #24 im gitops-Repo): purer Stdlib-HTTP-Server, Bot-Token
# aus der Umgebung, Nachricht per Client-Server-API.
#
# Ein Alarm = EINE Nachricht: beim Firing wird pro Alarm (Alertmanager-Fingerprint)
# eine Nachricht gesendet und deren Event-ID gemerkt; beim Resolved wird dieselbe
# Nachricht per m.replace-Edit durchgestrichen und abgehakt statt eine neue zu
# posten (Wunsch sorb 2026-08-01: append-only wird unuebersichtlich). Die Zuordnung
# ueberlebt Container-Restarts via State-Datei; ohne Zuordnung (z.B. nach Neubau)
# faellt Resolved auf eine eigenstaendige ✅-Nachricht zurueck.
import html
import json
import os
import time
import urllib.parse
import urllib.request
import uuid
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HOMESERVER = os.environ["MATRIX_HOMESERVER"]
ROOM_ID = os.environ["MATRIX_ROOM_ID"]
TOKEN = os.environ["MATRIX_TOKEN"]
STATE_FILE = os.environ.get("MATRIX_STATE_FILE", "/tmp/matrix-alerts-state.json")
# Raum-Routing (gitops#47): Alerts mit Label room="<name>" landen im Raum aus
# MATRIX_ROOM_<NAME> (z.B. room="security" -> MATRIX_ROOM_SECURITY), alles
# andere im Default-Raum. Der Bot muss in jedem Zielraum Mitglied sein.
ROOM_MAP = {k[len("MATRIX_ROOM_"):].lower(): v
for k, v in os.environ.items()
if k.startswith("MATRIX_ROOM_") and k != "MATRIX_ROOM_ID" and v}
try:
with open(STATE_FILE) as f:
_persisted = json.load(f)
state = _persisted.get("alerts", _persisted) # alt: flaches Format
recent_resolved = _persisted.get("recent_resolved", [])
except Exception:
state = {}
recent_resolved = []
def save_state():
try:
with open(STATE_FILE, "w") as f:
json.dump({"alerts": state, "recent_resolved": recent_resolved[-200:]}, f)
except Exception as e:
print(f"state save failed: {e}", flush=True)
def matrix_put(content, room=ROOM_ID):
url = (f"{HOMESERVER}/_matrix/client/v3/rooms/{urllib.parse.quote(room)}"
f"/send/m.room.message/{uuid.uuid4()}")
req = urllib.request.Request(url, data=json.dumps(content).encode(), method="PUT",
headers={"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json"})
return json.loads(urllib.request.urlopen(req, timeout=10).read()).get("event_id")
def send_text(text, room=ROOM_ID):
return matrix_put({"msgtype": "m.text", "body": text}, room)
def edit_resolved(event_id, old_text, room=ROOM_ID):
# m.replace-Edit: Original wird in Element in-place ersetzt (durchgestrichen + Haken)
new_body = f"✅ ~~{old_text}~~"
new_html = f"✅ <del>{html.escape(old_text)}</del>"
matrix_put({
"msgtype": "m.text",
"body": f"* {new_body}",
"m.new_content": {"msgtype": "m.text", "body": new_body,
"format": "org.matrix.custom.html", "formatted_body": new_html},
"m.relates_to": {"rel_type": "m.replace", "event_id": event_id},
}, room)
class Handler(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/alert":
self.send_response(404); self.end_headers(); return
data = json.loads(self.rfile.read(int(self.headers.get("Content-Length", 0))))
failed = 0
for a in data.get("alerts", []):
name = a.get("labels", {}).get("alertname", "?")
summary = a.get("annotations", {}).get("summary", "")
fp = a.get("fingerprint", "")
room = ROOM_MAP.get(a.get("labels", {}).get("room", ""), ROOM_ID)
try:
if a.get("status") == "firing":
if fp in state: # re-notify/Batch-Retry -> keine Doppelnachricht
continue
text = f"\U0001F534 [firing] {name}: {summary}"
event_id = send_text(text, room)
if fp and event_id:
state[fp] = {"event_id": event_id, "text": text, "room": room}
else:
if fp in recent_resolved: # Batch-Retry -> Fallback nicht doppeln
continue
known = state.pop(fp, None)
if known:
edit_resolved(known["event_id"], known["text"], known.get("room", ROOM_ID))
else:
send_text(f"✅ [resolved] {name}: {summary}", room)
recent_resolved.append(fp)
save_state() # inkrementell: Teilfortschritt uebersteht Fehler/Retry
time.sleep(1) # Synapse-Ratelimit (rc_message) nicht reizen
except Exception as e:
failed += 1
self.log_message("matrix send failed (%s): %s", name, e)
save_state()
time.sleep(2)
if failed:
# Alertmanager wiederholt den Batch; Dedup liefert nur den Rest nach
self.send_response(502); self.end_headers(); return
self.send_response(200); self.end_headers()
def log_message(self, fmt, *args):
print(fmt % args, flush=True)
ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
+99
View File
@@ -0,0 +1,99 @@
#!/usr/bin/env python3
# Release-/Advisory-Watch fuer den Element-Stack (gitops#22). Gleiche Machart wie
# matrix-alerts.py: purer Stdlib-Daemon, Bot-Token aus der Umgebung, Nachricht per
# Client-Server-API in den Alerts-Raum.
#
# Quelle sind die oeffentlichen GitHub-Release-Atom-Feeds (kein API-Token noetig).
# Element veroeffentlicht Security-Fixes als Releases - der Feed ist damit der
# praktikable Advisory-Kanal; echte GHSA-Advisories haben keinen oeffentlichen Feed.
# Neue Eintraege werden einmalig als 📦-Notiz gemeldet; Security-verdaechtige
# Titel/Inhalte (CVE/security/vulnerab...) bekommen 🚨 und stehen vorn.
#
# Abgrenzung (Frage sorb 2026-08-01): REPOS unten ist eine HANDGEPFLEGTE Liste -
# de facto ein Mini-SBOM auf Repo-Granularitaet, ohne Versions-/Dependency-Wissen.
# Bei Stack-Aenderungen (neue Komponente, Fork-Wechsel) muss sie mitgezogen werden.
# Trivy (gitops#31/#47) braucht dagegen keine Pflege: es leitet sein SBOM selbst
# aus den Images ab. Beide ergaenzen sich, ersetzen sich nicht.
import json
import os
import re
import time
import urllib.parse
import urllib.request
import uuid
import xml.etree.ElementTree as ET
HOMESERVER = os.environ["MATRIX_HOMESERVER"]
# Eigener CVE-/Release-Raum (Entscheidung sorb 2026-08-01, gitops#47);
# Fallback auf den Alerts-Raum, solange der Bot dort noch nicht eingeladen ist.
ROOM_ID = os.environ.get("MATRIX_RELEASE_ROOM_ID") or os.environ["MATRIX_ROOM_ID"]
TOKEN = os.environ["MATRIX_TOKEN"]
STATE_FILE = os.environ.get("RELEASE_WATCH_STATE_FILE", "/state/release-watch.json")
INTERVAL = int(os.environ.get("RELEASE_WATCH_INTERVAL", "21600")) # 6h
REPOS = [
"element-hq/synapse",
"element-hq/ess-helm",
"element-hq/element-web",
"element-hq/matrix-authentication-service",
"element-hq/element-call",
]
SECURITY_RE = re.compile(r"cve|security|vulnerab|advisory", re.IGNORECASE)
ATOM = "{http://www.w3.org/2005/Atom}"
try:
with open(STATE_FILE) as f:
seen = json.load(f) # repo -> [entry ids]
except Exception:
seen = {}
def send_notice(text):
url = (f"{HOMESERVER}/_matrix/client/v3/rooms/{urllib.parse.quote(ROOM_ID)}"
f"/send/m.room.message/{uuid.uuid4()}")
req = urllib.request.Request(url, data=json.dumps({"msgtype": "m.notice", "body": text}).encode(),
method="PUT", headers={"Authorization": f"Bearer {TOKEN}",
"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=10).read()
def check(repo):
feed = urllib.request.urlopen(f"https://github.com/{repo}/releases.atom", timeout=20).read()
root = ET.fromstring(feed)
entries = root.findall(f"{ATOM}entry")
known = set(seen.get(repo, []))
first_run = repo not in seen
new = []
for e in entries:
eid = e.findtext(f"{ATOM}id", "")
title = e.findtext(f"{ATOM}title", "?").strip()
link = ""
le = e.find(f"{ATOM}link")
if le is not None:
link = le.get("href", "")
content = e.findtext(f"{ATOM}content", "") or ""
if eid and eid not in known:
new.append((eid, title, link, bool(SECURITY_RE.search(title + " " + content[:2000]))))
# Erstlauf: nur Stand merken, nicht den Raum mit Historie fluten
seen[repo] = [e.findtext(f"{ATOM}id", "") for e in entries][:30]
if first_run:
return
for eid, title, link, is_sec in new:
icon = "\U0001F6A8" if is_sec else "\U0001F4E6"
kind = "Security-verdaechtiges Release" if is_sec else "Neues Release"
send_notice(f"{icon} {kind}: {repo}{title}\n{link}")
while True:
for repo in REPOS:
try:
check(repo)
except Exception as exc:
print(f"{repo}: {exc}", flush=True)
try:
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(seen, f)
except Exception as exc:
print(f"state: {exc}", flush=True)
time.sleep(INTERVAL)
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
# Trivy-JSON -> Prometheus-Metriken (gitops#47). Stdlib-only, Machart wie die
# uebrigen Monitoring-Helfer. Liest die Reports des cve-scan-Sidecars und
# serviert /metrics; merkt sich je (CVE, Target) den Erstfund (first_seen),
# damit der geforderte Zeitstrahl (erstmals gesehen / geschlossen) abbildbar
# ist - "geschlossen" = Serie verschwindet, resolved kommt via Alertmanager.
#
# Schema (Pflichtfelder-Vorgabe sorb: CVE-ID, Mitigation, Zeitstrahl, Ort, Typ):
# trivy_vuln_info{cve,severity,target,target_type,host,pkg,installed,fixed_version} 1
# (nur HIGH/CRITICAL als Einzelserien - Kardinalitaet)
# trivy_vuln_count{target,target_type,host,severity} <n> (alle Severities)
# trivy_vuln_first_seen_timestamp{cve,target} <unix>
# trivy_last_scan_timestamp{target,target_type,host} <unix>
import json
import os
import time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
RESULTS = os.environ.get("RESULTS_DIR", "/results")
STATE_FILE = os.environ.get("STATE_FILE", "/state/first-seen.json")
# Ort: Container-Images laufen (bis auf Weiteres) alle auf dem MATRIX-Host;
# Host-rootfs-Scans (Ausbaustufe) bringen ihren Hostnamen im Dateinamen mit.
DEFAULT_HOST = os.environ.get("DEFAULT_HOST", "matrix")
try:
with open(STATE_FILE) as f:
first_seen = json.load(f) # "cve|target" -> unix-ts
except Exception:
first_seen = {}
def esc(v):
return str(v).replace("\\", "\\\\").replace('"', '\\"').replace("\n", " ")
def collect():
lines = []
now = int(time.time())
seen_keys = set()
gelesene_targets = set() # nur DEREN Erstfunde duerfen geprunt werden
berichte = 0
lesefehler = 0
for fn in sorted(os.listdir(RESULTS)):
if not fn.endswith(".json"):
continue
berichte += 1
path = os.path.join(RESULTS, fn)
try:
rep = json.load(open(path))
except Exception as e:
# Frueher: stilles 'continue'. Ein einmaliger Lesefehler (Datei wird
# gerade geschrieben, kurzer I/O-Fehler) liess die Findings dieses
# Targets aus seen_keys verschwinden - und der Prune unten loeschte
# ihre Erstfund-Zeitstempel DAUERHAFT. Der Zeitstrahl war damit weg,
# ohne dass irgendetwas gemeldet haette. Jetzt zaehlbar und sichtbar.
lesefehler += 1
print(f"report unlesbar: {fn}: {e}", flush=True)
continue
target = rep.get("ArtifactName", fn[:-5])
gelesene_targets.add(target)
ttype = "host" if rep.get("ArtifactType") in ("filesystem", "rootfs") else "image"
host = fn.split("__host__")[1].split(".json")[0] if "__host__" in fn else DEFAULT_HOST
base = f'target="{esc(target)}",target_type="{ttype}",host="{esc(host)}"'
lines.append(f'trivy_last_scan_timestamp{{{base}}} {int(os.path.getmtime(path))}')
counts = {}
for res in rep.get("Results") or []:
for v in res.get("Vulnerabilities") or []:
sev = v.get("Severity", "UNKNOWN")
counts[sev] = counts.get(sev, 0) + 1
if sev in ("HIGH", "CRITICAL"):
cve = v.get("VulnerabilityID", "?")
key = f"{cve}|{target}"
if key not in first_seen:
first_seen[key] = now
seen_keys.add(key)
lines.append(
'trivy_vuln_info{cve="%s",severity="%s",%s,pkg="%s",installed="%s",fixed_version="%s"} 1'
% (esc(cve), sev, base, esc(v.get("PkgName", "?")),
esc(v.get("InstalledVersion", "?")), esc(v.get("FixedVersion", ""))))
lines.append(f'trivy_vuln_first_seen_timestamp{{cve="{esc(cve)}",target="{esc(target)}"}} {first_seen[key]}')
for sev, n in sorted(counts.items()):
lines.append(f'trivy_vuln_count{{{base},severity="{sev}"}} {n}')
# State kompakt halten: verschwundene Findings raus (= "geschlossen").
# ABER nur fuer Targets, deren Bericht in DIESEM Durchgang auch wirklich
# gelesen wurde. Sonst loescht ein einzelner Lesefehler den Zeitstrahl eines
# Targets, das es noch gibt - unwiederbringlich, weil "erstmals gesehen"
# danach auf 'jetzt' neu anfaengt.
for k in list(first_seen):
ziel = k.split("|", 1)[1] if "|" in k else ""
if ziel in gelesene_targets and k not in seen_keys:
del first_seen[k]
# Sichtbarkeit des Scanners selbst: ein still gestorbener Scanner macht blind,
# und TrivyScanStale kann ein Target, das NIE einen Bericht hatte, nicht melden
# (ohne Serie kein time()-Vergleich). Diese beiden Zahlen schliessen die Luecke
# so weit, wie sie ohne Soll-Liste zu schliessen ist.
lines.append(f"trivy_reports_total {berichte}")
lines.append(f"trivy_report_read_errors {lesefehler}")
try:
os.makedirs(os.path.dirname(STATE_FILE), exist_ok=True)
with open(STATE_FILE, "w") as f:
json.dump(first_seen, f)
except Exception as e:
print(f"state save failed: {e}", flush=True)
return "\n".join(lines) + "\n"
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/metrics":
self.send_response(404); self.end_headers(); return
body = collect().encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain; version=0.0.4")
self.end_headers()
self.wfile.write(body)
def log_message(self, fmt, *args):
pass
ThreadingHTTPServer(("0.0.0.0", 9101), Handler).serve_forever()
+29
View File
@@ -0,0 +1,29 @@
clamav/clamav:1.5.3
coturn/coturn:latest
docker.io/grafana/alloy:v1.7.5
docker.io/library/haproxy:3.2-alpine
docker.io/library/postgres:17.9-bookworm
docker.io/livekit/livekit-server:v1.10.0
docker.io/postgres:17-alpine
docker.io/prometheuscommunity/postgres-exporter:v0.18.1
ghcr.io/element-hq/ess-helm/matrix-tools:0.7.3
ghcr.io/element-hq/matrix-authentication-service:1.15.0
ghcr.io/fluxcd/helm-controller:v1.5.3
ghcr.io/fluxcd/kustomize-controller:v1.8.3
ghcr.io/fluxcd/notification-controller:v1.8.3
ghcr.io/fluxcd/source-controller:v1.8.2
ghcr.io/goauthentik/server:2026.2.3
gnuxie/draupnir:v3.1.0
nginx:1.26-alpine
oci.element.io/element-admin:0.1.11
oci.element.io/lk-jwt-service:0.4.2
oci.element.io/synapse:v1.151.0-ess.1
quay.io/jetstack/cert-manager-cainjector:v1.14.0
quay.io/jetstack/cert-manager-controller:v1.14.0
quay.io/jetstack/cert-manager-webhook:v1.14.0
quay.io/prometheus-operator/prometheus-config-reloader:v0.81.0
registry.k8s.io/kube-state-metrics/kube-state-metrics:v2.15.0
rohana.axion1337.de/sorb/axion-backup:v2
rohana.axion1337.de/sorb/axion-secret-rotation:v1
rohana.axion1337.de/sorb/clamav-http-scanner:v1.0.0
rohana.axion1337.de/sorb/threadnet-web:v0.3.0
+23
View File
@@ -0,0 +1,23 @@
#!/bin/sh
# Trivy-Scan-Schleife (gitops#47): scannt alle 24h die Liste der real deployten
# Images (cve/images.txt, aus dem Cluster inventarisiert) und legt die
# JSON-Reports fuer den cve-exporter ab. Kein Abbruch bei Einzelfehlern -
# ein nicht mehr pullbares Image darf den Rest nicht verhindern (der Exporter
# alarmiert ueber trivy_last_scan_timestamp, wenn ein Report veraltet).
set -u
RESULTS=/results
IMAGES=/config/images.txt
INTERVAL="${SCAN_INTERVAL_SECONDS:-86400}"
while true; do
while IFS= read -r img; do
case "$img" in ""|\#*) continue;; esac
safe=$(echo "$img" | tr '/:@' '___')
echo "scan: $img"
trivy image --scanners vuln --format json --output "$RESULTS/$safe.json.tmp" "$img" \
&& mv "$RESULTS/$safe.json.tmp" "$RESULTS/$safe.json" \
|| echo "FEHLER bei $img (Report bleibt auf altem Stand)"
done < "$IMAGES"
echo "runde fertig, schlafe ${INTERVAL}s"
sleep "$INTERVAL"
done
+110 -2
View File
@@ -4,7 +4,12 @@ services:
container_name: prometheus
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
# Verzeichnis- statt Einzeldatei-Mount: Docker haengt Einzeldatei-Mounts am
# Inode auf, und `git pull` ersetzt Dateien per Rename (neuer Inode) - der
# Container sah die Aenderung dann nie, waehrend SIGHUP brav Erfolg meldete.
# Verzeichnis-Mounts loesen bei jedem Zugriff ueber den Pfad auf.
# Config-Pfade bleiben unveraendert (--config.file zeigt weiter dorthin).
- ./prometheus:/etc/prometheus:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
@@ -20,6 +25,98 @@ services:
networks:
- traefik
alertmanager:
image: prom/alertmanager:v0.28.1
container_name: alertmanager
restart: unless-stopped
volumes:
# Verzeichnis-Mount, gleicher Grund wie bei Prometheus. Die beiden .py aus
# diesem Ordner liegen dadurch mit unter /etc/alertmanager - ungenutzt und
# harmlos, alertmanager laedt ausschliesslich --config.file.
- ./alertmanager:/etc/alertmanager:ro
- alertmanager_data:/alertmanager
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
- '--storage.path=/alertmanager'
# bewusst kein oeffentlicher Port - nur Prometheus/Receiver im traefik-Netz
networks:
- traefik
# Alertmanager-Webhook -> Matrix (wartung-Raum), gleiche Machart wie
# maintenance-notify (gitops Issue #24). Secrets kommen aus .env.
matrix-alerts:
image: python:3.13-slim
container_name: matrix-alerts
restart: unless-stopped
environment:
- MATRIX_HOMESERVER=${MATRIX_ALERT_HOMESERVER}
- MATRIX_ROOM_ID=${MATRIX_ALERT_ROOM_ID}
- MATRIX_TOKEN=${MATRIX_ALERT_TOKEN}
# Raum-Routing (gitops#47): Alerts mit Label room=security -> Security-Raum
- MATRIX_ROOM_SECURITY=${MATRIX_RELEASE_ROOM_ID:-}
# Ohne das liegt der State unter /tmp im Writable Layer: der ueberlebt
# zwar ein "compose restart", aber kein "up -d", das den Container neu
# baut -- also genau jeden Deploy. Dann verlieren offene Alarme ihre
# Event-Zuordnung und loesen sich ueber den Fallback als separate
# Nachricht auf, statt die Firing-Nachricht abzuhaken.
- MATRIX_STATE_FILE=/state/matrix-alerts-state.json
volumes:
- ./alertmanager/matrix-alerts.py:/app/matrix-alerts.py:ro
- matrix_alerts_data:/state
command: python3 /app/matrix-alerts.py
networks:
- traefik
# Release-/Advisory-Watch (gitops#22): meldet neue Releases der Element-Stack-
# Upstreams in den Alerts-Raum (🚨 bei Security-Verdacht). Gleicher Bot/Raum
# wie matrix-alerts, eigener State (Erstlauf merkt nur, flutet nicht).
release-watch:
image: python:3.13-slim
container_name: release-watch
restart: unless-stopped
environment:
- MATRIX_HOMESERVER=${MATRIX_ALERT_HOMESERVER}
- MATRIX_ROOM_ID=${MATRIX_ALERT_ROOM_ID}
# CVE-/Release-Raum (gitops#47): leer lassen = Fallback Alerts-Raum
- MATRIX_RELEASE_ROOM_ID=${MATRIX_RELEASE_ROOM_ID:-}
- MATRIX_TOKEN=${MATRIX_ALERT_TOKEN}
volumes:
- ./alertmanager/release-watch.py:/app/release-watch.py:ro
- release_watch_data:/state
command: python3 /app/release-watch.py
networks:
- traefik
# CVE-Pipeline (gitops#47), Teil 1: Trivy scannt die real deployten Images
# (cve/images.txt - Inventur aus dem Cluster, bei Stack-Aenderungen nachziehen)
cve-scan:
image: aquasec/trivy:0.58.2
container_name: cve-scan
restart: unless-stopped
entrypoint: ["/bin/sh", "/config/scan-loop.sh"]
volumes:
- ./cve:/config:ro
- cve_results:/results
- cve_trivy_cache:/root/.cache
networks:
- traefik
# Teil 2: Reports -> Prometheus-Metriken (Schema + Pflichtfelder siehe gitops#47)
cve-exporter:
image: python:3.13-slim
container_name: cve-exporter
restart: unless-stopped
environment:
- RESULTS_DIR=/results
- STATE_FILE=/state/first-seen.json
volumes:
- ./cve/cve-exporter.py:/app/cve-exporter.py:ro
- cve_results:/results:ro
- cve_exporter_state:/state
command: python3 /app/cve-exporter.py
networks:
- traefik
loki:
image: grafana/loki:3.7.1
container_name: loki
@@ -44,6 +141,10 @@ services:
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
# Muss persistent sein, sonst ist die Positions-Datei nach jedem
# Recreate weg und Alloy liest alle Container-Logs von vorn. Loki
# weist die alten Eintraege dann ab (reject_old_samples, Default 7d).
- alloy_data:/var/lib/alloy
command: run --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy
cap_add:
- DAC_READ_SEARCH
@@ -72,7 +173,7 @@ services:
- "traefik.docker.network=traefik"
- "traefik.http.routers.grafana.rule=Host(`selendis.axion1337.de`)"
- "traefik.http.routers.grafana.entrypoints=websecure"
- "traefik.http.routers.grafana.tls.certresolver=le"
- "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
depends_on:
- prometheus
@@ -101,8 +202,15 @@ services:
volumes:
prometheus_data:
alertmanager_data:
matrix_alerts_data:
release_watch_data:
cve_results:
cve_trivy_cache:
cve_exporter_state:
grafana_data:
loki_data:
alloy_data:
networks:
traefik:
@@ -0,0 +1,64 @@
{
"title": "CVE-Übersicht (Trivy)",
"uid": "cve-overview",
"tags": ["security", "cve", "gitops-47"],
"timezone": "browser",
"schemaVersion": 39,
"refresh": "5m",
"time": { "from": "now-30d", "to": "now" },
"panels": [
{
"type": "stat", "title": "Offene CRITICAL", "id": 1,
"gridPos": { "x": 0, "y": 0, "w": 4, "h": 4 },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 1 } ] } }, "overrides": [] },
"targets": [ { "expr": "sum(trivy_vuln_count{severity=\"CRITICAL\"}) or vector(0)", "instant": true, "refId": "A" } ]
},
{
"type": "stat", "title": "Offene HIGH", "id": 2,
"gridPos": { "x": 4, "y": 0, "w": 4, "h": 4 },
"fieldConfig": { "defaults": { "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "orange", "value": 1 } ] } }, "overrides": [] },
"targets": [ { "expr": "sum(trivy_vuln_count{severity=\"HIGH\"}) or vector(0)", "instant": true, "refId": "A" } ]
},
{
"type": "stat", "title": "MEDIUM/LOW (Summe)", "id": 3,
"gridPos": { "x": 8, "y": 0, "w": 4, "h": 4 },
"targets": [ { "expr": "sum(trivy_vuln_count{severity=~\"MEDIUM|LOW\"}) or vector(0)", "instant": true, "refId": "A" } ]
},
{
"type": "stat", "title": "Ältester Scan (Stunden)", "id": 4,
"gridPos": { "x": 12, "y": 0, "w": 4, "h": 4 },
"fieldConfig": { "defaults": { "unit": "h", "color": { "mode": "thresholds" }, "thresholds": { "mode": "absolute", "steps": [ { "color": "green", "value": null }, { "color": "red", "value": 48 } ] } }, "overrides": [] },
"targets": [ { "expr": "(time() - min(trivy_last_scan_timestamp)) / 3600", "instant": true, "refId": "A" } ]
},
{
"type": "table", "title": "Offene CVEs (HIGH/CRITICAL) — CVE · Ort · Typ · Paket · Fix · seit", "id": 5,
"gridPos": { "x": 0, "y": 4, "w": 24, "h": 12 },
"targets": [
{ "expr": "trivy_vuln_info", "instant": true, "format": "table", "refId": "A" },
{ "expr": "trivy_vuln_first_seen_timestamp * 1000", "instant": true, "format": "table", "refId": "B" }
],
"transformations": [
{ "id": "merge", "options": {} },
{ "id": "organize", "options": {
"excludeByName": { "Time": true, "__name__": true, "job": true, "instance": true, "Value #A": true },
"renameByName": { "cve": "CVE", "severity": "Severity", "host": "Ort (Host)", "target_type": "Typ", "target": "Target", "pkg": "Paket", "installed": "Installiert", "fixed_version": "Fix-Version", "Value #B": "Erstmals gesehen" }
} }
],
"fieldConfig": { "defaults": {}, "overrides": [
{ "matcher": { "id": "byName", "options": "Erstmals gesehen" }, "properties": [ { "id": "unit", "value": "dateTimeAsIso" } ] },
{ "matcher": { "id": "byName", "options": "CVE" }, "properties": [ { "id": "links", "value": [ { "title": "NVD (Mitigation/Details)", "targetBlank": true, "url": "https://nvd.nist.gov/vuln/detail/${__value.text}" } ] } ] }
] }
},
{
"type": "state-timeline", "title": "Zeitstrahl: offene HIGH/CRITICAL je Target", "id": 6,
"gridPos": { "x": 0, "y": 16, "w": 24, "h": 8 },
"targets": [ { "expr": "count by (target) (trivy_vuln_info)", "legendFormat": "{{target}}", "refId": "A" } ],
"fieldConfig": { "defaults": { "custom": { "fillOpacity": 70 } }, "overrides": [] }
},
{
"type": "timeseries", "title": "Funde je Severity über Zeit", "id": 7,
"gridPos": { "x": 0, "y": 24, "w": 24, "h": 8 },
"targets": [ { "expr": "sum by (severity) (trivy_vuln_count)", "legendFormat": "{{severity}}", "refId": "A" } ]
}
]
}
@@ -37,3 +37,10 @@ providers:
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards/general
- name: security
folder: 'Security'
type: file
allowUiUpdates: true
options:
path: /var/lib/grafana/dashboards/security
+181
View File
@@ -0,0 +1,181 @@
# Alert-Regeln (gitops#32). Bewusst wenige, dafuer die real erlebten Fehlerklassen:
# - TargetDown: "Exporter/Dienst tot und keiner merkt es" (coturn-Klasse)
# - KubePodRestartLoop: Crashloops im k3s-Cluster (36k-coturn-Restarts-Klasse)
# - HostOom/HostLowMemory/HostLowDisk/HostHighSwap: Ressourcendruck der Hosts
groups:
- name: axion-basics
rules:
- alert: TargetDown
expr: up == 0
for: 5m
labels:
severity: critical
annotations:
summary: "Target {{ $labels.job }}/{{ $labels.instance }} ist seit 5m nicht erreichbar"
- alert: KubePodRestartLoop
expr: increase(kube_pod_container_status_restarts_total[1h]) > 3
for: 10m
labels:
severity: warning
annotations:
summary: "Pod {{ $labels.namespace }}/{{ $labels.pod }} restartet wiederholt ({{ $value | printf \"%.0f\" }}x in 1h)"
- alert: HostOomKill
expr: increase(node_vmstat_oom_kill[15m]) > 0
labels:
severity: warning
annotations:
summary: "OOM-Kill auf {{ $labels.instance }}"
- alert: HostLowMemory
expr: node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes < 0.08
for: 15m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }}: <8% RAM verfuegbar seit 15m"
- alert: HostHighSwap
expr: (node_memory_SwapTotal_bytes - node_memory_SwapFree_bytes) / node_memory_SwapTotal_bytes > 0.5
for: 30m
labels:
severity: warning
annotations:
summary: "{{ $labels.instance }}: >50% Swap in Nutzung seit 30m"
- alert: HostLowDisk
expr: node_filesystem_avail_bytes{mountpoint="/",fstype!~"tmpfs|overlay"} / node_filesystem_size_bytes{mountpoint="/",fstype!~"tmpfs|overlay"} < 0.10
for: 15m
labels:
severity: critical
annotations:
summary: "{{ $labels.instance }}: <10% Platz auf / frei"
# CVE-Pipeline (gitops#47): Funde aus den Trivy-Scans. Pflichtfelder je Alarm:
# CVE-ID, Mitigation (Fix-Version + NVD-Link), Ort (host), Typ (target_type);
# der Zeitstrahl kommt aus trivy_vuln_first_seen_timestamp + resolved-Edit.
# Alle Regeln routen per room-Label in den Security-Raum.
- name: axion-cve
rules:
# Aggregiert pro Image statt pro CVE (gitops#51, Entscheidung sorb
# 2026-08-01): ~1000 Einzelalarme waeren Rauschen; die menschlich
# handhabbare Einheit ist "Image X hat N kritische CVEs". Die
# CVE-Details (Pflichtfelder) liegen im Grafana-Dashboard cve-overview,
# die trivy_vuln_info-Einzelserien bleiben dafuer erhalten.
- alert: TrivyCriticalVulns
expr: count by (target, target_type, host) (trivy_vuln_info{severity="CRITICAL"}) > 0
labels:
severity: critical
room: security
annotations:
summary: "{{ $labels.target }} [{{ $labels.target_type }}@{{ $labels.host }}]: {{ $value }} CRITICAL-CVEs — Details: https://selendis.axion1337.de/d/cve-overview"
- alert: TrivyHighVulns
expr: count by (target, target_type, host) (trivy_vuln_info{severity="HIGH"}) > 0
for: 24h
labels:
severity: warning
room: security
annotations:
summary: "{{ $labels.target }} [{{ $labels.target_type }}@{{ $labels.host }}]: {{ $value }} HIGH-CVEs (seit 24h offen) — Details: https://selendis.axion1337.de/d/cve-overview"
- alert: TrivyScanStale
expr: time() - trivy_last_scan_timestamp > 172800
labels:
severity: warning
room: security
annotations:
summary: "CVE-Scan fuer {{ $labels.target }} ist aelter als 2 Tage — Scanner pruefen (ein still gestorbener Scanner macht blind)"
# TrivyScanStale hat eine Blindstelle, die es prinzipiell nicht schliessen
# kann: ein Target OHNE je erfolgreichen Bericht hat gar keine Serie, an der
# 'time() - ...' haengen koennte. Es bleibt also still. Die beiden Regeln
# darunter fangen die zwei Faelle ab, die dahinterstecken.
- alert: TrivyReportUnreadable
expr: trivy_report_read_errors > 0
for: 30m
labels:
severity: warning
room: security
annotations:
summary: "{{ $value }} CVE-Bericht(e) sind seit 30 min unlesbar — die betroffenen Targets werden NICHT ausgewertet und faellt sonst niemandem auf"
- alert: TrivyNoReports
expr: trivy_reports_total == 0
for: 1h
labels:
severity: critical
room: security
annotations:
summary: "Der CVE-Exporter findet ueberhaupt keine Berichte mehr — der Scanner liefert nichts, die Schwachstellen-Sicht ist blind"
# Sicherungen (management #0030). Bis 2026-08-14 gab es hierzu KEINE Regel: ein
# fehlgeschlagenes naechtliches Backup waere unbemerkt geblieben - genau der stille
# Ausfall, den das Issue beschreibt. Metriken kommen aus kube-state-metrics im
# Matrix-Cluster (Alloy -> remote_write, kube_job_*/kube_cronjob_* passieren den
# Filter, der nur go_.*|process_.* verwirft).
- name: axion-backup
rules:
# Ein Backup- oder Probe-Job ist fehlgeschlagen.
- alert: BackupJobFailed
expr: max by (namespace, job_name) (kube_job_status_failed{job_name=~"(synapse|authentik|wikijs)-backup.*|restore-drill.*"}) > 0
for: 15m
labels:
severity: critical
annotations:
summary: "Backup-Job {{ $labels.namespace }}/{{ $labels.job_name }} ist fehlgeschlagen — ohne Sicherung ist der naechste Ausfall ein Datenverlust"
# Der CronJob plant gar nicht mehr (ausgesetzt, geloescht, Cluster-Problem).
# 26h Toleranz fuer die taeglichen Laeufe um 03:00/03:15/03:30.
# Fallback auf die Anlagezeit: ein frisch (neu) angelegter CronJob hat noch
# keine last_schedule_time-Serie - ein Ausdruck ueber eine fehlende Serie
# liefert nichts, der Alarm koennte also nie feuern. max by(...) fasst beide
# Metriken zu EINER Serie je CronJob zusammen; ohne das wuerde die nie
# aktualisierte created-Serie nach Ablauf der Frist dauerhaft falsch feuern
# ('or' matcht inkl. __name__, ergibt also eine Vereinigung beider Serien).
- alert: BackupNotRunning
expr: time() - max by (namespace, cronjob) (kube_cronjob_status_last_schedule_time{cronjob=~"(synapse|authentik|wikijs)-backup"} or kube_cronjob_created{cronjob=~"(synapse|authentik|wikijs)-backup"}) > 93600
labels:
severity: critical
annotations:
summary: "Seit ueber 26h kein Lauf von {{ $labels.cronjob }} — CronJob ausgesetzt oder verschwunden?"
# Die monatliche Restore-Probe laeuft nicht mehr. Eine Sicherung, die nie
# zurueckgespielt wurde, ist eine Vermutung — deshalb ist auch das Ausbleiben
# der PRUEFUNG ein Alarm, nicht nur ihr Fehlschlag. 40 Tage = Monatsrhythmus + Puffer.
# Bis zum ersten REGULAEREN Lauf gibt es keine last_schedule_time-Serie (ein
# manuell ausgeloester Job setzt sie nicht) - ohne Fallback waere dieser
# Alarm bis dahin blind. Gleiche max-by-Konstruktion wie oben.
- alert: RestoreDrillStale
expr: time() - max by (namespace, cronjob) (kube_cronjob_status_last_schedule_time{cronjob="restore-drill"} or kube_cronjob_created{cronjob="restore-drill"}) > 3456000
labels:
severity: warning
annotations:
summary: "Restore-Probe seit ueber 40 Tagen nicht gelaufen — Wiederherstellbarkeit ist wieder unbewiesen (Verfahren: notfallhandbuch)"
# Ein Backup-CronJob existiert gar nicht mehr (geloescht, Kustomization
# entfernt, Namespace weg). Bei den Alarmen oben verschwindet dann die
# Serie - und eine verschwundene Serie ist in Prometheus kein Alarm,
# sondern Stille. Genau das war der Fehler, den dieses Regelwerk
# verhindern soll, also ist das Fehlen selbst der Alarm.
# for: 30m puffert kurze Luecken der Metrik-Pipeline ab.
- alert: BackupCronJobMissing
expr: >-
absent(kube_cronjob_info{namespace="matrix", cronjob="synapse-backup"})
or absent(kube_cronjob_info{namespace="matrix", cronjob="wikijs-backup"})
or absent(kube_cronjob_info{namespace="authentik", cronjob="authentik-backup"})
or absent(kube_cronjob_info{namespace="matrix", cronjob="restore-drill"})
for: 30m
labels:
severity: critical
annotations:
summary: "Ein Backup-/Probe-CronJob fehlt im Cluster — Sicherung oder Wiederherstellungs-Nachweis laeuft nicht mehr"
# Die Alarmkette meldet ihr eigenes Reissen. Ironie inklusive: schlaegt die
# Zustellung komplett fehl, kommt auch dieser Alarm nicht an - er ist dann
# aber in Prometheus/Grafana sichtbar, statt dass gar nichts existiert.
# Teilausfaelle (ein Receiver von mehreren, Rate-Limit 429) meldet er sauber.
- alert: AlertDeliveryFailing
expr: increase(alertmanager_notifications_failed_total[15m]) > 0
labels:
severity: critical
annotations:
summary: "Alertmanager konnte Benachrichtigungen nicht zustellen ({{ $labels.integration }}) — Alarme laufen ins Leere"
+21
View File
@@ -1,6 +1,14 @@
global:
scrape_interval: 15s
rule_files:
- /etc/prometheus/alerts.yml
alerting:
alertmanagers:
- static_configs:
- targets: ["alertmanager:9093"]
# Zusaetzlich zu diesen Scrape-Targets kommen Metriken per Remote-Write rein
# (k3s-Cluster: flux, kube_state_metrics; Matrix-Server: synapse).
scrape_configs:
@@ -18,6 +26,14 @@ scrape_configs:
static_configs:
- targets: ["cadvisor:8080"]
# Alertmanager-Eigenmetriken. Ohne diesen Job sind ZUSTELLfehler unsichtbar:
# Prometheus kennt alertmanager zwar als Alarm-Ziel (oben unter alerting:),
# scrapt ihn aber nicht - eine still reissende Alarmkette waere also genau das,
# was niemand meldet. Liefert u.a. alertmanager_notifications_failed_total.
- job_name: "operating_alertmanager"
static_configs:
- targets: ["alertmanager:9093"]
- job_name: "operating_traefik"
metrics_path: "/metrics"
static_configs:
@@ -39,3 +55,8 @@ scrape_configs:
- job_name: "gameserver_cadvisor"
static_configs:
- targets: ["157.90.155.206:8080"]
# CVE-Exporter (gitops#47)
- job_name: "cve_exporter"
static_configs:
- targets: ["cve-exporter:9101"]