Container Image Vulnerabilities: Scanning, CVEs, and Supply Chain Risk
How vulnerability scanners actually inspect container image layers, how to read a scan report, and the practices that reduce real supply-chain risk.
A container image isn’t just your application — it’s every package, library, and binary in every layer beneath it, and each of those is a potential source of a known vulnerability. Treating image scanning as a checkbox rather than understanding what it actually inspects is how “we scan our images” ends up providing far less real protection than it appears to.
What a scanner actually inspects
An image vulnerability scanner (Trivy, Grype, Clair, and similar tools) doesn’t run the image — it inspects its layers statically, extracting package manifests (dpkg/rpm/apk databases, language-specific lockfiles like package-lock.json or requirements.txt, even statically-linked Go binaries’ embedded module info) and cross-references every discovered package version against vulnerability databases.
trivy image nginx:1.25
nginx:1.25 (debian 12.4)
=========================
Total: 42 (CRITICAL: 2, HIGH: 11, MEDIUM: 20, LOW: 9)
┌─────────────┬────────────────┬──────────┬───────────────┬─────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Ver │ Fixed Ver │
├─────────────┼────────────────┼──────────┼───────────────┼─────────────────┤
│ libssl3 │ CVE-2023-5678 │ HIGH │ 3.0.11-1 │ 3.0.13-1 │
CVE severity is a starting point, not a verdict
CVSS severity scores (Critical/High/Medium/Low) reflect a vulnerability’s theoretical worst-case impact, not whether it’s actually exploitable in your specific image — a Critical vulnerability in a library your application never calls into is a very different real risk than a Medium one in code that parses untrusted network input directly. This is why mature scanning workflows filter and triage rather than blocking every build on any Critical/High finding indiscriminately, which quickly becomes unsustainable noise otherwise.
trivy image --severity CRITICAL,HIGH --ignore-unfixed nginx:1.25
--ignore-unfixed is a particularly important flag in practice — a vulnerability with no available fixed version yet can’t be remediated by updating anyway, so surfacing it as a build-blocking failure just creates noise without an actionable next step.
Base image choice is the highest-leverage decision
The single biggest lever over an image’s vulnerability surface isn’t scanning at all — it’s which base image you start from. A full ubuntu:22.04 base carries hundreds of packages your application never uses, each a potential CVE source; a minimal or distroless base carries dramatically fewer:
# Large surface: full OS userland
FROM ubuntu:22.04
# Small surface: distroless, no shell, no package manager
FROM gcr.io/distroless/base-debian12
Comparing the same application built on both bases with a scanner is usually the fastest way to make this trade-off concrete and visible to a team rather than abstract.
Scanning in CI: shifting left
Scanning should run at build time, before an image is ever pushed to a registry — catching a vulnerable dependency in a CI pipeline is dramatically cheaper than catching it after deployment:
- name: Scan image
run: trivy image --exit-code 1 --severity CRITICAL myapp:${{ github.sha }}
Registries themselves (Docker Hub, GHCR, ECR, GAR) increasingly run scanning automatically on push too — a second layer of defense, useful for catching newly-disclosed CVEs against images that were clean at build time but aren’t anymore now that a new CVE has been published against something already inside them.
Supply chain risk beyond just CVEs
Known-CVE scanning addresses one risk category, but modern supply-chain security also concerns itself with provenance — is this image actually built from the source you think it is, unmodified in transit. Image signing (via cosign/Sigstore) and SBOM (Software Bill of Materials) generation address this directly:
cosign sign --key cosign.key myregistry/myapp:latest
cosign verify --key cosign.pub myregistry/myapp:latest
syft myapp:latest -o spdx-json > sbom.json
An SBOM is a complete, structured manifest of every component in an image — useful both for scanning and for rapidly answering “are we affected by this new CVE” across an entire fleet of images the moment a new vulnerability is disclosed, without re-scanning everything from scratch.
Admission control: enforcing scan results at deploy time
Scanning in CI only helps if a vulnerable image is actually prevented from being deployed — Kubernetes admission controllers (covered in more depth elsewhere on this blog) can enforce this as a cluster-wide policy, rejecting any pod whose image fails a vulnerability or signature check regardless of which pipeline produced it:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredImageSignature
metadata:
name: require-signed-images
A practical baseline
For most teams, a reasonable minimum is: scan every image in CI, fail builds only on Critical/High vulnerabilities with an available fix, prefer minimal/distroless base images to shrink the surface in the first place, generate and store SBOMs for every release, and enforce signature verification at admission time in production clusters. None of this makes an image invulnerable — it makes known, fixable risk visible and blockable before it reaches production, which is the realistic, achievable goal.