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.