Trivy Operator in Kubernetes: continuous vulnerability scanning
Published: 2026-02-26
Running trivy image in CI catches vulnerabilities at build time. But what about images already running in production that were clean last month? Trivy Operator continuously scans every workload in the cluster and exposes results as Kubernetes resources — with Prometheus metrics so you can alert on them.
Install via Helm / FluxCD
yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: trivy-operator
spec:
interval: 1h
chart:
spec:
chart: trivy-operator
version: "0.x"
sourceRef:
kind: HelmRepository
name: aquasecurity
namespace: flux-system
values:
operator:
vulnerabilityScannerEnabled: true
configAuditScannerEnabled: true
rbacAssessmentScannerEnabled: true
infraAssessmentScannerEnabled: true
metricsVulnIdEnabled: true
scanJobsConcurrentLimit: 2
trivy:
ignoreUnfixed: true
mode: Standalone
severity: UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL
serviceMonitor:
enabled: true
labels:
release: kube-prom-stack
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 2048Mi
ignoreUnfixed: true is important in practice: unfixed vulnerabilities have no actionable remediation. Without it, dashboards fill up with thousands of CVEs that you can't do anything about.
scanJobsConcurrentLimit: 2 throttles scan parallelism. Without a limit, the operator spawns a job per workload simultaneously, which can saturate nodes during initial install.
mode: Standalone means Trivy downloads its vulnerability database inside the scan job pod, rather than using a central Trivy server. For air-gapped environments, use ClientServer mode and pre-populate a Trivy server cache.
What gets scanned
The operator creates scan jobs for:
- VulnerabilityReport — CVEs in container images (per container per pod)
- ConfigAuditReport — Kubernetes security misconfigs (missing
securityContext, privileged containers, etc.) - RbacAssessmentReport — over-permissive RBAC roles
- InfraAssessmentReport — control plane hardening checks
Check them with:
bashkubectl get vulnerabilityreport -A
kubectl get configauditreport -A
kubectl get rbacassessmentreport -A
kubectl get infraassessmentreport -A
Scan results are stored as Kubernetes custom resources in the same namespace as the workload. They survive pod restarts and are updated when the operator re-scans (by default, every 24 hours).
Reading a VulnerabilityReport
bash# Summary
kubectl get vulnerabilityreport -n app my-app-deployment-my-container \
-o jsonpath='{.report.summary}'
Output:
json{"criticalCount":0,"highCount":3,"lowCount":47,"mediumCount":12,"noneCount":0,"unknownCount":2}
For details on HIGH CVEs:
bashkubectl get vulnerabilityreport -n app my-app-deployment-my-container \
-o json | jq '.report.vulnerabilities[] | select(.severity=="HIGH") | {id: .vulnerabilityID, pkg: .resource, fixed: .fixedVersion}'
Prometheus metrics and alerting
With metricsVulnIdEnabled: true, the operator exposes per-CVE metrics:
promql# Count of CRITICAL CVEs across all workloads
sum(trivy_image_vulnerabilities{severity="CRITICAL"}) by (namespace, resource_name)
Alert on new HIGH/CRITICAL CVEs:
yaml- alert: TrivyCriticalVulnerabilities
expr: sum(trivy_image_vulnerabilities{severity="CRITICAL"}) by (namespace, resource_name) > 0
for: 10m
labels:
severity: high
annotations:
summary: "{{ $labels.resource_name }} in {{ $labels.namespace }} has {{ $value }} CRITICAL CVEs"
description: "Update the base image or add .trivyignore for accepted risks"
- alert: TrivyConfigAuditFailed
expr: trivy_resource_configaudits{severity="CRITICAL"} > 0
for: 5m
labels:
severity: warning
annotations:
summary: "Critical config audit finding in {{ $labels.resource_name }}"
Grafana dashboard: import dashboard ID 17813 (Trivy Operator - Vulnerabilities Dashboard).
Scanning in CI vs Trivy Operator
CI scanning catches issues before they land in production. The operator catches issues that appear after deployment:
- Base image OS gets a new CVE (no rebuild happened)
- A dependency in a running image was just added to NVD
- A new workload was deployed without going through CI (hotfix, manual kubectl apply)
They complement each other. Run both.
Trivy in GitLab CI
yamltrivy-scan:
image: aquasec/trivy:latest
script:
- trivy image
--format json
--output trivy-report.json
--severity HIGH,CRITICAL
--exit-code 1
registry.example.com/myapp:$CI_COMMIT_SHORT_SHA
artifacts:
reports:
container_scanning: trivy-report.json
when: always
--exit-code 1 fails the pipeline on HIGH or CRITICAL findings. --format json lets GitLab parse the report and show vulnerabilities in the Security tab of the MR.
To suppress a known false-positive, create a .trivyignore file:
# Known false-positive, no fix available as of 2026-02-26
CVE-2023-12345
Commit it alongside the Dockerfile — the suppression is visible and auditable.
Namespace exclusions
Skip scanning system namespaces:
yamlvalues:
operator:
excludeNamespaces: "kube-system,flux-system,cert-manager"
This reduces noise and scan job overhead on infrastructure components you don't control.
Trivy Operator + OPA Gatekeeper
Combine Trivy Operator with OPA/Gatekeeper to enforce: "no pods with CRITICAL CVEs allowed in production."
yaml# OPA ConstraintTemplate checks VulnerabilityReport before admission
# (requires creating a custom constraint)
The Trivy Operator report can feed into admission webhook policies to block deployments of vulnerable images entirely.