Module 6: Runtime Security Admission Control And Image Scanning

7. Hands-on: `trivy image` on `andes-cargo-status-api`

Description

This module's lessons 3-6 evaluated a Kubernetes object's shape: does this Pod have the fields the policy requires? This lesson switches layers completely — it evaluates the content of the Docker image that Pod references: what software, at what exact version, with what already-published known vulnerabilities, is installed inside andes-cargo-status-api:latest? trivy image answers that question by scanning every layer of the real image, not a configuration file describing it.

Connection to the module

cloud-security-and-guardrails-guide, in its Module 5, already used Trivy — but against a completely different artifact. That guide ran trivy config over .tf (HCL) files: it looks for infrastructure configuration declared insecurely, before it exists (an aws_s3_bucket with no encryption, for example), with nothing executed. This lesson runs trivy image: it looks for real vulnerabilities, already installed inside an image that's already built and already running. They're two different commands (trivy config versus trivy image), from the same binary, pointed at two completely different layers of the stack.


Confirm Trivy's version

trivy --version

What to expect:

Version: 0.74.0
Check Bundle:
  Digest: sha256:1583562f8b90ed2a071b99f0e5ffff6b57e4ceb6ca3e4796577b4e6a339eb74c
  DownloadedAt: 2026-08-14 16:37:24.938201 +0000 UTC

Version: 0.74.0 — the same exact version cloud-security-and-guardrails-guide already used. Digest and DownloadedAt are variable: they depend on when your own machine last downloaded the vulnerability database.


trivy image versus trivy config: the same binary, two completely different commands

   cloud-security-and-guardrails-guide, M5           this module (M6.7)

   trivy config andes-cargo-infra/                    trivy image andes-cargo-status-api:latest
              │                                                    │
              ▼                                                    ▼
   ┌──────────────────────┐                          ┌──────────────────────────┐
   │  reads .tf files          │                          │  reads image layers          │
   │  (text, nothing               │                          │  (real filesystem, already     │
   │   executed, no image           │                          │   built, with real software     │
   │   involved at all)             │                          │   installed)                    │
   └──────────┬───────────┘                          └──────────┬───────────────┘
              │ compares community rules                          │ compares installed packages
              │ against DECLARED configuration                    │ against CVE databases
              ▼                                                    ▼
   "this S3 bucket doesn't have                        "this Debian package, at this
    encryption enabled" (before                        exact version, has this
    the bucket exists)                                 already-published vulnerability"

