The Uncomfortable Truth About Container Images
You’re running a container from Docker Hub right now. Probably a bunch of them. And here’s the thing: you have no idea what’s actually inside them.
That nginx:latest you pulled six months ago? It’s got seventeen CVEs baked into the base OS. That open-source app you grabbed? The maintainer hasn’t updated it since 2023. And nobody’s stopping someone from building a malicious image with the exact same name as the one you trust, uploading it, and hoping you don’t notice.
This isn’t paranoia. It’s Tuesday in the supply chain attack calendar.
The fix is two-fold: scan your images for vulnerabilities (Trivy) and prove they haven’t been tampered with (Cosign). Together, they form a security checkpoint that actually catches problems instead of just hoping for the best.
Let’s build that checkpoint.
Part 1: Trivy — Your Container Vulnerability Scanner
Trivy is a vulnerability scanner that runs locally, doesn’t need a database server, and tells you exactly what’s wrong with your image. It’s fast, accurate, and free.
Installation
On Linux:
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | apt-key add -echo "deb https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | tee -a /etc/apt/sources.list.d/trivy.listapt-get update && apt-get install -y trivyOn macOS:
brew install trivyScanning an Image
trivy image nginx:latestThat’s it. Trivy will pull the image (if needed), scan all the layers, and dump a report. You’ll see something like:
nginx:latest (debian 12.1)
Found 23 vulnerabilities CRITICAL: 2 HIGH: 8 MEDIUM: 13 LOW: 0 UNKNOWN: 0Each CVE gets a score, a link to CVE details, and the affected package. It’s a one-command wall of truth.
Output Formats
If you need to feed the results into other tools (CI/CD, ticketing systems, dashboards), Trivy can spit out JSON, SARIF, or SBOM formats:
# JSON for machine parsingtrivy image --format json --output report.json nginx:latest
# SARIF for GitHub Security tab integrationtrivy image --format sarif --output report.sarif nginx:latest
# SBOM (Software Bill of Materials) — list every packagetrivy image --format cyclonedx nginx:latest > sbom.xmlScanning Filesystems
Trivy isn’t limited to pulling images from registries. You can scan a local directory:
trivy fs /path/to/appUseful if you’re building a custom image and want to know what you’re about to ship.
GitHub Actions: Fail the Build on Critical CVEs
Here’s the real magic — lock CVE scanning into your CI pipeline so a bad image never reaches production:
name: Container Security
on: [push, pull_request]
jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Build image run: docker build -t myapp:${{ github.sha }} .
- name: Scan with Trivy uses: aquasecurity/trivy-action@master with: image-ref: myapp:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH'
- name: Upload SARIF to GitHub Security uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif'
- name: Fail on critical vulns run: trivy image --exit-code 1 --severity CRITICAL myapp:${{ github.sha }}The --exit-code 1 flag is the key: the build fails if any CRITICAL CVEs are found. MEDIUM? You’ll see the report but the build proceeds. It’s a pragmatic balance — not everything is a showstopper, but critical gets the veto.
Part 2: Cosign — Proving Your Image Is Actually Yours
Trivy tells you what’s inside the box. Cosign proves the box came from you and hasn’t been swapped with a counterfeit.
Image signing is about supply chain integrity. When you push an image to a registry, Cosign creates a cryptographic signature. When someone else pulls it, Cosign verifies the signature matches. If someone tampered with the image (even changing one byte), the signature breaks.
Keyless Signing with Sigstore
The old way: manage private keys yourself (pain). The new way: Sigstore OIDC — your GitHub/GitLab/Google identity becomes your signing key, no key management required.
Installation:
wget https://github.com/sigstore/cosign/releases/download/v2.0.0/cosign-linux-amd64chmod +x cosign-linux-amd64sudo mv cosign-linux-amd64 /usr/local/bin/cosignSigning an Image (in CI)
In your GitHub Actions workflow, add:
name: Build, Scan, and Sign
on: [push]
jobs: build: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - uses: actions/checkout@v4
- name: Build and push image run: | docker build -t myregistry.azurecr.io/myapp:${{ github.sha }} . docker push myregistry.azurecr.io/myapp:${{ github.sha }}
- name: Sign image with Cosign env: COSIGN_EXPERIMENTAL: 1 run: | cosign sign --yes myregistry.azurecr.io/myapp:${{ github.sha }}The magic: COSIGN_EXPERIMENTAL=1 tells Cosign to use OIDC token from GitHub Actions. No keys. No secrets. Just your GitHub identity signing the image.
Verifying the Signature
cosign verify --certificate-identity-regexp ".*@github.com" \ --certificate-oidc-issuer-regexp ".*" \ myregistry.azurecr.io/myapp:latestIf the signature is valid, you get the cert details. If someone tampered with the image, cosign verify fails hard.
SLSA Attestations: One Step Further
Cosign can also attach SLSA provenance attestations to images — cryptographic proof of how the image was built, when, and by whom. It’s the full chain of custody.
cosign attest --predicate predicate.json myregistry.azurecr.io/myapp:latestcosign verify-attestation myregistry.azurecr.io/myapp:latestFor now, know it exists. If you need to prove to a customer or auditor exactly how an image was built, attestations are your answer.
Part 3: Putting It Together in Your Deployment
Your full security gate looks like this:
- Build the image
- Scan with Trivy — block CRITICAL CVEs, warn on HIGH
- Sign with Cosign using Sigstore keyless signing
- Push to your registry
- On the server, verify the signature before running:
cosign verify ... && docker run ...
That’s it. From local dev to production, CVEs are caught, and supply chain tampering is detectable.
Harbor Integration
If you’re already running Harbor (your private container registry), here’s the shortcut: Harbor has Trivy built in. Enable it in Harbor’s admin UI under “Scanning” → “Trivy” and every push gets scanned automatically. Signatures are stored in Harbor too. One dashboard, both tools.
The Practical Policy
Here’s the honest advice: block CRITICAL, warn on HIGH, ignore MEDIUM unless you’ve got time.
Not because MEDIUM vulns don’t matter — they do. But because the universe of vulns is infinite, and not all of them are exploitable in your specific setup. A CRITICAL in a package you don’t even use is still noise. Use that noise to fuel risk decisions, not to stop deployments.
Sign everything, though. That part isn’t negotiable. Supply chain attacks are real, they’re increasing, and Cosign keyless signing removes the excuse of “managing keys is hard.”
Your 2 AM self, debugging why a container image changed without you touching it, will thank you.