Your container image is only as secure as the weakest link in its supply chain. From base images and package managers to build systems and registries — every step is a potential attack vector. This lesson covers how to scan, sign, attest, and enforce trust across the entire pipeline.

1. The Supply Chain Problem

A typical container image depends on dozens of components you didn't write:

  • Base image — an OS layer maintained by someone else
  • OS packages — apt/apk/yum packages pulled at build time
  • Application dependencies — npm, pip, Maven packages
  • Build tools — compilers, CI plugins, Dockerfile instructions

Each of these is an attack vector. The SolarWinds attack (2020) showed how adversaries inject malicious code into trusted build pipelines. Supply chain compromises in event-stream (npm), codecov (CI), and ua-parser-js demonstrated the same pattern: attack the dependency, compromise everyone downstream.

Trust AND Verify: It's not enough to use "official" images. You need automated, continuous verification — scanning, signing, attestation — applied at every stage.

2. Vulnerability Scanning

Scanners compare the packages in your image against known vulnerability databases (NVD, OSV, vendor advisories).

What Gets Scanned

  • OS packages — CVEs in libc, openssl, curl, etc.
  • Application dependencies — npm audit, pip safety, Go vulncheck
  • Misconfigurations — running as root, exposed secrets, unnecessary capabilities

Tools Comparison

Tool Type OS Pkgs App Deps SBOM CI Integration Cost
Trivy CLI / CI GitHub Actions, GitLab, Jenkins Free (OSS)
Grype CLI / CI Via Syft GitHub Actions, any CI Free (OSS)
Docker Scout CLI / Docker Desktop Docker Build Cloud Free tier + paid
Snyk Container SaaS + CLI All major CI systems Free tier + paid

Example: Trivy Scan Output

$ trivy image myapp:latest

myapp:latest (debian 12.4)
===========================
Total: 23 (UNKNOWN: 0, LOW: 12, MEDIUM: 7, HIGH: 3, CRITICAL: 1)

┌──────────────┬──────────────────┬──────────┬─────────────────────┐
│   Library    │  Vulnerability   │ Severity │   Fixed Version     │
├──────────────┼──────────────────┼──────────┼─────────────────────┤
│ libssl3      │ CVE-2024-0727    │ CRITICAL │ 3.0.13-1~deb12u1    │
│ libcurl4     │ CVE-2024-2004    │ HIGH     │ 7.88.1-10+deb12u5   │
│ zlib1g       │ CVE-2023-45853   │ HIGH     │ 1:1.2.13.dfsg-1+1   │
│ libc6        │ CVE-2024-2961    │ HIGH     │ 2.36-9+deb12u7      │
└──────────────┴──────────────────┴──────────┴─────────────────────┘

CI Integration: Fail on Critical

# GitHub Actions step
- name: Scan image
  run: |
    trivy image --exit-code 1 --severity CRITICAL,HIGH myapp:${{ github.sha }}
# Exit code 1 = vulnerabilities found → build fails

3. Software Bill of Materials (SBOM)

An SBOM is a complete inventory of everything inside your image — every package, library, and version. Think of it as an ingredient list for software.

Why SBOMs Matter

  • Incident response — When Log4Shell hit, teams with SBOMs knew in minutes which images were affected
  • Compliance — Regulations now mandate SBOMs (see callout below)
  • License auditing — Know what licenses you're shipping
  • Dependency tracking — Understand your full transitive dependency tree

Formats

  • SPDX — Linux Foundation standard, ISO/IEC 5962:2021
  • CycloneDX — OWASP standard, designed for security use cases

Generating an SBOM

# Using syft (Anchore)
$ syft myapp:latest -o spdx-json > sbom.spdx.json

# Using Docker
$ docker sbom myapp:latest --format cyclonedx-json > sbom.cdx.json