trivy config answers "is this declared correctly?" — a matter of intent, verifiable with nothing executed. trivy image answers "does this have known, already-installed vulnerabilities?" — a matter of real content, which only exists once the image is already built with docker build (the same Dockerfile inherited from this guide's Module 1, never rewritten). They're different questions, about different objects, at different points in an artifact's lifecycle.


The complete scan

trivy image andes-cargo-status-api:latest

What to expect — the summary (the per-severity counts are real from this run; yours are going to differ depending on the date you run the command, because Trivy's CVE database updates constantly):

Report Summary

┌──────────────────────────────────────────────┬────────────┬─────────────────┬─────────┐
│                    Target                     │    Type    │ Vulnerabilities │ Secrets │
├──────────────────────────────────────────────┼────────────┼─────────────────┼─────────┤
│ andes-cargo-status-api:latest (debian 13.6)   │   debian   │       179       │    -    │
├──────────────────────────────────────────────┼────────────┼─────────────────┼─────────┤
│ Python                                        │ python-pkg │        5        │    -    │
└──────────────────────────────────────────────┴────────────┴─────────────────┴─────────┘

andes-cargo-status-api:latest (debian 13.6)
===========================================
Total: 179 (UNKNOWN: 30, LOW: 66, MEDIUM: 60, HIGH: 19, CRITICAL: 4)

184 total findings (179 in the base operating system + 5 in Python dependencies), split across two completely different categories:

  • andes-cargo-status-api:latest (debian 13.6) — 179 findings. These don't come from any code Andes Cargo wrote — they come from the image's base operating system (Debian 13.6, the distribution andes-cargo-status-api's Dockerfile builds on). Every system package (apt, bash, coreutils, bsdutils, etc.) carries its own history of CVEs, accumulated over the years, regardless of what application runs on top.
  • Python — 5 findings. These are specific to the dependencies andes-cargo-status-api declares — Flask, boto3, and their transitive dependencies — installed by pip inside the image.

The 4 CRITICAL findings

┌───────────┬────────────────┬──────────┬──────────────┬───────────────────┬───────────────┐
│  Library  │ Vulnerability  │ Severity │    Status    │ Installed Version │ Fixed Version │
├───────────┼────────────────┼──────────┼──────────────┼───────────────────┼───────────────┤
│ perl-base │ CVE-2026-13221 │ CRITICAL │ affected     │ 5.40.1-6          │               │
│           │ CVE-2026-42496 │          │ fix_deferred │                   │               │
│           │ CVE-2026-57433 │          │ affected     │                   │               │
│           │ CVE-2026-8376  │          │              │                   │               │
└───────────┴────────────────┴──────────┴──────────────┴───────────────────┴───────────────┘
Total: 4 (CRITICAL: 4)

This run's four CRITICAL findings all fall, all four, on the same package: perl-base 5.40.1-6, the minimal Perl install Debian ships as part of its base system (used internally by the operating system's own tools, not by Andes Cargo's code). Notice the Fixed Version column: it's empty in all four cases — there's no fixed version available yet for any of the four, in Debian's package repository, at the time of this scan. Status: affected/fix_deferred confirms the same from another angle: Debian knows about these vulnerabilities, and hasn't published (or decided to defer) the patch yet. This isn't a mistake in Andes Cargo's Dockerfile — it's the real, public state of that specific package at that exact moment. This module's lesson 8 picks this table back up to make a real gate decision on it.


The findings in Python dependencies

trivy image --format json andes-cargo-status-api:latest | \
  python3 -c "
import json, sys
data = json.load(sys.stdin)
for r in data.get('Results', []):
    if r.get('Target') == 'Python':
        for v in r.get('Vulnerabilities', []) or []:
            print(v['Severity'], v['PkgName'], v['VulnerabilityID'], v['InstalledVersion'], '->', v.get('FixedVersion'))
"

What to expect (real from this run — exact CVEs and severity vary with the scan's date, andes-cargo-status-api's package list doesn't):

LOW    Flask       CVE-2025-47278 3.1.0   -> 3.1.1
LOW    Flask       CVE-2026-27205 3.1.0   -> 3.1.3
HIGH   msgpack     GHSA-6v7p-g79w-8964 1.1.2 -> 1.2.1
HIGH   setuptools  CVE-2025-47273 70.3.0  -> 78.1.1
MEDIUM setuptools  CVE-2026-59890 70.3.0  -> 83.0.0

Unlike perl-base's four CRITICAL findings, these do have a Fixed Version available in every row — updating Flask to 3.1.3, msgpack to 1.2.1, and setuptools to 83.0.0 (in requirements.txt, with the Dockerfile untouched) would resolve all five at once. In a real team's practice, this is the most actionable finding category: application dependencies, with an already-published patch, only waiting for someone to bump a version and rebuild the image.


Common mistakes

Confusing Debian's 179 findings with "Andes Cargo's code vulnerabilities" (wrong attribution). What happens: someone sees Total: 179 under the image's name and concludes the Andes Cargo team wrote insecure code. How to spot it: if your reaction is "what did we do wrong in 179 places?" How to fix it: look at the full table's Library column — apt, bash, coreutils, bsdutils, perl-base — none of those packages is Andes Cargo code; they're part of the base operating system (Debian 13.6) any image built on that base inherits. The only 5 findings directly tied to team decisions (which version of Flask, boto3, etc. to declare) are in the Python section, explicitly separated in the summary.

Expecting trivy image to rebuild or modify the image (confusion with a different kind of tool). What happens: someone runs trivy image andes-cargo-status-api:latest and expects, once it's done, the local image to have patches applied automatically. How to spot it: if you check, after the scan, for any change in docker images or in the image's size. How to fix it: trivy image is strictly read-only — it inspects layers, compares installed versions against CVE databases, and prints a report. It never modifies the scanned image. Fixing a real finding (for example, updating Flask in requirements.txt) is a separate, manual step the team decides to take after reading the report — the exact same pattern cloud-security-and-guardrails-guide already established with trivy config in its Module 5, lesson 6 ("Fixing, suppressing, or accepting a finding").

Assuming an empty Fixed Version means "Trivy couldn't determine it" (misreading the field). What happens: someone sees an empty Fixed Version in perl-base's table and assumes it's a tool limitation, not the package's real state. How to spot it: if your conclusion is "we need a different tool to see the fixed version." How to fix it: an empty Fixed Version, together with Status: affected or fix_deferred, means Debian itself hasn't published a fixed version for that specific package in that repository yet — it isn't a Trivy limitation, it's real, verifiable information about the state of the ecosystem the image depends on. Trivy reports the state as it exists; it doesn't invent or fill it in.


Exercises

Exercise 1 — Classify five hypothetical findings by whether they're immediately actionable. Without running any new command, for each of these hypothetical findings, decide whether a team could resolve it today by updating a version: (a) Fixed Version: 2.4.1, Status: fixed; (b) Fixed Version: (empty), Status: affected; (c) Fixed Version: 1.9.0, Status: fixed; (d) Fixed Version: (empty), Status: fix_deferred.

See solution

(a) and (c) are immediately actionable — both have a published fixed version (Status: fixed with a non-empty Fixed Version); a team can update that dependency today and resolve the finding. (b) and (d) are not immediately actionable — neither has a fixed version available yet (Fixed Version empty), regardless of whether the status is affected or fix_deferred; the only real option, until the package's maintainer publishes a patch, is documenting the risk as accepted (the same pattern from cloud-security-and-guardrails-guide, Module 5, lesson 6) or looking for a different mitigation (a different base image, for example). If you classified all four correctly, you have this lesson's central criterion: an empty Fixed Version isn't the team's problem, it's a limit of the ecosystem.

Exercise 2 — Explain why cloud-security-and-guardrails-guide's trivy config wouldn't have found any of this lesson's 184 findings. In one sentence, explain why running trivy config over the andes-cargo-k8s repository (this guide's YAML manifests) would never have shown any of the perl-base or Flask findings you saw in this lesson.

See solution

Because trivy config reads configuration text — the .yaml/.tf files themselves, on disk — and looks for insecurely declared configuration patterns right there; it never opens, downloads, or inspects the real content of any Docker image referenced by a manifest's image: field. This lesson's 184 findings live inside andes-cargo-status-api:latest's filesystem layers — installed Debian packages, installed Python dependencies — a completely different artifact from any YAML file that references it by name.

Exercise 3 — Design the change that would resolve, all at once, the 5 Python findings. Based on this lesson's Python findings table, what exact change (nothing executed yet) would resolve all five findings at once, with the Dockerfile untouched?

See solution

Update requirements.txt (or the equivalent dependency file) pinning Flask>=3.1.3, msgpack>=1.2.1, and setuptools>=83.0.0 — or simply loosening the version pins so pip install picks up the latest available of each — and then rebuild the image with docker build (the same Dockerfile from Module 1's lesson 7, with no content changes, only the versions requirements.txt pins). None of the three requires changing the Debian base image or the application code — all three are third-party dependency updates, with a patch already published by their respective maintainers.


Summary and next step

This lesson actually ran trivy image andes-cargo-status-api:latest, against the image running in andes-cargo-cluster since Module 1, and found 184 real findings: 179 inherited from the base operating system (Debian 13.6, none attributable to Andes Cargo's code), and 5 in Python dependencies the team declared, each of the latter with an already-published fixed version. You saw, with this lesson's diagram, why trivy image is a completely different command from trivy config (the one cloud-security-and-guardrails-guide used): one inspects an already-built image's real content; the other reads configuration text, touching no image at all.

Before moving on you should be able to: explain the difference between trivy config and trivy image in one sentence; read a Trivy findings table and identify which ones are actionable today (Fixed Version column non-empty) versus which aren't; and explain why most of a real image's findings aren't its own code's fault, but the inherited base operating system's.

Next lesson: project — Andes Cargo's runtime guardrails. There you're going to activate Gatekeeper and Kyverno at the same time over andes-cargo, honestly document what happens when both evaluate the same object, and use this lesson's CRITICAL finding as the real basis for a trivy image gate that runs before loading any new image into the cluster.

Resources

  1. Trivy — Container Image — official documentation for trivy image, this lesson's central command.
  2. Trivy — Config Scanning — official documentation for trivy config, contrasted here with trivy image.
  3. cloud-security-and-guardrails-guide (NIEVA), Module 5, lesson 6 — the "fix, suppress, or accept" pattern for a finding with no available patch, applicable to this lesson's four perl-base CRITICALs.
  4. Debian Security Tracker — the public source for Status: affected/fix_deferred for base operating system packages.