Home DevOps Docker Image Security Scanning: Stop Shipping Vulnerable Containers to Production
Advanced 9 min · July 11, 2026

Docker Image Security Scanning: Stop Shipping Vulnerable Containers to Production

Docker image security scanning explained with real production failures.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 30 min
  • Docker CLI basics (build, run, push)
  • Understanding of container images and layers
  • Familiarity with CI/CD pipelines
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

To scan a Docker image for vulnerabilities, run trivy image your-image:tag. This checks all installed packages against the CVE database. For CI/CD, add a scan step that fails the build if critical vulnerabilities are found. Use .trivyignore to suppress false positives.

✦ Definition~90s read
What is Docker Image Security Scanning?

Docker image security scanning is the automated process of analyzing container images for known vulnerabilities (CVEs), misconfigurations, and secrets. Tools like Trivy, Grype, and Docker Scout compare image contents against vulnerability databases to flag risks before deployment.

Think of a Docker image like a pre-packed suitcase for your app.
Plain-English First

Think of a Docker image like a pre-packed suitcase for your app. Security scanning is like an airport security check that X-rays every item inside. It flags any banned items (vulnerable packages) or suspicious objects (secrets). You wouldn't fly without checking your bag, so don't deploy without scanning your image.

You just pushed a Docker image to production. Two hours later, your security team calls: 'We found a critical CVE in your base image.' Now you're scrambling to rebuild, retest, and redeploy. Sound familiar? Docker image security scanning isn't optional anymore — it's the difference between a routine deploy and a breach notification.

The problem is simple: your image is a stack of layers, each potentially carrying vulnerable packages. Base images like node:18 or python:3.11 are updated frequently, but your Dockerfile might pin an old version. Without scanning, you're blind to known vulnerabilities until it's too late.

By the end of this, you'll be able to integrate vulnerability scanning into your Docker build pipeline, interpret scan results like a pro, and avoid the gotchas that burn teams in production. You'll know which tool to use when, how to handle false positives, and how to set up automated gates that stop vulnerable images from ever reaching your registry.

Why You Can't Trust Your Base Image

Every Docker image starts with a base — FROM node:18, FROM python:3.11-slim, FROM alpine:3.18. These images are maintained by communities, but they're not immune to vulnerabilities. A single outdated package in the base can expose your entire application.

Consider this: the node:18 image contains over 100 OS packages (glibc, openssl, zlib, etc.). Each has a version string that maps to CVEs. If you don't scan, you're flying blind. The worst part? Base images are updated frequently, but your Dockerfile might pin a specific tag like node:18 which points to a moving target. Today's node:18 is not tomorrow's node:18.

The fix: always pin base images by digest (sha256:...) and scan them before building your app layer. This gives you reproducible builds and a known vulnerability baseline.

DockerfileDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// io.thecodeforge — DevOps tutorial

# Bad: tag-based, changes without notice
# FROM node:18

# Good: digest-based, reproducible
FROM node:18@sha256:abc123def456...

# Now scan this base image before adding your code
# $ trivy image node:18@sha256:abc123def456...

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Output
No output — this is a Dockerfile example.
⚠ Production Trap: Tag Drift
Using node:18 without a digest means your CI might build against a different image today than yesterday. If the base image gets a security update, your build might suddenly include new packages — or worse, break. Always pin digests in production.
docker-image-security-scanning THECODEFORGE.IO Container Security Scanning Stack Layered approach from base image to runtime Base Image Distroless | Alpine | Ubuntu LTS Build Stage Multi-stage Dockerfile | Dependency install Scanner Integration Trivy | Grype | Docker Scout CI/CD Pipeline GitHub Actions | Jenkins | GitLab CI Registry & Runtime Container Registry | Kubernetes Admission THECODEFORGE.IO
thecodeforge.io
Docker Image Security Scanning

Choosing the Right Scanner: Trivy vs Grype vs Docker Scout

You have options. Trivy (Aqua Security) is the Swiss Army knife — scans OS packages, language-specific deps, IaC misconfigurations, and even Kubernetes manifests. Grype (Anchore) focuses on container images and integrates tightly with Syft for SBOM generation. Docker Scout is Docker's own offering, baked into Docker Desktop and Hub.

Which one should you use? If you're already in the Docker ecosystem and want zero-config, Docker Scout is tempting. But it's a SaaS product — your image data leaves your network. For air-gapped environments or strict compliance, Trivy or Grype are better. Trivy wins on speed and breadth: it can scan a 1GB image in under 30 seconds. Grype is slower but produces more detailed SBOMs.

My take: use Trivy for CI/CD gates (fast, reliable) and Grype for compliance audits (detailed SBOM). Docker Scout is fine for local dev awareness but don't rely on it as your only gate.

scan-comparison.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// io.thecodeforge — DevOps tutorial

# Install Trivy (macOS)
brew install trivy

# Install Grype
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin

# Scan with Trivy (fast)
trivy image --severity CRITICAL,HIGH --no-progress myapp:latest

# Scan with Grype (detailed SBOM)
grype myapp:latest --only-fixed --fail-on high

# Docker Scout (requires Docker Desktop)
docker scout quickview myapp:latest
Output
Trivy output (example):
2024-01-15T10:00:00Z INFO Vulnerability scanning is enabled
2024-01-15T10:00:30Z INFO Number of language-specific files: 0
2024-01-15T10:00:30Z INFO Found 2 vulnerabilities (CRITICAL: 1, HIGH: 1)
Total: 2 (CRITICAL: 1, HIGH: 1)
┌──────────┬────────────────┬──────────┬──────────┐
│ Library │ Vulnerability │ Severity │ Status │
├──────────┼────────────────┼──────────┼──────────┤
│ libssl │ CVE-2024-1234 │ CRITICAL │ fixed in │
│ curl │ CVE-2024-5678 │ HIGH │ fixed in │
└──────────┴────────────────┴──────────┴──────────┘
💡Senior Shortcut: Cache the DB
Trivy downloads its vulnerability database on first run. In CI, cache the ~/.cache/trivy directory between builds to avoid downloading 50MB every time. Use --skip-db-update if the DB is fresh (e.g., updated once per day).

Integrating Scanning into CI/CD: The Gate That Actually Works

A scan that doesn't block the pipeline is just a report nobody reads. You need a hard gate: if the image has any CRITICAL vulnerability with a known fix, fail the build. But beware — not all CVEs are equal. Some are in unused packages, some have no exploit in the wild, and some are in test dependencies.

The trick: use --fail-on with a severity threshold, but also maintain a .trivyignore for accepted risks. Every ignored CVE must have a comment with a ticket number and expiration date. This prevents 'ignore and forget'.

Here's a production-grade CI step for GitHub Actions that scans, fails on critical/high, and allows exemptions.

.github/workflows/docker-scan.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// io.thecodeforge — DevOps tutorial

name: Docker Security Scan

on:
  push:
    branches: [main]
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'  # Fail on any critical/high
          ignore-unfixed: true  # Only fail on vulnerabilities with a fix

      - name: Upload Trivy results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: 'trivy-results.sarif'

      - name: Fail if critical vulnerabilities older than 30 days
        run: |
          # Custom check: fail if any critical CVE has been known for >30 days
          trivy image --severity CRITICAL --format json myapp:${{ github.sha }} | \
            jq -e '.Results[].Vulnerabilities[] | select(.PublishedDate < (now - 30*86400))' > /dev/null && \
            echo "Found old critical CVE" && exit 1 || echo "All critical CVEs are recent"
Output
On failure:
Error: exit code 1
Found old critical CVE
On success:
All critical CVEs are recent
🔥Interview Gold: Ignoring Unfixed Vulnerabilities
The --ignore-unfixed flag is critical. Without it, you'll fail on CVEs that have no patch yet — blocking your deploy for a problem you can't fix. Always use this flag in CI, and track unfixed CVEs separately with a risk acceptance process.
Trivy vs Grype for Image Scanning Trade-offs in speed, accuracy, and ecosystem Trivy Grype Vulnerability Database NVD, RedHat, Debian, Alpine NVD, RedHat, Ubuntu, Alpine Scan Speed Fast (cached DB) Moderate (full DB fetch) Language Support Python, Node, Java, Go, Rust Python, Node, Java, Go, Ruby CI/CD Integration Native GitHub Actions, GitLab GitHub Actions, Jenkins plugin False Positive Rate Low (vendor-verified) Moderate (aggregated sources) THECODEFORGE.IO
thecodeforge.io
Docker Image Security Scanning

Handling False Positives and Ignored CVEs

No scanner is perfect. You'll get false positives — a CVE in a library you don't use, or a vulnerability that's only exploitable with a specific configuration you don't have. Ignoring them is fine, but do it right.

Create a .trivyignore file in your repo. Each line is a CVE ID followed by a comment. The comment must include a ticket number and an expiration date. This forces periodic review. If the CVE is still unfixed after 90 days, your team must re-evaluate.

Example: CVE-2024-1234 # OPS-5678: Accepted risk until 2024-06-01. Library not loaded at runtime.

Never ignore a CVE without a ticket. That's how vulnerabilities become 'technical debt' that never gets paid.

.trivyignoreTEXT
1
2
3
4
5
6
7
8
// io.thecodeforge — DevOps tutorial

# Accepted risks - review quarterly
CVE-2024-1234 # OPS-5678: Accepted until 2024-06-01. Library not loaded at runtime.
CVE-2023-4567 # OPS-5679: Accepted until 2024-09-01. No known exploit in the wild.

