Skip to content

Commit 114a493

Browse files
committed
refactor(kubeaid-agent): fold the security exporter into this chart
Collapses argocd-helm-charts/kubeaid-security-exporter into the agent chart, so a cluster gets both workloads from one Argo CD application instead of two, and there is one chart to maintain rather than two. They remain TWO Deployments with TWO ServiceAccounts. That is the point of the arrangement, not an accident of it: a pod carries a single ServiceAccount, so running the exporter as a sidecar would hand the workload holding the Obmondo mTLS credential the exporter's cluster-wide read across seven API groups -- undoing, one layer down, the separation that splitting them achieved. It also bounds failure: a collection pass holds every VulnerabilityReport in memory at once, and a sidecar OOM would take the agent down with it, silencing the cluster-liveness ping that tells the portal a cluster is alive. The exporter's Service name is pinned to kubeaid-security-exporter rather than derived from the release name. The agent's default appConfig.securityPosture.exporterURL is that literal string, and a release-prefixed Service would leave the agent polling a host that does not resolve -- which fails quietly, because a failed poll only sets a metric and submits nothing. The helper says so, so nobody "fixes" it later. securityExporter.enabled: false runs the agent alone. The agent already tolerates a missing exporter, so that switch and appConfig.securityPosture.enabled stay independent. backup-exporter is deliberately left as its own chart. Its values are some two hundred lines of S3 credentials, RPO policy and four alert blocks for Postgres, Velero, MongoDB and sealed-secrets, none of which the agent has anything to do with; folding it in would make the agent chart mostly backup configuration, and would drag five docs pages and a promtool CI job along with it.
1 parent 7ccbe79 commit 114a493

20 files changed

Lines changed: 517 additions & 600 deletions

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
apiVersion: v2
22
name: kubeaid-agent
3-
description: KubeAid Agent Helm chart
3+
description: KubeAid Agent and its security-posture exporter — one Argo CD application, two Deployments.
44
icon: https://avatars.githubusercontent.com/u/13882947?s=250&v=4
55

66
type: application
77

8-
version: 0.2.0
8+
version: 0.3.0
99
appVersion: 0.0.3
1010

1111
keywords:

argocd-helm-charts/kubeaid-agent/README.md

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,23 @@ The KubeAid Agent (`ghcr.io/obmondo/kubeaid-agent`) is Obmondo's in-cluster agen
44
Obmondo API over mTLS and reports cluster state — node counts, Kubernetes version, security posture — and can run
55
Argo CD syncs during agreed service windows. This chart is authored in KubeAid (not a vendored upstream wrapper).
66

7+
## What this chart deploys
8+
9+
Two Deployments from one Argo CD application:
10+
11+
| Workload | Purpose |
12+
|---|---|
13+
| `kubeaid-agent` | Talks to the Obmondo API over mTLS. Holds the credential; holds no CRD access. |
14+
| `kubeaid-security-exporter` | Collects the cluster's security posture and serves it at `/api/v1/security-posture`. Holds cluster-wide read; talks to nothing outside the cluster. |
15+
16+
They are **two Deployments with two ServiceAccounts, not one pod with a sidecar**. A pod carries a single
17+
ServiceAccount, so co-locating them would hand the workload holding the Obmondo credential the exporter's
18+
cluster-wide read across seven API groups — the coupling that separating them removed in the first place.
19+
Keeping them apart also bounds the blast radius: a collection pass holds every VulnerabilityReport in memory
20+
at once, and as a sidecar an OOM there would take down the agent, and with it the cluster-liveness ping.
21+
22+
Set `securityExporter.enabled: false` to run the agent alone.
23+
724
## Why it's in KubeAid
825