# Output includes every package:
# { "name": "openssl", "version": "3.0.13", "type": "deb", ... }
🏛️ Industry: Executive Order 14028 (US, 2021)
The US federal government now requires SBOMs from all software vendors selling to federal agencies. This cascades to the private sector: if you supply software to enterprises or government, you need SBOM generation in your pipeline. The EU Cyber Resilience Act introduces similar requirements for the European market.

4. Image Signing

Signing proves that an image came from a trusted source and hasn't been tampered with. The leading tool is cosign from the Sigstore project.

How It Works

  1. Build the image and push to registry
  2. Sign the digest (not the tag!) with cosign
  3. Signature is stored in the registry alongside the image
  4. At deploy time, verify the signature before pulling
⚠️ Why sign the digest, not the tag?
Tags are mutable — myapp:latest can point to different images over time. A digest (sha256:abc123...) is immutable and content-addressed. Signing the digest guarantees you're verifying the exact bytes of the image, not just a name that could be reassigned.

Cosign in Practice

# Generate a key pair (once)
$ cosign generate-key-pair

# Sign after push
$ cosign sign --key cosign.key registry.example.com/myapp@sha256:abc123...

# Verify before deploy
$ cosign verify --key cosign.pub registry.example.com/myapp@sha256:abc123...

Verification for registry.example.com/myapp@sha256:abc123... --
The following checks were performed:
- The cosign claims were validated
- The signatures were verified against the specified public key

Keyless Signing (OIDC)

In CI environments, cosign supports keyless signing — no long-lived keys to manage. Instead, it uses your CI system's OIDC identity (e.g., GitHub Actions' token) to get a short-lived certificate from Sigstore's Fulcio CA. The signature is recorded in a transparency log (Rekor).

# In GitHub Actions — no keys needed!
- name: Sign image
  run: cosign sign registry.example.com/myapp@${{ steps.build.outputs.digest }}
  env:
    COSIGN_EXPERIMENTAL: 1  # enables keyless

Docker Content Trust (Notary)

Docker's built-in signing mechanism. Enable with export DOCKER_CONTENT_TRUST=1. Less flexible than cosign but integrated into the Docker CLI. Being superseded by Notary v2 (notation).

5. Provenance & Attestation

Signing tells you WHO built an image. Provenance tells you WHERE and HOW.

SLSA Framework

SLSA (Supply-chain Levels for Software Artifacts) defines increasing levels of build integrity:

  • Level 1 — Build process is documented
  • Level 2 — Build service generates provenance
  • Level 3 — Build runs on hardened, isolated infrastructure

Build Provenance

Provenance attestation records:

  • Which CI system ran the build
  • Which source commit triggered it
  • Which Dockerfile was used
  • Build parameters and environment
# Generate provenance with BuildKit
$ docker buildx build --provenance=true --push -t registry.example.com/myapp:v1.2 .

# Inspect provenance
$ docker buildx imagetools inspect registry.example.com/myapp:v1.2 --format '{{json .Provenance}}'
{
  "buildType": "https://mobyproject.org/buildkit@v1",
  "builder": { "id": "https://github.com/actions/runner" },
  "metadata": {
    "buildInvocationID": "run-12345",
    "completeness": { "parameters": true, "environment": true }
  },
  "materials": [
    { "uri": "git+https://github.com/org/repo@refs/heads/main", "digest": {"sha1": "abc..."} }
  ]
}

6. Admission Control

All the scanning, signing, and attesting is pointless if nothing enforces policies at deploy time. Admission controllers are the gatekeepers.

What They Enforce

  • Only signed images may be deployed
  • Only images from approved registries
  • No images with critical CVEs
  • SBOM must be present and valid
  • Provenance must match expected build system

Tools

  • Kyverno — Kubernetes-native policy engine; YAML-based rules
  • OPA Gatekeeper — Open Policy Agent for Kubernetes; Rego language
  • Sigstore policy-controller — Validates cosign signatures and attestations
