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>
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>
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>
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>
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>
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>
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
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
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>
- 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
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
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>
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
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>
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>
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>
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>
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>
--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>
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>
- Grafana-Provisioning ergaenzt: Datasources (prometheus/loki) mit den
bestehenden UIDs, damit alle Dashboards weiter funktionieren, sowie vier
Dashboard-Provider passend zu den vorhandenen Ordnern.
- 9 Dashboards aus der laufenden Instanz exportiert und bereinigt: id/version
entfernt, hart kodierte und verwaiste Datasource-UIDs auf die
provisionierten UIDs umgeschrieben.
- Images von :latest auf feste Versionen gepinnt (Prometheus v3.3.1,
Loki 3.7.1, Alloy v1.16.0, Grafana 12.0.0, node-exporter v1.9.1).
- Alloy: Docker-Metadaten (container, compose_service, compose_project) als
Loki-Labels via discovery.relabel.
- .env-Variablen auf GRAFANA_ADMIN_USER/GRAFANA_ADMIN_PASSWORD vereinheitlicht,
.env.example ergaenzt, .env bleibt ungetrackt.
- prometheus.yml kommentiert (Remote-Write-Quellen, Fremd-Stack-Targets),
toten auskommentierten Matrix-Job entfernt.
- README mit Deployment, Struktur, externen Abhaengigkeiten und offenen
Sicherheitspunkten (offene Ports 9090/3100/9100).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Import des bisherigen Stands aus /opt/monitoring auf dem Operating-Host,
unveraendert. Images auf :latest, Grafana ohne Provisioning (Datasources und
Dashboards nur in der grafana.db), keine Dokumentation.
Dient als Ausgangspunkt fuer den Umbau auf einen vollstaendig
provisionierbaren Stack.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>