926
It is the link between a KubeAid cluster and the Obmondo platform. On clusters with `obmondo.monitoring` enabled,
@@ -18,9 +35,16 @@ on the `kubeaid` Argo CD project, stored in the `argocd-project-role-kubeaid-age
1835
- `argocd-project-role-kubeaid-agent` Secret in the `argocd` namespace, holding the Argo CD auth token under the
1936
`token` key (created automatically by `kubeaid-cli`). Name overridable via
2037
`appConfig.argocd.authTokenSecretName`.
21-
- `kubeaid-security-exporter` in the cluster, if `appConfig.securityPosture.enabled` stays `true`. The agent
22-
forwards that exporter's snapshots; it collects no posture data itself.
23-
- kube-prometheus, if `serviceMonitor` stays enabled (defaults to `true`).
38+
- kube-prometheus, if `serviceMonitor` / `securityExporter.prometheusRule` stay enabled (all default to `true`).
39+
40+
The exporter additionally wants, but does not require:
41+
42+
- **A vulnerability scanner** — either **trivy-operator** or **kubescape-operator**. Without one it reports no
43+
findings, and says so rather than reporting a clean cluster.
44+
- **version-checker**, for upgrade availability. Without it findings still ship, with upgrade availability
45+
unknown rather than "up to date".
46+
47+
Cilium, Tetragon and KubeArmor are read when present and skipped when not.
2448

2549
## Key values / KubeAid-specific configuration
2650

@@ -32,22 +56,71 @@ on the `kubeaid` Argo CD project, stored in the `argocd-project-role-kubeaid-age
3256
| `appConfig.obmondoAPI.url` | `https://api.obmondo.com/api` | Obmondo API endpoint (mTLS). |
3357
| `appConfig.kubeaidUpdate.enabled` | `false` | Opt-in: schedule the service-window Argo CD sync cron job. |
3458
| `appConfig.kubeaidUpdate.checkInterval` | `15m` | Poll cadence for an active KubeAid update service window. |
35-
| `appConfig.securityPosture.enabled` | `true` | Poll `kubeaid-security-exporter` and forward its snapshots to the Obmondo API. The agent collects nothing itself. |
36-
| `appConfig.securityPosture.exporterURL` | `http://kubeaid-security-exporter` | In-cluster URL of the exporter. A bare Service name resolves in the agent's own namespace; qualify it if the two charts deploy to different namespaces. |
37-
| `appConfig.securityPosture.pollInterval` | `1h` | Poll cadence. The submit is skipped when `collectedAt` has not advanced, so end-to-end freshness is bounded by the exporter's collection interval, not by this. |
59+
| `appConfig.securityPosture.enabled` | `true` | Poll the exporter and forward its snapshots to the Obmondo API. The agent collects nothing itself. |
60+
| `appConfig.securityPosture.exporterURL` | `http://kubeaid-security-exporter` | In-cluster URL of the exporter. Matches the Service this chart creates — change both or neither. |
61+
| `appConfig.securityPosture.pollInterval` | `1h` | Poll cadence. The submit is skipped when `collectedAt` has not advanced, so end-to-end freshness is bounded by `securityExporter.exporter.interval`, not by this. |
3862
| `obmondoAPITLSSecretName` | `obmondo-clientcert` | Secret with the mTLS keypair. |
3963
| `extraSecretReaderNamespaces` | `[]` | Extra namespaces where a secrets-read Role/RoleBinding is created for the agent. |
64+
| `securityExporter.enabled` | `true` | Deploy the exporter alongside the agent. |
65+
| `securityExporter.exporter.interval` | `12h` | Collection cadence. Trivy refreshes its reports on a 24h TTL, so polling faster re-reads identical data. |
66+
| `securityExporter.prometheusRule.upgradableThreshold` | `20` | `ImageOutdatedAndVulnerable` fires above this many images having both a fixable Critical/High CVE and a newer tag available. |
67+
| `securityExporter.prometheusRule.upgradableFor` | `24h` | How long the count must hold before the alert fires. |
68+
69+
## What the exporter collects
70+
71+
Each source is detected through API discovery and skipped when absent.
72+
73+
- **Vulnerabilities and least-privilege findings** from Trivy Operator's report CRs — full CVE detail
74+
(CVSS score, installed and fixed version, advisory link), rather than the lossy Prometheus projection.
75+
- **Or the same from Kubescape**, read through its aggregated APIService, with per-CVE relevancy — whether the
76+
vulnerable code actually loaded. The two scanners are alternatives, never merged.
77+
- **Upgrade availability**, by joining findings against version-checker on canonical image references resolved
78+
in Go. Rebuilding image references in PromQL fails silently.
79+
- **Network enforcement** from Cilium — whether policy is actually realised on an app's pods, which is a
80+
different question from whether the app ships a policy.
81+
- **Runtime detection posture** from Tetragon and KubeArmor — which engines are armed, and whether they
82+
observe or enforce.
83+
84+
### Which scanner is used
85+
86+
Exactly one, chosen by API discovery: **Trivy takes precedence, Kubescape is used where Trivy is absent.**
87+
Merging them would double-count the same CVE from two databases that disagree at the margins, and switching an
88+
existing cluster's scanner rewrites every finding's ID, score and link at once — which reads as mass CVE churn
89+
rather than as a configuration change. The snapshot names the one it used in `scanner`.
90+
91+
Two fields differ by scanner, and both are absent rather than false when unanswerable:
92+
93+
| Field | Trivy | Kubescape |
94+
|---|---|---|
95+
| `relevant` (did the vulnerable code load) | never — Trivy cannot observe runtime | when the eBPF node-agent runs |
96+
| `os.eosl` (base image past end of life) | yes | never — Grype has no end-of-life data |
97+
98+
## Alerting
99+
100+
One alert, `ImageOutdatedAndVulnerable`. It is a count, so it fires once per cluster rather than once per
101+
image, and the threshold is deliberately high — every real cluster carries a few of these at any moment, so a
102+
low threshold fires everywhere on day one and gets ignored. The signal worth acting on is a pile of easy
103+
upgrades, not the existence of one.
104+
105+
Collection status is exported as `security_exporter_collection` (1 ok, 0 failed, -1 not installed) but is
106+
deliberately not alerted on: a collection failure is a debugging signal, not something worth paging for.
40107

