Skip to content

Trivy Operator: Deep Dive

This document explains how vulnerability scanning works, what CVEs are, how Trivy scans container images, the VulnerabilityReport and ConfigAuditReport CRD structures, and how to build a security scanning pipeline for Kubernetes.

CVE stands for Common Vulnerabilities and Exposures. It is a standardized naming system for publicly known security vulnerabilities. Each CVE entry has:

  • CVE ID: A unique identifier like CVE-2023-44487
  • Description: What the vulnerability is and how it works
  • Severity: CRITICAL, HIGH, MEDIUM, LOW, or UNKNOWN
  • Affected packages: Which software versions are vulnerable
  • Fixed version: The version that patches the vulnerability (if available)
  • CVSS score: A numeric severity score from 0.0 to 10.0

CVEs are published by MITRE and tracked in databases maintained by organizations like the National Vulnerability Database (NVD) and GitHub Security Advisories.

Vulnerability scanners analyze container images by:

  1. Extracting the image layers
  2. Reading the package manifest (dpkg, apk, rpm, pip, npm, etc.)
  3. Building a Software Bill of Materials (SBOM) listing all installed packages
  4. Comparing each package version against vulnerability databases
  5. Reporting any matches

For example, if an image contains openssl 1.1.1k and the database says CVE-2022-0778 affects versions below 1.1.1n, the scanner flags it.

Container images are built in layers. Each layer represents a file system change:

FROM debian:11
RUN apt-get update && apt-get install -y nginx=1.18.0
COPY app.conf /etc/nginx/

The scanner reads:

  • Base layer: Debian 11 with all default packages
  • Second layer: nginx 1.18.0 and its dependencies
  • Third layer: Custom config file (no packages)

It extracts the full package list (libc6, libssl1.1, nginx, zlib1g, etc.) and checks each one.

Trivy is a comprehensive security scanner that detects:

  • OS package vulnerabilities (Alpine apk, Debian dpkg, RedHat rpm)
  • Language dependencies (Python pip, Node npm, Ruby gem, Go modules)
  • Misconfigurations (Kubernetes, Docker, Terraform)
  • Secrets (API keys, passwords, tokens in files or environment variables)
  • License compliance (GPL, MIT, Apache)

Trivy maintains a vulnerability database synced from multiple sources:

SourceCoverage
National Vulnerability Database (NVD)All CVEs
Red Hat Security DataRHEL, CentOS, Fedora
Debian Security TrackerDebian, Ubuntu
Alpine SecDBAlpine Linux
GitHub Advisory Databasenpm, pip, RubyGems, Go, Rust
OSV (Open Source Vulnerabilities)Aggregated source

The database is updated daily. The Trivy Operator downloads it on startup and refreshes periodically.

The Trivy Operator runs as a Kubernetes controller with these components:

The main controller pod watches for:

  • New pods created in the cluster
  • Workload changes (Deployments, StatefulSets, DaemonSets, Jobs)
  • ConfigMaps and Secrets for configuration scanning
  • RBAC resources for role checks

When a pod is created, the operator reads the image references and triggers scans.

For each scan, the operator spawns a Job that:

  • Pulls the container image from the registry
  • Runs the Trivy CLI to scan the image
  • Writes results to a VulnerabilityReport CRD
  • Deletes itself on completion

This isolates scanning from the control plane. The scanner pods run with minimal privileges.

The operator creates several CRD types:

CRDPurpose
VulnerabilityReportCVE findings for a container image
ConfigAuditReportKubernetes misconfiguration findings
ExposedSecretReportHardcoded secrets in images or configs
RbacAssessmentReportRBAC role permission checks
InfraAssessmentReportCluster-level security checks
ClusterComplianceReportCIS Benchmark compliance

Each report is labeled with the workload name, namespace, and resource kind for filtering.

Here is an example VulnerabilityReport for nginx:1.21:

apiVersion: aquasecurity.github.io/v1alpha1
kind: VulnerabilityReport
metadata:
name: replicaset-old-nginx-abc123-nginx
namespace: trivy-demo
labels:
trivy-operator.resource.kind: ReplicaSet
trivy-operator.resource.name: old-nginx-abc123
trivy-operator.container.name: nginx
spec:
image:
repository: nginx
tag: "1.21"
registry:
server: index.docker.io
report:
summary:
criticalCount: 12
highCount: 34
mediumCount: 56
lowCount: 22
unknownCount: 3
vulnerabilities:
- vulnerabilityID: CVE-2023-44487
resource: libnghttp2-14
installedVersion: 1.43.0-1
fixedVersion: 1.43.0-1+deb11u1
severity: CRITICAL
title: "HTTP/2 Rapid Reset Attack"
description: "The HTTP/2 protocol allows a denial of service..."
links:
- https://nvd.nist.gov/vuln/detail/CVE-2023-44487
score: 7.5
- vulnerabilityID: CVE-2023-4911
resource: libc6
installedVersion: 2.31-13+deb11u5
fixedVersion: 2.31-13+deb11u6
severity: HIGH
title: "glibc buffer overflow in ld.so"
primaryLink: https://nvd.nist.gov/vuln/detail/CVE-2023-4911
score: 7.8

The report.vulnerabilities[] array contains all findings.

This report checks Kubernetes resources against best practices:

apiVersion: aquasecurity.github.io/v1alpha1
kind: ConfigAuditReport
metadata:
name: replicaset-old-nginx-abc123
namespace: trivy-demo
spec:
summary:
criticalCount: 0
highCount: 2
mediumCount: 5
lowCount: 8
checks:
- checkID: KSV001
title: "Process can elevate its own privileges"
severity: MEDIUM
category: "Pod Security Standards"
description: "allowPrivilegeEscalation should be set to false"
success: false
messages:
- "Container 'nginx' does not set allowPrivilegeEscalation to false"
- checkID: KSV012
title: "Runs as root user"
severity: HIGH
category: "Pod Security Standards"
description: "Containers should run as non-root user"
success: false
messages:
- "Container 'nginx' is running as root (UID 0)"
- checkID: KSV020
title: "No resource limits defined"
severity: LOW
category: "Resource Management"
success: false

Each check references a control from the CIS Kubernetes Benchmark or Pod Security Standards.

ScannerStrengthWeakness
TrivyFast, multi-language, free, easy to runDatabase updates lag slightly behind NVD
GrypeVery fast, minimal dependenciesFewer vulnerability sources than Trivy
SnykGreat language support, dev tools integrationCommercial, expensive for large teams
ClairDesigned for registry integrationHarder to deploy, slower scans
AnchorePolicy enforcement, SBOM generationComplex setup, resource-heavy

All scanners work similarly (extract packages, compare to database). The differences are:

  • Database sources: More sources mean better coverage but slower updates
  • Speed: Grype is fastest, Clair is slowest
  • Language support: Snyk and Trivy cover the most languages
  • Kubernetes integration: Trivy Operator has the best native K8s support

You can scan images before deploying them to catch vulnerabilities early. Two strategies:

Run Trivy in your build pipeline:

# GitHub Actions example
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # Fail the build if vulnerabilities found

This prevents deploying vulnerable images.

The operator scans what is actually running in the cluster. This catches:

  • Images deployed before scanning was enabled
  • New CVEs discovered after deployment
  • Images pulled from untrusted registries

Best practice: Use both. Scan in CI to prevent deployment, and scan at runtime to catch drift.

Sometimes you need to ignore false positives or accept risk. Trivy supports a .trivyignore file:

# Ignore this CVE because we use a patched kernel
CVE-2023-1234
# Ignore all LOW severity findings
SEVERITY:LOW
# Ignore specific package
pkg:apk/alpine/busybox@1.35.0

For the Trivy Operator, configure this in the Helm values:

trivy:
ignoreUnfixed: true # Ignore CVEs with no fix available
severity: CRITICAL,HIGH # Only report critical and high

The demo uses --set trivy.ignoreUnfixed=true because some vulnerabilities have no patch yet. Reporting them creates noise without actionable fixes.

Example: A LOW severity CVE in a kernel package might have no fix for 6 months. If you scan a base image, you’ll see dozens of these. Filtering to only fixed vulnerabilities focuses on what you can act on.

By default, Trivy Operator keeps reports forever. In large clusters, this creates thousands of CRDs. Configure TTL:

vulnerabilityReports:
ttl: 24h # Delete reports older than 24 hours

Reports are recreated when pods restart or images change.

The operator needs permissions to:

  • Watch pods, deployments, jobs, configmaps in all namespaces
  • Create VulnerabilityReport and ConfigAuditReport CRDs
  • Create scanner jobs in the trivy-system namespace

The Helm chart creates a ClusterRole with these permissions. Review it:

Terminal window
kubectl get clusterrole trivy-operator -o yaml

Trivy Operator scans for known vulnerabilities and misconfigurations. It does not protect against:

  • Zero-day exploits (CVEs not yet in the database)
  • Application logic bugs
  • Runtime attacks (like process injection or network exfiltration)

For runtime protection, use tools like Falco, which detects suspicious behavior in running containers.

Trivy Operator continuously scans container images in your cluster and reports vulnerabilities via Kubernetes CRDs. It compares installed packages against a daily-updated CVE database and flags known security issues. Use it alongside CI scanning (pre-deployment) and runtime protection (Falco) for defense in depth.