Skip to content

Commit 72161d3

Browse files
committed
feat(kubeaid-agent): alert on vulnerable images that have an upgrade available
Restores the outdated-and-vulnerable signal that was removed from the trivy-operator chart, computed correctly this time. The alert wants the intersection of two facts: an image carries a fixable Critical/High finding, and a newer tag exists upstream. That overlap is the actionable set. A CVE with no newer image is a wait, and a newer image with no CVE is routine maintenance. The old rule tried to join those in PromQL and could not. Trivy splits and normalises registry and repository while version-checker copies the pod spec verbatim, so joining means reimplementing Docker reference grammar in chained label_replace calls. It failed silently on short-form Docker Hub references, and the fix for that wrongly rewrote registry-local ones. The agent already parses both sides with go-containerregistry, so the count is computed there and exported as a metric. One alert per cluster rather than one per image. The count is what decides whether anyone acts, and a per-image alert across a fleet produces a list nobody reads. Also alerts on collection failure, using == 0 so it stays quiet on clusters that simply do not run a scanner or Tetragon, which report -1. Collapsing those two states would make the alert fire everywhere and get it silenced, taking the real signal with it.
1 parent 3080d43 commit 72161d3

3 files changed

Lines changed: 115 additions & 0 deletions

File tree

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
{{- if and .Values.appConfig.securityPosture.enabled .Values.prometheusRule.enabled }}
2+
apiVersion: monitoring.coreos.com/v1
3+
kind: PrometheusRule
4+
metadata:
5+
name: {{ include "kubeaid-agent.fullname" . }}-security-posture
6+
labels:
7+
{{- include "kubeaid-agent.labels" . | nindent 4 }}
8+
{{- with .Values.prometheusRule.additionalLabels }}
9+
{{- toYaml . | nindent 4 }}
10+
{{- end }}
11+
{{- with .Values.prometheusRule.additionalAnnotations }}
12+
annotations:
13+
{{- toYaml . | nindent 4 }}
14+
{{- end }}
15+
spec:
16+
groups:
17+
- name: kubeaid-agent.security-posture
18+
rules:
19+
# The actionable set: an image carrying a fixable Critical/High finding
20+
# that ALSO has a newer tag upstream. A CVE with no newer image is a
21+
# wait, and a newer image with no CVE is routine maintenance. Only the
22+
# overlap is work that can be done today and removes a known exposure.
23+
#
24+
# One alert per cluster, not one per image. The count is what decides
25+
# whether anyone acts, and a per-image alert on a fleet produces a list
26+
# nobody reads. The images themselves are in the VulnerabilityReport
27+
# CRs.
28+
#
29+
# The correlation is computed by the agent, not by a PromQL join.
30+
# Trivy splits and normalises registry and repository while
31+
# version-checker copies the pod spec verbatim, so joining them in
32+
# PromQL means reimplementing Docker reference grammar in
33+
# label_replace, which fails silently on short-form and registry-local
34+
# references.
35+
- alert: ImageOutdatedAndVulnerable
36+
expr: |
37+
kubeaid_agent_security_posture_upgradable_vulnerable_images
38+
> {{ .Values.prometheusRule.upgradableThreshold }}
39+
for: {{ .Values.prometheusRule.upgradableFor }}
40+
labels:
41+
severity: warning
42+
annotations:
43+
summary: "{{ "{{ $value }}" }} images have fixable Critical/High CVEs with an upgrade available"
44+
description: "These images can be moved to a newer tag today, and doing so clears a known fixable exposure. List them with: kubectl get vulnerabilityreports -A"
45+
46+
# Collection status, reported per component as 1 ok, 0 failed and -1
47+
# not installed.
48+
#
49+
# The comparison is == 0 and must stay that way. Not-installed is -1
50+
# precisely so this alert stays quiet on a cluster that never ran
51+
# Tetragon or a scanner, while still firing when something that IS
52+
# installed cannot be read -- a missing RBAC rule, an unreachable
53+
# scanner. Collapsing the two states would make this fire everywhere
54+
# and it would be silenced, taking the real signal with it.
55+
- alert: SecurityPostureCollectionFailing
56+
expr: |
57+
kubeaid_agent_security_posture_collection == 0
58+
for: {{ .Values.prometheusRule.collectionFailingFor }}
59+
labels:
60+
severity: warning
61+
annotations:
62+
summary: "kubeaid-agent cannot collect {{ "{{ $labels.component }}" }} security posture"
63+
description: "The component is installed but unreadable, so its posture is unknown rather than clean. Check the agent's RBAC and the component's availability."
64+
{{- end }}

argocd-helm-charts/kubeaid-agent/values.schema.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,33 @@
313313
"type": "string"
314314
}
315315
}
316+
},
317+
"prometheusRule": {
318+
"description": "Alerts on the agent's security-posture metrics. Only rendered when appConfig.securityPosture.enabled is true.",
319+
"type": "object",
320+
"properties": {
321+
"enabled": {
322+
"type": "boolean"
323+
},
324+
"additionalLabels": {
325+
"type": "object"
326+
},
327+
"additionalAnnotations": {
328+
"type": "object"
329+
},
330+
"upgradableThreshold": {
331+
"description": "Alert when more than this many images have both a fixable Critical/High finding and a newer tag upstream. One alert per cluster.",
332+
"type": "integer"
333+
},
334+
"upgradableFor": {
335+
"description": "How long the count must stay above the threshold before alerting.",
336+
"type": "string"
337+
},
338+
"collectionFailingFor": {
339+
"description": "How long a component must report collection failure before alerting.",
340+
"type": "string"
341+
}
342+
}
316343
}
317344
}
318345
}

argocd-helm-charts/kubeaid-agent/values.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,27 @@ serviceMonitor:
162162
interval: 30s
163163
scrapeTimeout: 10s
164164

165+
# Alerts on the agent's own security-posture metrics. Only rendered when
166+
# appConfig.securityPosture.enabled is true, since without collection the
167+
# metrics never appear and every rule would evaluate against nothing.
168+
prometheusRule:
169+
enabled: true
170+
171+
# -- Extra labels on the PrometheusRule object, for Prometheus selector
172+
# matching.
173+
additionalLabels: {}
174+
additionalAnnotations: {}
175+
176+
# -- Alert when more than this many images have BOTH a fixable Critical/High
177+
# finding and a newer tag available upstream. Zero means alert on any such
178+
# image. One alert per cluster, not one per image.
179+
upgradableThreshold: 0
180+
181+
# -- How long the count must stay above the threshold. Long by design: this
182+
# is a backlog to work through, not an incident.
183+
upgradableFor: 24h
184+
185+
# -- How long a component must report collection failure before alerting.
186+
# Absorbs a transient API error without hiding a persistent one.
187+
collectionFailingFor: 6h
188+

0 commit comments

Comments
 (0)