41108
## Operational notes
42109

43-
- RBAC is least-privilege by construction: a purpose-built ClusterRole grants exactly the verbs the agent's code
44-
calls (no `watch`, no blanket `view`). Secrets access stays on namespaced Roles. The agent holds no CRD access
45-
at all — reading Trivy, Cilium, Tetragon and KubeArmor resources belongs to `kubeaid-security-exporter`.
46-
- `securityPosture.enabled: true` is safe where the exporter is not installed — the poll fails, a metric records
47-
it, and nothing is submitted. Set it to `false` only where vulnerability detail must not leave the cluster.
48-
- Runs unprivileged: non-root, all capabilities dropped, `RuntimeDefault` seccomp.
110+
- RBAC is least-privilege by construction, and **separate per workload**: the agent gets a purpose-built
111+
ClusterRole granting exactly the verbs its code calls, with secrets access on namespaced Roles and no CRD
112+
access at all. The exporter gets its own, covering the report CRs it reads. Neither can use the other's.
113+
- No `watch` anywhere — neither workload builds informers.
114+
- The exporter's rules are all consumed through a dynamic client, so no Go source references those resource
115+
names; removing a rule fails silently and renders the cluster as clean rather than erroring.
116+
- `appConfig.securityPosture.enabled: true` is safe where the exporter is disabled — the poll fails, a metric
117+
records it, and nothing is submitted. Set it to `false` only where vulnerability detail must not leave the
118+
cluster.
119+
- Both run unprivileged: non-root, all capabilities dropped, `RuntimeDefault` seccomp; the exporter also runs
120+
with a read-only root filesystem.
49121

50122
## Docs links
51123

52124
- Chart source: `templates/` and [values.yaml](./values.yaml) in this directory (documented inline).
125+
- Exporter source: <https://gitea.obmondo.com/EnableIT/kubeaid-security-exporter>
53126
- Obmondo: <https://obmondo.com>

