Architektur, Design-Entscheidungen und Konfiguration
Dieses Dokument beschreibt, wie und warum wir Vulnerability-Alerts aus Trivy über Prometheus nach MS Teams senden - und warum das Setup bewusst so gebaut ist.
Ziel ist:
- reproduzierbares Alerting
- klare Semantik
- kein Alert-Spam
- nachvollziehbare Entscheidungen
1. Gesamtüberblick (Signalfluss)
Trivy Operator
↓ (Prometheus Metrics)
Prometheus
↓ (Recording Rules + Alert Rules)
Alertmanager
↓ (Webhook)
prometheus-msteams
↓
Microsoft Teams Channel
2. Warum Prometheus für Trivy-Alerts?
Der Trivy Operator erzeugt keine Events, sondern:
- Kubernetes Custom Resources (
VulnerabilityReport) - Prometheus-Metriken (
trivy_image_vulnerabilities{...})
Prometheus ist damit:
- die einzige Quelle, die Zeitvergleiche erlaubt
- das Tool, das “neu vs alt” korrekt bewerten kann
- notwendig für Logik wie:
- “Count ist gestiegen”
- “Image gab es vorher nicht”
Ein reines Event-basiertes Alerting ist hier nicht möglich.
3. Warum eine Recording Rule?
Problem ohne Recording Rule
Die Rohmetrik trivy_image_vulnerabilities:
- existiert pro Vulnerability
- enthält sehr viele Labels
- ist für Zeitvergleiche unhandlich
Beispiel:
trivy_image_vulnerabilities{severity="Critical"}→ liefert eine Zeitreihe pro CVE, nicht pro Image.
Lösung: Aggregation als Recording Rule
record: trivy:critical_vuln_count_by_image
expr: |
sum by (
namespace,
image_registry,
image_repository,
image_tag,
image_digest,
resource_kind
) (
trivy_image_vulnerabilities{severity="Critical"}
)Warum das sinnvoll ist
- erzeugt eine saubere Zeitreihe pro Image
- reduziert Komplexität in Alert-Queries
- macht
offset,max_over_time,unlessstabil nutzbar - verbessert Performance
- ermöglicht Filterung nach Workload-Typ (
resource_kind), um Spam durch Workflows / Jobs zu verhindern
Wichtig:
Recording Rules sind stateless.
Der “Zustand” liegt ausschließlich in der Prometheus-TSDB.
4. Warum zwei Alerts statt einem?
Es gibt zwei fachlich unterschiedliche Ereignisse, die zufällig dieselbe Metrik verändern:
A) Bestehendes Image bekommt neue Vulnerabilities
- Ursache: CVE-Datenbank-Update
- Image hat sich nicht geändert
- Security-relevant
B) Neues Image wird deployed und ist bereits verwundbar
- Ursache: Deployment / Change
- Vulnerabilities waren schon bekannt
- Plattform-/Ops-relevant
👉 Ein einzelner Alert kann diese Semantik nicht korrekt unterscheiden.
Deshalb: zwei Alerts, klar getrennt.
5. Alert A - CRITICAL-Count im bestehenden Image gestiegen
Fachliche Bedeutung
„Dieses Image war bereits im Cluster und hat jetzt mehr CRITICAL Vulnerabilities als vorher.“
Typischer Auslöser:
- neue CVE
- CVSS-Neubewertung
- Trivy-DB-Update
Warum Einschränkung auf langlebige Workloads?
In unserer Umgebung laufen u.a. Argo CronWorkflows, deren Pods nach erfolgreichem Lauf gelöscht werden. Dadurch entstehen ständig neue VulnerabilityReports und neue kurzlebige Ressourcen - was ohne Filter zu Alert-Spam führt.
Daher wird Alert A bewusst nur auf “stabile Workloads” eingeschränkt:
Deployment/ReplicaSetStatefulSetDaemonSet
PromQL-Logik
(
max_over_time(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"}[10m])
-
max_over_time(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"}[10m] offset 15m)
) > 0Warum das funktioniert
offset 15m= Vergleich mit Vergangenheit- nur Images, die damals schon existierten, werden verglichen
- kein Alert beim erstmaligen Auftauchen eines Images
- kein Alert-Sturm nach Prometheus-Neustart
Alert-Definition
- alert: TrivyCriticalCountIncreasedForExistingImage
expr: |
(
max_over_time(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"}[10m])
-
max_over_time(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"}[10m] offset 15m)
) > 0
labels:
severity: critical
source: trivy
alert_type: image_delta
annotations:
summary: "🚨 CRITICAL Vulnerabilities gestiegen"
description: |
Der CRITICAL-Vulnerability-Count ist für ein bestehendes Image gestiegen.
Namespace: {{ $labels.namespace }}
Image: {{ $labels.image_registry }}/{{ $labels.image_repository }}:{{ $labels.image_tag }}
Digest: {{ $labels.image_digest }}6. Alert B - Neues Image mit CRITICAL Vulnerabilities
Fachliche Bedeutung
„Ein Image ist neu im Cluster und enthält bereits bekannte CRITICAL Vulnerabilities.“
Das ist kein CVE-Update, sondern ein Deployment-Event.
Auch Alert B wird auf stabile Workloads eingeschränkt, damit jede CronWorkflow-Ausführung nicht als “neues Image” gezählt wird.
Warum unless und nicht absent()
❌ absent():
- global
- liefert keine Image-Labels
- ungeeignet für per-Image-Vergleiche
✅ unless:
- echte Mengen-Differenz
- funktioniert pro Zeitreihe
- genau das, was wir brauchen
PromQL-Logik
(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"} > 0)
unless
(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"} offset 15m)Bedeutung:
- jetzt vorhanden
- vor 15 Minuten nicht vorhanden
- → neues Image
Alert-Definition
- alert: TrivyNewImageWithCriticalVulnerabilitiesDetected
expr: |
(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"} > 0)
unless
(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"} offset 15m)
labels:
severity: warning
source: trivy
alert_type: image_new
annotations:
summary: "⚠️ Neues Image mit CRITICAL Vulnerabilities"
description: |
Ein neues Image ist im Cluster aufgetaucht und enthält CRITICAL Vulnerabilities.
Namespace: {{ $labels.namespace }}
Image: {{ $labels.image_registry }}/{{ $labels.image_repository }}:{{ $labels.image_tag }}
Digest: {{ $labels.image_digest }}7. Umgang mit Argo Workflows / kurzlebigen Pods
Problem
Argo CronWorkflows erzeugen kurzlebige Pods, die nach Abschluss gelöscht werden.
Der Trivy Operator erzeugt dazu jeweils neue VulnerabilityReports, wodurch Prometheus-Metriken “neu erscheinen” und Alerts getriggert würden.
Konsequenz
Ohne Filterung führt das dazu, dass bei jedem Workflow-Lauf Alerts entstehen, obwohl sich an der Security-Lage nichts geändert hat.
Lösung
Alerts werden bewusst auf stabile Workloads eingeschränkt, indem resource_kind gefiltert wird:
- erlaubt:
Deployment|StatefulSet|DaemonSet|ReplicaSet - ignoriert:
Pod|Job|ReplicaSet|CronJob(je nach Trivy Operator Version/Labeling)
Damit werden Workflows weiterhin durch Trivy gescannt und im Dashboard sichtbar, aber lösen keine Event-Alerts aus.
Optional: Workflow-spezifische Alerts
Falls wir CronWorkflows dennoch überwachen wollen, sollte dafür ein separater Alert-Typ genutzt werden, z.B.:
- “First seen in 24h” (max. 1 Alert pro Tag & Image)
- oder nur Namespace-gebunden (z.B.
namespace="argo")
Wichtig: Diese Alerts müssen stark gedrosselt werden (repeat_interval >= 24h), sonst ist MS Teams nutzlos.
8. Warum Alertmanager + prometheus-msteams?
Problem
Alertmanager kann:
- Webhooks
- Slack
- Email
aber kein MS Teams nativ.
Lösung: prometheus-msteams
- übersetzt Alertmanager-Webhooks → Teams MessageCards
- leichtgewichtig
- etabliert
- gut anpassbar per Template
Image:
quay.io/prometheusmsteams/prometheus-msteams
9. MS Teams Template - bewusste Entscheidungen
Warum activityTitle
- stabil gerendert
- keine Überraschungen mit Markdown
- akzeptiert einzeiligen Text zuverlässig
"activityTitle": "{{ $alert.Annotations.description }}"Warum kein ExternalURL
- Alertmanager läuft intern
- der Link war für Empfänger nicht nutzbar
- besser: gar kein Link als ein kaputter
10. Alertmanager-Routing (Konzept)
Wir unterscheiden Alerts über Labels:
alert_type: image_delta
alert_type: image_newTypische Nutzung:
image_delta→ Security-Channelimage_new→ Plattform-/Ops-Channelsend_resolved: false(Event-Alerts)
11. Testing & Betrieb
Wichtig für Tests
offsetbraucht Historie- ohne PVC → Prometheus-Restart = Historie weg
- in Minikube: kleine Offsets (5-15m)
Nützliche Debug-Queries
trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"}(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"} > 0)
unless
(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"} offset 15m)max_over_time(trivy:critical_vuln_count_by_image{resource_kind=~"Deployment|StatefulSet|DaemonSet|ReplicaSet"}[10m])12. Zusammenfassung
- Trivy liefert Metriken, keine Events
- Prometheus ist notwendig für Zeitvergleiche
- Recording Rule = saubere Basis pro Image
- Zwei Alerts, nicht einer
unlessfür „neu“,offset-Differenz für „gestiegen“- MS Teams via prometheus-msteams
- Alert-Semantik bewusst getrennt