# False positive - not applicable to our architecture
CVE-2022-7890 # OPS-5680: False positive. Only affects Windows builds.
⚠ Never Do This: Ignoring Without a Ticket
I've seen teams with 50+ ignored CVEs and zero documentation. When an auditor asks 'why is this CVE accepted?', nobody knows. Always link to a ticket. Always set an expiration. Otherwise, you're accumulating risk silently.

Scanning During Build: Multi-Stage and Distroless Images

Multi-stage builds are great for reducing image size, but they complicate scanning. You need to scan the final stage, not the builder stage. The builder stage might have compilers and dev tools with CVEs, but they're not in the final image.

Distroless images (e.g., gcr.io/distroless/nodejs) are minimal — no shell, no package manager. This reduces the attack surface dramatically. But they also make scanning harder because there's no package database to query. Trivy handles this by scanning the binary layers directly.

Best practice: scan both the builder stage (for awareness) and the final stage (for the gate). Fail only on the final stage.

Dockerfile.multistageDOCKERFILE
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// io.thecodeforge — DevOps tutorial

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM gcr.io/distroless/nodejs18-debian11
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
CMD ["dist/server.js"]

# Scan the final stage:
# $ trivy image myapp:latest
# Note: distroless images have no package manager, but Trivy scans binaries.
💡Senior Shortcut: Scan Builder Stage Too
Scan the builder stage in CI to catch vulnerabilities in dev dependencies. Even though they're not in the final image, they could be exploited if someone gains access to your build environment. Use --severity CRITICAL for builder stage, fail only on final.

Secrets Scanning: The CVE Nobody Talks About

Vulnerabilities are one thing, but hardcoded secrets in your image are a direct line to your infrastructure. Docker images are often shared via registries — if you accidentally include an AWS key or a database password, anyone with pull access can use it.

Trivy has a built-in secrets scanner (--scanners secret). It checks for patterns like AWS keys, GitHub tokens, private keys, and more. Run it as part of your scan. The output is clear: file path, line number, and the type of secret.

Never build an image with secrets. Use Docker build secrets (--secret) or multi-stage builds to avoid copying sensitive files into the final image.

scan-secrets.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// io.thecodeforge — DevOps tutorial

# Scan for secrets in the image
trivy image --scanners secret myapp:latest

# Example output:
# 2024-01-15T10:00:00Z    INFO    Secret scanning is enabled
# Total: 1 secret
# ┌─────────────────────┬──────────────────┬──────────┐
# │ FileSecret TypeSeverity │
# ├─────────────────────┼──────────────────┼──────────┤
# │ /app/.env           │ AWS Access KeyCRITICAL │
# └─────────────────────┴──────────────────┴──────────┘

# Fix: remove the .env file from the image. Use Docker secrets or env vars at runtime.
Output
See inline comment above.
⚠ Production Trap: .env in the Image

Scanning in Air-Gapped Environments

If your production environment has no internet access (air-gapped), you can't download vulnerability databases on the fly. You need to pre-download the DB and make it available to the scanner.

Trivy supports offline mode. Download the DB on a connected machine, then transfer it to the air-gapped environment. Use --cache-dir to point to the local DB. Update the DB periodically (e.g., weekly) and re-scan your images.

Grype has a similar approach with its grype db update command. Both tools allow you to specify a local DB path.

airgap-scan.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
// io.thecodeforge — DevOps tutorial

# On connected machine: download Trivy DB
trivy image --download-db-only --cache-db /tmp/trivy-db

# Copy /tmp/trivy-db to air-gapped environment (e.g., via USB)

# On air-gapped machine: scan using local DB
trivy image --cache-dir /path/to/trivy-db --skip-db-update myapp:latest

# For Grype:
# Download DB: grype db update -o /tmp/grype-db
# Scan: grype myapp:latest --db /tmp/grype-db
🔥Interview Gold: Air-Gapped Scanning
In air-gapped environments, you must plan for DB updates. A common mistake is using a stale DB (months old) and missing recent CVEs. Automate the DB download on a connected machine and transfer it regularly. Set up a cron job to re-scan images after each DB update.

When Not to Use Image Scanning (And What to Do Instead)

Image scanning is not a silver bullet. It won't catch zero-day vulnerabilities (no known CVE yet), logic flaws in your application, or misconfigurations in your runtime environment. It also won't help if your base image is completely custom and not in any CVE database.

For zero-days, you need runtime security monitoring (e.g., Falco, Sysdig). For application logic, you need SAST/DAST tools. For misconfigurations, use Kubernetes security policies (OPA, Kyverno).

Also, scanning every image in a large registry daily can be expensive. Prioritize: scan production images weekly, development images on every build. Use differential scanning — only scan layers that changed since the last scan.

🔥When Simpler Is Better
If you're a small team with 5 microservices, don't over-engineer. Use Trivy in CI with default settings. The complexity of Grype's SBOM and Docker Scout's SaaS model isn't worth it. Start simple, add sophistication only when you need it.

Cosign Attestation Integration — Attach Scan Results as Signed Attestations