argocd-helm-charts/kubeaid-agent/templates/_helpers.tpl

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,42 @@ app.kubernetes.io/version: {{ .Chart.AppVersion }}
3131
app.kubernetes.io/name: {{ include "kubeaid-agent.name" . }}
3232
app.kubernetes.io/instance: {{ .Release.Name }}
3333
{{- end }}
34+
35+
{{/*
36+
kubeaid-security-exporter runs as a SECOND Deployment in this chart, with its
37+
own ServiceAccount and ClusterRole. It is deliberately not a sidecar: a pod
38+
carries one ServiceAccount, so co-locating the two would hand the workload
39+
holding the Obmondo mTLS credential the exporter's cluster-wide read across
40+
seven API groups — the exact coupling that splitting them removed.
41+
*/}}
42+
{{- define "kubeaid-agent.securityExporter.name" -}}
43+
kubeaid-security-exporter
44+
{{- end -}}
45+
46+
{{/*
47+
Fixed rather than release-derived. The agent's default
48+
appConfig.securityPosture.exporterURL is the literal string
49+
http://kubeaid-security-exporter, and a release-prefixed Service name would
50+
leave the agent polling a host that does not resolve — which fails QUIETLY,
51+
because a failed poll only sets a metric and submits nothing.
52+
*/}}
53+
{{- define "kubeaid-agent.securityExporter.fullname" -}}
54+
{{- default (include "kubeaid-agent.securityExporter.name" .) .Values.securityExporter.fullnameOverride | trunc 63 | trimSuffix "-" -}}
55+
{{- end -}}
56+
57+
{{- define "kubeaid-agent.securityExporter.labels" -}}
58+
helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
59+
app.kubernetes.io/name: {{ include "kubeaid-agent.securityExporter.name" . }}
60+
app.kubernetes.io/instance: {{ .Release.Name }}
61+
app.kubernetes.io/managed-by: {{ .Release.Service }}
62+
app.kubernetes.io/component: security-exporter
63+
{{- end -}}
64+
65+
{{/*
66+
Distinct from the agent's selector labels on purpose: two Deployments in one
67+
release must not select each other's pods.
68+
*/}}
69+
{{- define "kubeaid-agent.securityExporter.selectorLabels" -}}
70+
app.kubernetes.io/name: {{ include "kubeaid-agent.securityExporter.name" . }}
71+
app.kubernetes.io/instance: {{ .Release.Name }}
72+
{{- end }}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
{{- if .Values.securityExporter.enabled }}
2+
apiVersion: apps/v1
3+
kind: Deployment
4+
metadata:
5+
name: {{ include "kubeaid-agent.securityExporter.fullname" . }}
6+
namespace: {{ .Release.Namespace }}
7+
labels:
8+
{{- include "kubeaid-agent.securityExporter.labels" . | nindent 4 }}
9+
spec:
10+
replicas: {{ .Values.securityExporter.deployment.replicas }}
11+
selector:
12+
matchLabels:
13+
{{- include "kubeaid-agent.securityExporter.selectorLabels" . | nindent 6 }}
14+
template:
15+
metadata:
16+
labels:
17+
{{- include "kubeaid-agent.securityExporter.selectorLabels" . | nindent 8 }}
18+
{{- with .Values.securityExporter.deployment.podLabels }}
19+
{{- toYaml . | nindent 8 }}
20+
{{- end }}
21+
{{- with .Values.securityExporter.deployment.podAnnotations }}
22+
annotations:
23+
{{- toYaml . | nindent 8 }}
24+
{{- end }}
25+
spec:
26+
{{- with .Values.securityExporter.deployment.imagePullSecrets }}
27+
imagePullSecrets:
28+
{{- toYaml . | nindent 8 }}
29+
{{- end }}
30+
serviceAccountName: {{ include "kubeaid-agent.securityExporter.fullname" . }}
31+
{{- with .Values.securityExporter.deployment.podSecurityContext }}
32+
securityContext:
33+
{{- toYaml . | nindent 8 }}
34+
{{- end }}
35+
containers:
36+
- name: {{ include "kubeaid-agent.securityExporter.name" . }}
37+
image: "{{ .Values.securityExporter.deployment.image.repository }}:{{ .Values.securityExporter.deployment.image.tag }}"
38+
imagePullPolicy: {{ .Values.securityExporter.deployment.image.pullPolicy }}
39+
env:
40+
- name: SECURITY_EXPORTER_PORT
41+
value: {{ .Values.securityExporter.exporter.port | quote }}
42+
- name: SECURITY_EXPORTER_INTERVAL
43+
value: {{ .Values.securityExporter.exporter.interval | quote }}
44+
{{- with .Values.securityExporter.deployment.env }}
45+
{{- toYaml . | nindent 12 }}
46+
{{- end }}
47+
ports:
48+
- name: http
49+
containerPort: {{ .Values.securityExporter.exporter.port }}
50+
protocol: TCP
51+
{{- with .Values.securityExporter.deployment.securityContext }}
52+
securityContext:
53+
{{- toYaml . | nindent 12 }}
54+
{{- end }}
55+
volumeMounts:
56+
- name: tmp
57+
mountPath: /tmp
58+
{{- with .Values.securityExporter.deployment.extraVolumeMounts }}
59+
{{- toYaml . | nindent 12 }}
60+
{{- end }}
61+
{{- with .Values.securityExporter.deployment.probes.readiness }}
62+
readinessProbe:
63+
{{- toYaml . | nindent 12 }}
64+
{{- end }}
65+
{{- with .Values.securityExporter.deployment.probes.liveness }}
66+
livenessProbe:
67+
{{- toYaml . | nindent 12 }}
68+
{{- end }}
69+
{{- with .Values.securityExporter.deployment.resources }}
70+
resources:
71+
{{- toYaml . | nindent 12 }}
72+
{{- end }}
73+
volumes:
74+
- name: tmp
75+
emptyDir: {}
76+
{{- with .Values.securityExporter.deployment.extraVolumes }}
77+
{{- toYaml . | nindent 8 }}
78+
{{- end }}
79+
terminationGracePeriodSeconds: {{ .Values.securityExporter.deployment.terminationGracePeriodSeconds }}
80+
{{- with .Values.securityExporter.deployment.nodeSelector }}
81+
nodeSelector:
82+
{{- toYaml . | nindent 8 }}
83+
{{- end }}
84+
{{- with .Values.securityExporter.deployment.affinity }}
85+
affinity:
86+
{{- toYaml . | nindent 8 }}
87+
{{- end }}
88+
{{- with .Values.securityExporter.deployment.tolerations }}
89+
tolerations:
90+
{{- toYaml . | nindent 8 }}
91+
{{- end }}
92+
{{- end }}

