Skip to content
Go back

Container Security: Scan and Sign Your Images Like You Mean It

By SumGuy 6 min read
Container Security: Scan and Sign Your Images Like You Mean It

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:

Terminal window
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.list
apt-get update && apt-get install -y trivy

On macOS:

Terminal window
brew install trivy

Scanning an Image

Terminal window
trivy image nginx:latest

That’s it. Trivy will pull the image (if needed), scan all the layers, and dump a report. You’ll see something like:

Terminal window
nginx:latest (debian 12.1)
Found 23 vulnerabilities
CRITICAL: 2
HIGH: 8
MEDIUM: 13
LOW: 0
UNKNOWN: 0

Each 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:

Terminal window
# JSON for machine parsing
trivy image --format json --output report.json nginx:latest
# SARIF for GitHub Security tab integration
trivy image --format sarif --output report.sarif nginx:latest
# SBOM (Software Bill of Materials) — list every package
trivy image --format cyclonedx nginx:latest > sbom.xml

Scanning Filesystems

Trivy isn’t limited to pulling images from registries. You can scan a local directory:

Terminal window
trivy fs /path/to/app

Useful 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:

.github/workflows/container-security.yml
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:

Terminal window
wget https://github.com/sigstore/cosign/releases/download/v2.0.0/cosign-linux-amd64
chmod +x cosign-linux-amd64
sudo mv cosign-linux-amd64 /usr/local/bin/cosign

Signing an Image (in CI)

In your GitHub Actions workflow, add:

.github/workflows/build-and-sign.yml
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

Terminal window
cosign verify --certificate-identity-regexp ".*@github.com" \
--certificate-oidc-issuer-regexp ".*" \
myregistry.azurecr.io/myapp:latest

If 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.

Terminal window
cosign attest --predicate predicate.json myregistry.azurecr.io/myapp:latest
cosign verify-attestation myregistry.azurecr.io/myapp:latest

For 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:

  1. Build the image
  2. Scan with Trivy — block CRITICAL CVEs, warn on HIGH
  3. Sign with Cosign using Sigstore keyless signing
  4. Push to your registry
  5. 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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Woodpecker CI vs Drone CI: Lightweight Pipelines for People Who Hate Waiting
Next Post
Gitea vs Forgejo vs GitLab CE: Self-Hosted Git Without the Existential Crisis

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts