Why use Trivy to extend pod security on Kubernetes

Life as a platform engineer

At my current assignment, I work as part of a platform engineering team. We have deployed a large number of applications to a Kubernetes cluster. Although most of these applications are not internet-facing, we are constantly looking for ways to improve our platform. But next to making things better, we also try to make things safer from external treats. But improving software isn’t just about quality, it is also about security. Protecting our workloads against newly discovered vulnerabilities is equally important.

Trivy against vulnerabilities

Our created software is packaged and published in containers. Also, on other levels we use container images. For that reason, we use Trivy (https://trivy.dev/; the all-in-one security scanner), a popular open-source vulnerability scanner. Initially, we incorporated the scan in our CI/ CD pipeline only. But then we realised that a deployed image, that is safe now, can ‘become’ vulnerable while it is running in production at the time a new CVE (https://www.cve.org/) is found.

Because of these reasons, we use Trivy on multiple levels. Find below our (simplified) implementation of the pipeline.

include:
- project: common-ci-project
  file:
  - /image/build.yaml
  - /version/env.yaml

stages:
- build
- test
- deploy

# Stage .pre
version:env:image:
  extends: .version:env:image

# Stage Build
image:build:
  extends: .image:build
  variables:
    IMAGE_NAME: my-image

# Stage test - vulnerabilities
image:trivy:
  stage: test
  image:
    name: our-harbor-proxy.nl/dockerhub/aquasec/trivy:0.67.2
    entrypoint: [""]
  allow_failure: false #strict
  variables:
    IMAGE_NAME: my-app
    TRIVY_AUTH_URL: ${CI_REGISTRY}
    TRIVY_CACHE_DIR: ".trivycache/"
    TRIVY_NO_PROGRESS: "true"
    TRIVY_PASSWORD: ${CI_REGISTRY_PASSWORD}
    TRIVY_USERNAME: ${CI_REGISTRY_USER}
  before_script:
    - FULL_IMAGE_NAME=${CI_REGISTRY_IMAGE}/${IMAGE_NAME}:${CUSTOM_TAG:-${IMAGE_VERSION:-${CI_COMMIT_TAG:-latest}}}
    - trivy --version
    # Update vulnerabilities db.
    - |
      retries=3
      while [ $retries -gt 0 ]; do
        if trivy image --download-db-only --db-repository "our-harbor-proxy.nl/ghcrhub/aquasecurity/trivy-db:2"; then
          break
        fi
        retries=$((retries - 1))
        echo "Retrying DB download... attempts left: $retries"
        sleep 5
      done
      if [ $retries -eq 0 ]; then
        echo "Failed to download vulnerability database after 3 attempts" && exit 1
      fi
  script:
    # Builds report and puts it in the default workdir $CI_PROJECT_DIR, so `artifacts:` can take it from there
    - >
      trivy image
      --skip-db-update
      --scanners vuln
      --exit-code 0
      --format template
      --template "@/contrib/gitlab.tpl"
      --java-db-repository "our-harbor-proxy.nl/ghcrhub/aquasecurity/trivy-java-db:1"
      --output "${CI_PROJECT_DIR}/gl-container-scanning-report.json" "${FULL_IMAGE_NAME}"
    # Prints full report
    - >
      trivy image
      --skip-db-update
      --skip-java-db-update
      --scanners vuln
      --exit-code 0
      "${FULL_IMAGE_NAME}"
    # Fail on critical vulnerabilities
    - trivy image
      --skip-db-update
      --skip-java-db-update
      --scanners vuln
      --ignore-unfixed
      --exit-code 1
      --severity CRITICAL
      "${FULL_IMAGE_NAME}"
  cache:
    paths:
      - .trivycache/
  artifacts:
    when: always
    reports:
      container_scanning: gl-container-scanning-report.json

If you prefer a more permissive approach, for example while gradually introducing vulnerability scanning, you can configure the job as follows:

allow_failure: true #permissive

When running a report is created, which can look like this:

Report

The day after

So, is this our final state? Of course not. As you can imagine, we are proud of what we accomplished. But sitting still is not in our mindset. With next generations of tools coming up, we see new opportunities, new improvements and a better and safer result. Even with Trivy in the pipeline, there are more options to improve, like using it in combination with the Trivy Operator (https://aquasecurity.github.io/trivy-operator/latest/). This operator extends the usage of Trivy functionality to the running objects on the cluster. Every relevant object is scanned and delivers one or more reports. An example is the vulnerability report. This report -generated as a CRD – contains the current vulnerabilities found of a deployment, replicaset, etcetera. The report shows, amongst others, the CVE id and Severity.

Find below a partial example of a vulnerability report for a replicaset.

❯ k get vulnerabilityreports.aquasecurity.github.io replicaset-5d87f7f4f4 -o yaml
apiVersion: aquasecurity.github.io/v1alpha1
kind: VulnerabilityReport
metadata:
  annotations:
    trivy-operator.aquasecurity.github.io/report-ttl: 24h0m0s
  creationTimestamp: "2025-12-11T18:11:00Z"
  generation: 1
  name: replicaset-5d87f7f4f4
  namespace: monitoring
  artifact:
    digest: sha256:7200df84a46ab7f4483f6e8a229afaa91483a0d46ad29a9c10ba6a4874db475d
    repository: dockerhub/bitnamilegacy/thanos
    tag: 0.37.2-debian-12-r9
  scanner:
    name: Trivy
    vendor: Aqua Security
    version: 0.66.0
  summary:
    criticalCount: 2
    highCount: 16
    lowCount: 0
    mediumCount: 30
    noneCount: 0
    unknownCount: 0
  vulnerabilities:
  - fixedVersion: 5.2.2
    installedVersion: v5.2.1
    lastModifiedDate: "2025-04-10T13:15:52Z"
    links: []
    packagePURL: pkg:golang/github.com/golang-jwt/jwt/v5@v5.2.1
    primaryLink: https://avd.aquasec.com/nvd/cve-2025-30204
    publishedDate: "2025-03-21T22:15:26Z"
    resource: github.com/golang-jwt/jwt/v5
    score: 7.5
    severity: HIGH
    target: ""
    title: 'golang-jwt/jwt: jwt-go allows excessive memory allocation during header
      parsing'
    vulnerabilityID: CVE-2025-30204

SBOM – Dependency Track

The Trivy operator also generates SBOMs (Software Bill of Material). With a complimentary tool – Dependency-Track (https://dependencytrack.org/), this SBOM can be used to give even better insights. The vulnerability reports of Trivy-operator itself are not used. Dependency-Track imports the SBOM and analyses it from its own vulnerability sources. From that analysis data is exposed in a front-end, which gives you insight from different angles. You can even create your own policies which may result in an overview showing the violations.

The end?

So you think this is the end?
Well… not quite. This is a new beginning.

As you might have guessed, creating a big, beautiful overview has consequences. Our next step is investigating the outcome and fix the critical vulnerabilities ASAP.