argocd-helm-charts/kubeaid-security-exporter/templates/prometheusrule.yaml renamed to argocd-helm-charts/kubeaid-agent/templates/security-exporter-prometheusrule.yaml

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
{{- if .Values.prometheusRule.enabled }}
1+
{{- if .Values.securityExporter.enabled }}
2+
{{- if .Values.securityExporter.prometheusRule.enabled }}
23
apiVersion: monitoring.coreos.com/v1
34
kind: PrometheusRule
45
metadata:
5-
name: {{ include "kubeaid-security-exporter.fullname" . }}
6+
name: {{ include "kubeaid-agent.securityExporter.fullname" . }}
67
labels:
7-
{{- include "kubeaid-security-exporter.labels" . | nindent 4 }}
8-
{{- with .Values.prometheusRule.additionalLabels }}
8+
{{- include "kubeaid-agent.securityExporter.labels" . | nindent 4 }}
9+
{{- with .Values.securityExporter.prometheusRule.additionalLabels }}
910
{{- toYaml . | nindent 4 }}
1011
{{- end }}
11-
{{- with .Values.prometheusRule.additionalAnnotations }}
12+
{{- with .Values.securityExporter.prometheusRule.additionalAnnotations }}
1213
annotations:
1314
{{- toYaml . | nindent 4 }}
1415
{{- end }}
@@ -24,8 +25,8 @@ spec:
2425
- alert: ImageOutdatedAndVulnerable
2526
expr: |
2627
security_exporter_upgradable_vulnerable_images
27-
> {{ .Values.prometheusRule.upgradableThreshold }}
28-
for: {{ .Values.prometheusRule.upgradableFor }}
28+
> {{ .Values.securityExporter.prometheusRule.upgradableThreshold }}
29+
for: {{ .Values.securityExporter.prometheusRule.upgradableFor }}
2930
labels:
3031
severity: warning
3132
annotations:
@@ -36,3 +37,4 @@ spec:
3637
# (1 ok, 0 failed, -1 not installed) but deliberately not alerted on —
3738
# it is a debugging signal, not something worth waking anyone for.
3839
{{- end }}
40+
{{- end }}

