Skip to content
Go back

Trivy + Cosign: Scan and Sign Your Images

By SumGuy 5 min read
Trivy + Cosign: Scan and Sign Your Images

You’re Running Untrusted Code in Production

Every time you docker pull nginx:latest, you’re downloading a binary blob from the internet and running it as root on your infrastructure. You have no idea what’s in it. It could have 40 CVEs. It could have a backdoor. It could just be slightly compromised from a supply chain attack that happened three months ago and nobody’s noticed yet.

This is not paranoia. This is how container security actually works.

You need two things: a way to scan images for known vulnerabilities (Trivy), and a way to verify that the image you pulled is actually the one the author intended (Cosign). Together, they form a pretty solid foundation for not getting totally pwned.

Trivy: The Vulnerability Scanner That Actually Works

Trivy is a vulnerability scanner that scans container images, filesystems, and IaC files for known CVEs. It’s fast, it’s accurate, and it integrates everywhere.

Installing Trivy

On most systems:

Terminal window
# macOS with Homebrew
brew install trivy
# Ubuntu/Debian
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 trivy
# Or grab a binary from https://github.com/aquasecurity/trivy/releases

Scanning a Local Image

Pull an image and scan it:

Terminal window
docker pull node:20
trivy image node:20

You’ll get output that looks like this:

node:20 (debian 12.5)
Total: 18 (CRITICAL: 2, HIGH: 8, MEDIUM: 8, LOW: 0)
┌────────────┬───────────────┬──────────┬────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Size │
├────────────┼───────────────┼──────────┼────────────────┤
│ expat │ CVE-2024-1664 │ CRITICAL │ 2.6.2 │
│ gnutls │ CVE-2024-0553 │ HIGH │ 3.8.4 │
│ openssl │ CVE-2024-1086 │ CRITICAL │ 3.0.13 │
└────────────┴───────────────┴──────────┴────────────────┘

That’s 18 vulnerabilities in a base Node image. Welcome to container life.

Scanning a Remote Image

You don’t have to pull it first:

Terminal window
trivy image ghcr.io/your-org/your-app:v1.2.3

Trivy will fetch the image metadata directly from the registry and scan it.

Scanning Dockerfiles and Infrastructure Code

You can also scan your Dockerfile for misconfigurations:

Terminal window
trivy config ./Dockerfile

Or scan your entire infrastructure directory:

Terminal window
trivy config ./terraform/

Useful for catching things like RUN chmod 777 /data before they make it into production.

Integrating Trivy into Your CI Pipeline

Here’s where the magic happens. Set up Trivy in GitHub Actions to fail the build if any CRITICAL vulnerabilities are found:

.github/workflows/scan.yml
name: Trivy Image Scan
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'

Now every image gets scanned before it lands in your registry. If a CRITICAL CVE is found, the build fails. Problem solved — or at least surfaced.

Cosign: Proving Your Image Hasn’t Been Tampered With

Scanning tells you what’s in the image. Cosign tells you who signed it and proves it hasn’t been modified since.

Container registries are just HTTP servers. If someone can compromise the HTTP server (DNS hijack, registry compromise, man-in-the-middle), they can replace your image with anything. Cosign prevents that by cryptographically signing images.

Generating a Cosign Keypair

Terminal window
# Generate a keypair (you'll be prompted for a password)
cosign generate-key-pair
# This creates:
# - cosign.key (private key, keep this secret)
# - cosign.pub (public key, check this into your repo)

Signing an Image

Terminal window
# Sign the image (will prompt for your private key password)
cosign sign --key cosign.key your-registry.azurecr.io/your-app:v1.0.0

Verifying an Image

Before running an image, verify it was signed by you:

Terminal window
# Verify the signature
cosign verify --key cosign.pub your-registry.azurecr.io/your-app:v1.0.0

If the signature is invalid or missing, Cosign exits with an error. Use that to prevent unsigned images from running.

Keyless Signing: The Modern Approach

Storing a private key is annoying. Keyless signing uses your GitHub Actions identity (via OIDC) to sign without touching a key file:

.github/workflows/sign.yml
name: Build, Scan, and Sign
on:
push:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-scan-sign:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # Required for OIDC token
steps:
- uses: actions/checkout@v4
- name: Build and push image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Scan with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: 'table'
severity: 'CRITICAL'
- name: Sign image with Cosign (keyless)
uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.2.0'
- name: Sign the image
env:
REGISTRY_USERNAME: ${{ github.actor }}
REGISTRY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
COSIGN_EXPERIMENTAL: 1 # Enable keyless signing
run: |
cosign sign \
-y \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

No private key to manage. No secret to rotate every 90 days. GitHub’s identity is the key.

Enforcing Signature Verification at Deploy Time

Once everything’s signed, you can enforce verification in your deploy pipeline:

Terminal window
# Deployment script: verify before running
COSIGN_EXPERIMENTAL=1 cosign verify \
--certificate-identity "https://github.com/${{ github.repository_owner }}/${{ github.repository }}" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
your-image:tag && \
docker run your-image:tag || exit 1

If the signature verification fails, the deployment stops. You’re not running unsigned or tampered images.

The Two-Part Defense

Trivy catches vulnerable software. Cosign ensures the image is the real deal. Together, they form a supply chain you can actually trust.

It’s not perfect — there are still zero-days and misconfigurations you might miss. But it’s infinitely better than docker pull whatever and hoping for the best.

Do this. Your security team will appreciate it. Your future self at 2 AM during an incident will really appreciate it.


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
Auditd & Audit Logging: Know Exactly Who Touched What on Your Server
Next Post
Kernel Live Patching: Security Updates Without the 3am Reboot

Discussion

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

Related Posts