2026-07-31 18:30:40 +02:00
# 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"
2026-08-01 13:42:07 +02:00
# 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 :
2026-08-01 16:11:25 +02:00
# 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
2026-08-01 13:42:07 +02:00
labels :
severity : critical
room : security
annotations :
2026-08-01 16:11:25 +02:00
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
2026-08-01 13:42:07 +02:00
for : 24h
labels :
severity : warning
room : security
annotations :
2026-08-01 16:11:25 +02:00
summary : "{{ $labels.target }} [{{ $labels.target_type }}@{{ $labels.host }}]: {{ $value }} HIGH-CVEs (seit 24h offen) — Details: https://selendis.axion1337.de/d/cve-overview"
2026-08-01 13:42:07 +02:00
- 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)"
2026-08-14 12:00:00 +00:00
# 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.
2026-08-14 12:00:00 +00:00
# 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).
2026-08-14 12:00:00 +00:00
- alert : BackupNotRunning
2026-08-14 12:00:00 +00:00
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
2026-08-14 12:00:00 +00:00
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.
2026-08-14 12:00:00 +00:00
# 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.
2026-08-14 12:00:00 +00:00
- alert : RestoreDrillStale
2026-08-14 12:00:00 +00:00
expr : time() - max by (namespace, cronjob) (kube_cronjob_status_last_schedule_time{cronjob="restore-drill"} or kube_cronjob_created{cronjob="restore-drill"}) > 3456000
2026-08-14 12:00:00 +00:00
labels :
severity : warning
annotations :
summary : "Restore-Probe seit ueber 40 Tagen nicht gelaufen — Wiederherstellbarkeit ist wieder unbewiesen (Verfahren: notfallhandbuch)"
2026-08-14 12:00:00 +00:00
# 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"
2026-08-14 12:00:00 +00:00
# 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"