Cosign, part of the Sigstore project, supports attaching arbitrary attestations to container images as OCI artifacts. You can attach vulnerability scan reports, SBOMs, test results, or SLSA provenance statements as signed attestations. The attestation is stored alongside the image in the registry and can be verified at admission time.

To integrate scan results with Cosign: run your vulnerability scanner (e.g., trivy image --format cosign-vuln), pipe the output to cosign attest, and attach to the image. The attestation includes the scan results in a standard in-toto predicate format. At admission time, Kyverno or a policy engine verifies both the image signature and the attestation — ensuring the image was scanned and passed policy before deployment.

SLSA (Supply-chain Levels for Software Artifacts) provenance attestations track where and how an image was built. A SLSA provenance attestation includes: build platform, build command, source repository, and builder identity. When combined with vulnerability scan attestations, you get a complete security pedigree for every image: who built it, how it was built, and what vulnerabilities it had at build time.

Cosign attestations support keyless signing via Fulcio and OIDC (OpenID Connect). In CI/CD, the build system (GitHub Actions, GitLab CI) obtains a short-lived certificate from Fulcio, signs the attestation, and attaches it to the image. No long-lived keys to manage or rotate. The attestation's validity can be verified against the OIDC token — proving it was created by your CI pipeline.

cosign-attestation.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
# Attach scan results as Cosign attestation

# ── 1. Scan image with Trivy, output in Cosign format ──────────
trivy image --format cosign-vuln --output vuln.json myapp:latest

# ── 2. Attach the vulnerability report as an attestation ───────
cosign attest --predicate vuln.json \
  --key cosign.key \
  myapp:latest

# ── 3. Verify the attestation ──────────────────────────────────
cosign verify-attestation --key cosign.pub myapp:latest

# ── 4. Verify with policy (filter for vulnerability assertions) ─
cosign verify-attestation --key cosign.pub \
  --type vuln myapp:latest

# ── Keyless (no key management) ────────────────────────────────
# In CI/CD (GitHub Actions):
cosign attest --predicate vuln.json \
  myapp:latest
# Uses OIDC token from GitHub — no key file needed

# ── SLSA provenance ───────────────────────────────────────────
cosign attest --predicate slsa-provenance.json \
  --type slsa.dev/provenance/v1 \
  myapp:latest
💡Senior Shortcut: Keyless Signing in CI
Keyless signing eliminates the biggest operational burden of image signing: key management. GitHub Actions and GitLab CI both support OIDC tokens. Use cosign attest without --key in CI. The Fulcio certificate automatically expires after the build. No key rotation, no secret storage, no key revocation list.
📊 Production Insight
Attach vulnerability attestations at build time, not post-push. If you scan after pushing to the registry, there is a window where the unsigned, unscanned image is available for pull. Build-time attestation guarantees that every image in the registry has a corresponding, verified scan result. Use cosign verify-attestation as a Kyverno admission check to block images without valid scan attestations.
🎯 Key Takeaway
Cosign attestations attach signed vulnerability reports, SBOMs, and SLSA provenance directly to OCI images. Keyless signing via Fulcio eliminates key management. Verify attestations at admission to block images without valid scan reports.

Kyverno Admission Controller for Image Security — Block Vulnerable Images at Deploy Time

Kyverno's ImageValidatingPolicy can enforce image security policies at Kubernetes admission time. Beyond signature verification, you can block pods that use images with known critical vulnerabilities, enforce that images come only from approved registries, or require that all images have a valid vulnerability scan attestation.