# Kyverno policy: only allow signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  rules:
  - name: check-signature
    match:
      any:
      - resources:
          kinds: ["Pod"]
    verifyImages:
    - imageReferences: ["registry.example.com/*"]
      attestors:
      - entries:
        - keys:
            publicKeys: |-
              -----BEGIN PUBLIC KEY-----
              MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
              -----END PUBLIC KEY-----

7. Base Image Policies

Golden Base Images

Enterprises maintain golden base images: company-approved, pre-hardened, regularly rebuilt images that all teams must use.

  • Pre-scanned and signed by the security team
  • Stripped of unnecessary packages
  • Automatically rebuilt when upstream patches land
  • Published to an internal registry with enforced freshness

Distroless & Minimal Alternatives

  • Chainguard Images — Minimal, SBOM-included, daily-rebuilt, zero-CVE target
  • Wolfi — Chainguard's undistro: an OS designed for containers with apk and no glibc baggage
  • Google Distroless — No shell, no package manager, minimal attack surface

Automated Rebuilds

# Rebuild when base image updates (GitHub Actions)
on:
  schedule:
    - cron: '0 3 * * *'  # daily at 3am
  repository_dispatch:
    types: [base-image-updated]

jobs:
  rebuild:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Build and push
      run: |
        docker buildx build --pull --push \
          --provenance=true \
          -t registry.example.com/myapp:latest .

Supply Chain Flow

Source Code + Deps Build Dockerfile Scan Trivy / SBOM Sign Cosign Store Registry Deploy Verify 🛡️ CHECKPOINT 🛡️ CHECKPOINT 🛡️ CHECKPOINT 🛡️ CHECKPOINT Reproducible + Provenance No critical CVEs + SBOM generated Digest signed + Attestation Admission control Each 🛡️ checkpoint must pass before the artifact moves forward

🧪 Hands-On: The Full Supply Chain

Practice the complete supply chain workflow: generate SBOM, scan, and sign.

# Prerequisites: install syft, trivy, cosign
# (On macOS: brew install syft trivy cosign)

# 1. Build an image
$ docker build -t myapp:v1 .

# 2. Generate SBOM
$ syft myapp:v1 -o cyclonedx-json > sbom.cdx.json
$ echo "SBOM contains $(cat sbom.cdx.json | jq '.components | length') components"

# 3. Scan for vulnerabilities
$ trivy image --severity HIGH,CRITICAL myapp:v1
# Review results — fix critical issues before continuing

# 4. Push to registry (use local registry for practice)
$ docker run -d -p 5000:5000 --name registry registry:2
$ docker tag myapp:v1 localhost:5000/myapp:v1
$ docker push localhost:5000/myapp:v1

# 5. Sign the image digest
$ DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' localhost:5000/myapp:v1)
$ cosign generate-key-pair  # creates cosign.key + cosign.pub
$ cosign sign --key cosign.key $DIGEST

# 6. Verify the signature
$ cosign verify --key cosign.pub $DIGEST

# ✅ You now have: SBOM + Scan results + Signed image

🧠 Knowledge Check

🌐 Not Just Docker: Sigstore (cosign, Rekor, Fulcio) is completely runtime-agnostic. It works with any OCI-compliant image — whether you run it with Docker, Podman, containerd, CRI-O, or any other runtime. The signatures and attestations are stored as OCI artifacts in standard registries.

Key Takeaways

  • Every dependency is an attack vector — base images, OS packages, app libraries, build tools
  • Scan continuously — integrate Trivy/Grype in CI; fail builds on critical vulnerabilities
  • Generate SBOMs — know what's inside your images; comply with regulations
  • Sign digests, not tags — use cosign for immutable, verifiable trust
  • Attest provenance — prove where and how the image was built (SLSA)
  • Enforce at deploy time — admission controllers make policies actionable, not advisory
  • Use golden base images — pre-hardened, pre-scanned, automatically rebuilt