argocd-helm-charts/kubeaid-security-exporter/templates/rbac-cluster-read.yaml renamed to argocd-helm-charts/kubeaid-agent/templates/security-exporter-rbac.yaml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{{- if .Values.securityExporter.enabled }}
12
{{/*
23
Cluster-scoped read access. Every rule is consumed through a DYNAMIC client,
34
so no Go code references these resource names as imported types — a reader
@@ -13,9 +14,9 @@
1314
apiVersion: rbac.authorization.k8s.io/v1
1415
kind: ClusterRole
1516
metadata:
16-
name: {{ include "kubeaid-security-exporter.fullname" . }}-read
17+
name: {{ include "kubeaid-agent.securityExporter.fullname" . }}-read
1718
labels:
18-
{{- include "kubeaid-security-exporter.labels" . | nindent 4 }}
19+
{{- include "kubeaid-agent.securityExporter.labels" . | nindent 4 }}
1920
rules:
2021
# Locating version-checker's metrics endpoint by label, which is how image
2122
# tags get compared against upstream.
@@ -88,14 +89,15 @@ rules:
8889
apiVersion: rbac.authorization.k8s.io/v1
8990
kind: ClusterRoleBinding
9091
metadata:
91-
name: {{ include "kubeaid-security-exporter.fullname" . }}-read
92+
name: {{ include "kubeaid-agent.securityExporter.fullname" . }}-read
9293
labels:
93-
{{- include "kubeaid-security-exporter.labels" . | nindent 4 }}
94+
{{- include "kubeaid-agent.securityExporter.labels" . | nindent 4 }}
9495
subjects:
9596
- kind: ServiceAccount
96-
name: {{ include "kubeaid-security-exporter.fullname" . }}
97+
name: {{ include "kubeaid-agent.securityExporter.fullname" . }}
9798
namespace: {{ .Release.Namespace }}
9899
roleRef:
99100
apiGroup: rbac.authorization.k8s.io
100101
kind: ClusterRole
101-
name: {{ include "kubeaid-security-exporter.fullname" . }}-read
102+
name: {{ include "kubeaid-agent.securityExporter.fullname" . }}-read
103+
{{- end }}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{{- if .Values.securityExporter.enabled }}
2+
apiVersion: v1
3+
kind: Service
4+
metadata:
5+
name: {{ include "kubeaid-agent.securityExporter.fullname" . }}
6+
namespace: {{ .Release.Namespace }}
7+
labels:
8+
{{- include "kubeaid-agent.securityExporter.labels" . | nindent 4 }}
9+
{{- with .Values.securityExporter.service.labels }}
10+
{{- toYaml . | nindent 4 }}
11+
{{- end }}
12+
{{- with .Values.securityExporter.service.annotations }}
13+
annotations:
14+
{{- toYaml . | nindent 4 }}
15+
{{- end }}
16+
spec:
17+
type: ClusterIP
18+
selector:
19+
{{- include "kubeaid-agent.securityExporter.selectorLabels" . | nindent 4 }}
20+
ports:
21+
- name: http
22+
port: {{ .Values.securityExporter.service.port }}
23+
targetPort: http
24+
{{- end }}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{{- if .Values.securityExporter.enabled }}
2+
apiVersion: v1
3+
kind: ServiceAccount
4+
metadata:
5+
name: {{ include "kubeaid-agent.securityExporter.fullname" . }}
6+
namespace: {{ .Release.Namespace }}
7+
labels:
8+
{{- include "kubeaid-agent.securityExporter.labels" . | nindent 4 }}
9+
{{- end }}

0 commit comments

Comments
 (0)