The key building blocks
  • CEL expressions: evaluate conditions like image.startsWith("trusted-registry.io/") or container name patterns.
  • OCI attestation verification: Kyverno can fetch and verify Cosign attestations attached to the image. Check that the attestation contains a vulnerability report with no critical CVEs, or that a SLSA provenance attestation exists.
  • Approved registries: enforce that all container images come from your private registry (e.g., registry.example.com/) or a set of approved public registries (docker.io/library/, gcr.io/distroless/*).
  • Tag-to-digest mutation: use mutateDigest: true to automatically resolve mutable tags (latest, v1) to immutable digests. This prevents tag-mutation attacks where an attacker replaces the image a tag points to.

Combine with Cosign attestations for maximum coverage: policy requires (1) the image is from an approved registry, (2) the image has a valid Cosign signature, (3) the image has a vulnerability scan attestation with no critical CVEs, (4) the image has a SLSA provenance attestation showing it was built in your CI pipeline.

Deploy in Audit mode first to discover which workloads would be blocked. Kyverno reports policy violations in the resource's status and in Kyverno logs. After reviewing, switch to Enforce mode.

kyverno-image-security.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# io.thecodeforge — devops tutorial

apiVersion: kyverno.io/v2beta1
kind: ImageValidatingPolicy
metadata:
  name: require-vuln-attestation
spec:
  rules:
    - name: verify-no-critical-cves
      imageReferences:
        - "*"
      verification:
        attestors:
          - entries:
              - keys:
                  publicKeys: |-
                    -----BEGIN PUBLIC KEY-----
                    MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                    -----END PUBLIC KEY-----
        attestations:
          - predicateType: https://cosign.sigstore.dev/attestation/vuln/v1
            conditions:
              - expression: "predicate.scanner.result < 1"
      failureAction: Enforce

---
apiVersion: kyverno.io/v2beta1
kind: ClusterPolicy
metadata:
  name: require-trusted-registry
spec:
  rules:
    - name: check-registry
      match:
        any:
          - resources:
              kinds:
                - Pod
      celPreconditions:
        - expression: "!image.startsWith('registry.example.com/')"
      validate:
        cel:
          expressions:
            - expression: "false"
              message: "Images must come from registry.example.com"
⚠ Production Trap: Admission Controller Performance
Each ImageValidatingPolicy requires Kyverno to pull the image's attestation from the registry during admission. In large deployments (100+ pods at once), this can slow down admission. Use a registry cache (e.g., Harbor proxy cache) to avoid registry rate limits. Consider using imageReview for performance-sensitive clusters.
📊 Production Insight
The most common gap is enforcing image security on init containers and sidecars (Envoy, Istio, Linkerd). These images are pulled from external registries and rarely scanned by the team's CI pipeline. Add explicit exemptions or create a separate policy with lower severity for sidecar images.
🎯 Key Takeaway
Kyverno admission policies enforce image security at deploy time: approved registries, signature verification, attestation checks (vulnerability reports, SLSA provenance), and tag-to-digest mutation. Start in Audit mode.

Notary v2 (Notation) — OCI-Compliant Image Signing Alternative to Cosign

Notation is the reference implementation of Notary v2, an OCI-compliant image signing standard. Unlike Cosign (which stores signatures as OCI artifacts in a separate repository), Notation embeds signatures directly into the OCI manifest using the OCI 1.1 referrers API. This means the signature travels with the image — no need to manage separate signature repositories or worry about signature GC.

Key differences from Cosign
  • Signature storage: Cosign stores signatures in a separate OCI artifact (.sig tag). Notation embeds signatures in the OCI manifest via referrers API.
  • Key management: Notation uses X.509 certificates managed via a trust store and trust policy. Cosign uses Cosign-generated key pairs or Fulcio short-lived certificates.
  • Compliance: Notation's X.509-based model aligns with enterprise PKI infrastructure. Cosign's keyless model (Fulcio/OIDC) aligns with cloud-native CI/CD.
  • Tooling: Notation is part of the OCI spec effort (CNCF). Cosign is a Sigstore tool (Linux Foundation).

Setting up Notation: generate a self-signed certificate or use an existing enterprise CA. Add it to a trust store (notation cert add). Sign images with notation sign --key . Verify with notation verify .

Notation integration with Kyverno: Kyverno's ImageValidatingPolicy supports Notation attestors natively. You specify trust stores and trust policies instead of Cosign public keys. This is useful for organizations that already operate an X.509 PKI.

Both Notation and Cosign achieve the same goal: proving image integrity and origin. The choice depends on your key management infrastructure. If you have an existing PKI, Notation is the natural fit. If you prefer OIDC-based keyless signing, use Cosign.

notation-setup.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/bin/bash
# Notary v2 (Notation) setup and signing

# ── 1. Install Notation ────────────────────────────────────────
# macOS: brew install notation
# Linux: curl -LO https://github.com/notaryproject/notation/releases/...

# ── 2. Create a certificate (self-signed, or use enterprise CA) ─
notation cert generate-test --default "registry.example.com"

# ── 3. Sign the image ──────────────────────────────────────────
notation sign --key "registry.example.com" registry.example.com/myapp:latest

# ── 4. Verify the signature ────────────────────────────────────
notation verify registry.example.com/myapp:latest
# Output: Successfully verified

# ── 5. List signatures ────────────────────────────────────────
notation ls registry.example.com/myapp:latest

# ── 6. Kyverno policy integration ──────────────────────────────
# In ImageValidatingPolicy, use:
# verification:
#   attestors:
#     - entries:
#         - notations:
#             certs: "notation-trust-store"
#             trustPolicy: "notation-trust-policy"
🔥Notation vs Cosign: When to Choose Which
Choose Notation if: you have an existing X.509 PKI, need compliance with enterprise certificate policies, or prefer OCI-native signature storage. Choose Cosign if: you want keyless signing via OIDC, prefer the Sigstore ecosystem, or need built-in attestation support for vulnerability reports and SLSA provenance.
📊 Production Insight
Notation's signature GC problem is solved: because signatures are stored as OCI referrers, they are garbage-collected when the image is deleted. Cosign signatures (stored as separate .sig tags) can orphan when the image is deleted. This matters in registries with automated cleanup policies.
🎯 Key Takeaway
Notation is the OCI-compliant image signing standard using X.509 certificates. Signatures are embedded in the OCI manifest via referrers — no separate artifact management. Best suited for enterprise PKI environments. Integrates with Kyverno admission policies.

Registry-Level Scanning with Harbor — Block Vulnerable Pushes at the Registry

Harbor is an open-source cloud-native registry that integrates vulnerability scanning directly into the registry workflow. When an image is pushed to Harbor, it automatically triggers a scan (using Trivy, Grype, or Aqua CSP). Harbor evaluates the scan results against policy: images with critical vulnerabilities can be blocked from being pulled, or the push itself can be rejected.

Harbor's security model operates at three enforcement points
  • Push prevention: block images from being pushed if they contain critical or high CVEs.
  • Pull prevention: allow push but block pull of images with vulnerabilities above a threshold.
  • Quarantine: push images to a quarantine repository for manual review.

Harbor integrates with Trivy natively (built-in scanner, no configuration needed). It also supports custom scanners via the Scanner Adapter interface (Snyk, Aqua, Anchore). The scan results are displayed in the Harbor UI and exposed via API for integration with CI/CD pipelines.

Setup: deploy Harbor, enable the Trivy scanner, and create a vulnerability policy. The policy specifies the severity threshold (e.g., block if any CRITICAL), whether to scan on push, and the action (block push, block pull, or quarantine). Harbor also supports image replication with scan results — only replicate images that pass policy to your production registry.

Comparison with CI/CD scanning: CI/CD scanning blocks vulnerable images before they reach the registry. Registry-level scanning catches images that bypass CI (e.g., manual pushes, images from external pipelines, or base image drift after rebuilding). Both are necessary for defense in depth.

harbor-policy.yamlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# io.thecodeforge — devops tutorial

# Harbor vulnerability policy (configured in UI or via API)
# This blocks pull of images with critical or high vulnerabilities

apiVersion: goharbor.io/v1alpha1
kind: HarborProject
metadata:
  name: production
spec:
  vulnerabilityScanPolicy:
    analyzer: trivy
    parameters:
      severity: critical,high
      fixable: true
  enforcement:
    pull:
      enabled: true
      severity: critical,high
    push:
      enabled: false  # Allow push, but block pull
  scanOnPush: true

# API equivalent:
# curl -X PUT "https://harbor.example.com/api/v2.0/projects/production" \
#   -H "Content-Type: application/json" \
#   -d '{"vulnerability_scan_policy": {"analyzer": "trivy", "parameters": {"severity": "critical,high"}}}'
⚠ Production Trap: Scan-on-Push Latency
Scanning on every push adds 30-60 seconds to the push time. For development projects with frequent pushes, use a scheduled scan (every 6 hours) instead of scan-on-push. Production projects should always scan on push. Configure this per-project in Harbor.
📊 Production Insight
The most effective Harbor configuration: block pull (not push) and let the CI/CD pipeline handle push-time rejection. This prevents developers from being blocked during local development (they can push to dev projects) while ensuring production workloads never pull vulnerable images. Use replication rules to only replicate images that pass policy to the production registry.
🎯 Key Takeaway
Harbor provides registry-level vulnerability scanning with push/pull enforcement. Block vulnerable images at the registry — catches what CI/CD scanning misses. Use Trivy as the built-in scanner. Block pull (not push) for developer productivity.

Choosing the right SBOM format depends on your use case: security operations, vulnerability management, or legal/license compliance. Both CycloneDX and SPDX are ISO/IEC standards, but they differ in scope and data model.

CycloneDX (OWASP) — ISO/IEC 19770-2:2024. Designed for application security and vulnerability management. Key features: - Component metadata: supplier, version, hashes, licenses - Dependency graph: explicit relationships between components - Vulnerability references: links to CVE, GHSA, and other advisory databases - Services and external references - Native support in Trivy, Syft, and Docker Scout - Best for: CI/CD security gates, vulnerability correlation, operations

SPDX (Linux Foundation) — ISO/IEC 5962:2021. Originally designed for license compliance. Key features: - File-level granularity (can list every file in a package) - Detailed license information (per-file and per-package) - Cross-reference document relationships - More verbose (3-5x larger than CycloneDX) - Best for: legal audits, M&A diligence, license compliance

NTIA Minimum Elements (EO 14028): Both formats can satisfy the six NTIA minimum elements: (1) Supplier name, (2) Component name, (3) Version, (4) Dependency relationships, (5) Author, (6) Timestamp. CycloneDX maps these naturally via supplier, name, version, dependencies, author, and timestamp fields. SPDX requires more fields but covers the same elements.

Recommendation: Generate CycloneDX for security workflows (vulnerability scanning, incident response) and SPDX for legal/compliance workflows. Most SBOM tools (Syft, Trivy) can generate both from a single scan. Store both in the registry as OCI attestations. For EO 14028 compliance, CycloneDX is the more practical choice — smaller, security-focused, and directly integrated with vulnerability databases.

sbom-comparison.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
# Generate both CycloneDX and SPDX from the same image

# ── CycloneDX (JSON) — for security workflows ──────────────────
syft myapp:latest -o cyclonedx-json > sbom.cdx.json

echo "CycloneDX size: $(wc -c < sbom.cdx.json) bytes"
echo "Components: $(jq '.components | length' sbom.cdx.json)"

# ── SPDX (JSON) — for legal compliance ────────────────────────
syft myapp:latest -o spdx-json > sbom.spdx.json

echo "SPDX size: $(wc -c < sbom.spdx.json) bytes"
echo "Files: $(jq '.files | length' sbom.spdx.json)"

# ── Compare NTIA minimum elements ─────────────────────────────
# CycloneDX:
jq '.metadata.supplier.name' sbom.cdx.json  # Supplier name
jq '.metadata.timestamp' sbom.cdx.json        # Timestamp

# SPDX:
jq '.creationInfo.creators' sbom.spdx.json   # Author
jq '.creationInfo.created' sbom.spdx.json     # Timestamp
🔥EO 14028 and NTIA Compliance Checklist
To meet US Executive Order 14028 SBOM requirements: (1) Generate SBOMs in CycloneDX or SPDX format. (2) Include all six NTIA minimum elements. (3) Automate SBOM generation in CI/CD — no manual steps. (4) Attach SBOMs to images as OCI attestations. (5) Make SBOMs available to customers and auditors on request. (6) Update SBOMs when images are rebuilt — even with same version tag.
📊 Production Insight
Most organizations over-engineer SBOM strategy. Start with CycloneDX only — it covers security and vulnerability management. Add SPDX only when legal/compliance explicitly requires it. The incremental benefit of SPDX over CycloneDX for operational security is minimal, and the 3-5x file size adds unnecessary storage and transfer overhead.
🎯 Key Takeaway
CycloneDX for security operations, SPDX for legal compliance. Both satisfy EO 14028 NTIA minimum elements. CycloneDX is smaller, vulnerability-aware, and the recommended default. Generate both from the same build — Syft supports both formats from a single scan.
● Production incidentPOST-MORTEMseverity: high

The 4GB Container That Kept Dying

Symptom
A microservice crashed every 6 hours with OOMKilled. Memory limit was 4GB, but RSS stayed under 2GB.
Assumption
Team assumed a memory leak in the Node.js app. Spent days profiling heap dumps.
Root cause
The base image node:14-slim contained a vulnerable version of glibc (CVE-2021-33574) that caused a memory corruption in DNS resolution under high concurrency. The vulnerability was known for 6 months, but the image was never rescanned.
Fix
Switched base image to node:14-slim with updated glibc (or moved to node:16-slim). Added weekly scheduled scans with trivy image --severity HIGH,CRITICAL and a CI gate that fails if any critical CVE is older than 30 days.
Key lesson
  • A vulnerability in your base image's OS packages can manifest as a hard-to-diagnose runtime crash.
  • Scan early, scan often, and pin base image digests, not tags.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Scan fails with 'FATAL: unable to get image' or 'manifest unknown'
Fix
1. Verify image exists: docker pull myapp:latest. 2. Check registry credentials: docker login. 3. Ensure image tag is correct. 4. If using private registry, pass --registry.username and --registry.password to Trivy.
Symptom · 02
Scan reports 0 vulnerabilities on an image you know has CVEs
Fix
1. Check if the scanner's DB is outdated: trivy image --update-db. 2. Verify the image has OS packages: docker run --rm myapp:latest cat /etc/os-release. 3. Try a different scanner (Grype) to cross-validate.
Symptom · 03
CI scan takes >10 minutes, slowing down pipeline
Fix
1. Cache Trivy DB: set TRIVY_CACHE_DIR and persist between runs. 2. Use --skip-db-update if DB is fresh. 3. Limit severity: --severity CRITICAL,HIGH. 4. Consider incremental scanning with --pkg-types os.
★ Docker Image Security Scanning Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
`FATAL: unable to get image`
Immediate action
Check if image exists and registry is accessible
Commands
docker pull myapp:latest
docker login
Fix now
Use trivy image --registry.username $USER --registry.password $PASS myapp:latest
`no vulnerabilities found` on known vulnerable image+
Immediate action
Update vulnerability database
Commands
trivy image --update-db
trivy image --severity CRITICAL myapp:latest
Fix now
If still no results, try Grype: grype myapp:latest
Scan takes >10 min in CI+
Immediate action
Cache the DB and skip update
Commands
export TRIVY_CACHE_DIR=/tmp/trivy-cache
trivy image --cache-dir $TRIVY_CACHE_DIR --skip-db-update myapp:latest
Fix now
Add a cron job to update DB daily, then use --skip-db-update in CI
`CVE-2024-XXXX` is a false positive+
Immediate action
Add to `.trivyignore` with ticket and expiration
Commands
echo 'CVE-2024-XXXX # OPS-1234: Accepted until 2024-12-01. Not exploitable.' >> .trivyignore
trivy image --ignorefile .trivyignore myapp:latest
Fix now
Ensure the ignore file is committed and reviewed quarterly
FeatureTrivyGrypeDocker Scout
Speed (1GB image)<30s~60s~45s
Offline supportYes (download DB)Yes (download DB)No (SaaS)
Secrets scanningYesNo (use Syft)No
IaC scanningYesNoNo
SBOM generationYes (via CycloneDX)Yes (native)Yes
CI/CD integrationGitHub, GitLab, JenkinsGitHub, GitLab, JenkinsGitHub, Docker Hub
CostFree, open sourceFree, open sourceFree tier, paid for advanced
⚙ Quick Reference
12 commands from this guide
FileCommand / CodePurpose
DockerfileFROM node:18@sha256:abc123def456...Why You Can't Trust Your Base Image
scan-comparison.shbrew install trivyChoosing the Right Scanner
.githubworkflowsdocker-scan.ymlname: Docker Security ScanIntegrating Scanning into CI/CD
.trivyignoreCVE-2024-1234 # OPS-5678: Accepted until 2024-06-01. Library not loaded at runti...Handling False Positives and Ignored CVEs
Dockerfile.multistageFROM node:18-alpine AS builderScanning During Build
scan-secrets.shtrivy image --scanners secret myapp:latestSecrets Scanning
airgap-scan.shtrivy image --download-db-only --cache-db /tmp/trivy-dbScanning in Air-Gapped Environments
cosign-attestation.shtrivy image --format cosign-vuln --output vuln.json myapp:latestCosign Attestation Integration
kyverno-image-security.yamlapiVersion: kyverno.io/v2beta1Kyverno Admission Controller for Image Security
notation-setup.shnotation cert generate-test --default "registry.example.com"Notary v2 (Notation)
harbor-policy.yamlapiVersion: goharbor.io/v1alpha1Registry-Level Scanning with Harbor
sbom-comparison.shsyft myapp:latest -o cyclonedx-json > sbom.cdx.jsonSBOM Format Comparison

Key takeaways

1
Pin base images by digest, not tag, to ensure reproducible builds and a known vulnerability baseline.
2
Use --ignore-unfixed in CI to avoid failing on CVEs without patches; track unfixed CVEs separately with expiration dates.
3
Always scan for secrets in addition to CVEs
a hardcoded AWS key is worse than any CVE.
4
In air-gapped environments, pre-download the vulnerability DB and update it regularly; schedule re-scans after each DB update.
5
Use Cosign attestations to attach signed vulnerability reports and SBOMs to images. Verify at admission with Kyverno. Keyless signing via Fulcio eliminates key management overhead.
6
Layer CI/CD scanning (Trivy in pipeline), registry scanning (Harbor on push), and admission scanning (Kyverno at deploy) for defense-in-depth. Registry scanning catches images that bypass CI.
7
Generate CycloneDX SBOMs for security operations and SPDX for legal compliance. Both satisfy EO 14028 NTIA minimum elements. Automate generation in CI/CD
no manual steps.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Trivy handle scanning a distroless image that has no package ma...
Q02SENIOR
When would you choose Grype over Trivy in a production environment?
Q03SENIOR
What happens if you run `trivy image` without `--ignore-unfixed` and a C...
Q04JUNIOR
What is the difference between a CVE and a vulnerability in the context ...
Q05SENIOR
You scan an image and find a critical CVE in a library that is only used...
Q06SENIOR
How would you design a scanning pipeline for a registry with 10,000 imag...
Q07SENIOR
Design an image supply chain security pipeline that covers build, regist...
Q08SENIOR
Compare Cosign and Notation for image signing. When would you choose one...
Q01 of 08SENIOR

How does Trivy handle scanning a distroless image that has no package manager?

ANSWER
Trivy scans the binary layers directly by parsing the filesystem and identifying known binaries (e.g., libssl.so). It matches these against its vulnerability database using hashes or version strings embedded in the binaries. No package database is needed.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
How do I scan a Docker image for vulnerabilities?
02
What's the difference between Trivy and Docker Scout?
03
How do I ignore a false positive vulnerability in Trivy?
04
Can I scan a Docker image without pulling it first?
05
How does Cosign attestation differ from simple image signing? Do I need both?
06
What is the difference between CI/CD scanning and registry-level scanning with Harbor? Do I need both?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 18, 2026
last updated
2,466
articles · all by Naren
🔥

That's Docker. Mark it forged?

9 min read · try the examples if you haven't

Previous
Docker Healthchecks and Restart Policies
26 / 43 · Docker
Next
Docker Monitoring and Logging