Suggest an editImprove this articleRefine the answer for “How to scan Docker images for vulnerabilities?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Use Trivy, Grype, or Docker Scout** in CI to scan images for known CVEs. Fail the build on HIGH/CRITICAL severity. Run periodic re-scans because new CVEs are disclosed against the same image bytes daily. ```bash trivy image --severity HIGH,CRITICAL --exit-code 1 myorg/api:1.0 grype myorg/api:1.0 docker scout cves myorg/api:1.0 ``` **Key:** scan at build time (gate the pipeline), at admission (gate deploy), and continuously (catch new CVEs in already-deployed images). Distinguish OS-level CVEs (fix by base-image bump) from app-level CVEs (fix by dependency update).Shown above the full answer for quick recall.Answer (EN)Image**Vulnerability scanning** answers "do my images contain known security holes?" Modern tooling makes this a near-zero-effort step in CI. Skipping it means shipping known CVEs to production. ## Theory ### TL;DR - A scanner inspects an image's installed packages (OS + language) and matches them against CVE databases (NVD, vendor advisories, GitHub Security Advisories). - **Three popular tools:** Trivy (Aqua), Grype (Anchore), Docker Scout (Docker Inc). - Severities: LOW, MEDIUM, HIGH, CRITICAL. Most teams gate on HIGH+ or CRITICAL. - **Three scanning moments:** build-time (CI gate), admission-time (cluster gate), continuously (catch new CVEs in already-running images). - **Two CVE categories:** OS-level (fix by base-image update) and app-level (fix by package update). - Always pair with a **`.trivyignore`** or equivalent for documented exceptions. ### Trivy (most popular) ```bash # Scan a local image trivy image myorg/api:1.0 # Fail on HIGH or CRITICAL trivy image --severity HIGH,CRITICAL --exit-code 1 myorg/api:1.0 # Ignore unfixed CVEs (no patch available — wait for upstream) trivy image --ignore-unfixed myorg/api:1.0 # Output formats trivy image --format json myorg/api:1.0 trivy image --format sarif --output report.sarif myorg/api:1.0 ``` Trivy is opinionated and fast. Built-in CI flags. Default severity is everything; production CI usually filters to HIGH+. ### Grype ```bash grype myorg/api:1.0 grype --fail-on high myorg/api:1.0 ``` From Anchore. Slightly different DB; sometimes catches what Trivy misses (and vice versa). Good for second-opinion scans. ### Docker Scout ```bash docker scout cves myorg/api:1.0 docker scout recommendations myorg/api:1.0 # suggests base-image upgrades to fix CVEs ``` Docker Inc's scanner. Tightly integrated with Docker CLI and Docker Hub. The `recommendations` command is unique and useful: it tells you exactly which base-image bump fixes which CVEs. ### CI integration ```yaml # GitHub Actions with Trivy - name: Build image run: docker build -t myorg/api:${{ github.sha }} . - name: Scan with Trivy uses: aquasecurity/trivy-action@master with: image-ref: myorg/api:${{ github.sha }} severity: 'HIGH,CRITICAL' exit-code: '1' ignore-unfixed: true - name: Upload SARIF to GitHub if: always() uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' ``` Uploads to GitHub's Code Scanning tab — vulnerabilities show up alongside the rest of your code-quality signals. ### Multi-stage scan during build ```dockerfile # syntax=docker/dockerfile:1.7 FROM golang:1.23-alpine AS build # ... build app ... FROM aquasec/trivy:0.59.0 AS scan COPY --from=build / /scan-target RUN trivy filesystem --severity HIGH,CRITICAL --exit-code 1 /scan-target FROM alpine:3.21 COPY --from=build /out/server /server ENTRYPOINT ["/server"] ``` The `scan` stage runs as part of the build. Build fails if HIGH/CRITICAL CVEs are found. Cannot be skipped by careless CI configs. ### Admission-time scanning Kubernetes admission controllers can refuse to deploy unscanned or vulnerable images: - **OPA Gatekeeper / Kyverno** with policies that check Cosign attestations of scan results. - **Trivy Operator** for K8s — continuously scans running images. - **Cosign + sigstore-policy-controller** — verify the image was signed *and* attested as scanned. The pipeline: CI signs an attestation ("this image was scanned at time X, found N HIGH CVEs"); admission verifies the attestation and checks the policy. ### Continuous re-scanning A CVE disclosed today affects the image you built last week. The bytes are the same; the threat landscape has changed. ```bash # Cron-driven re-scan in production 0 6 * * * trivy image --severity HIGH,CRITICAL myorg/api:current 2>&1 | mail security@example.com ``` Or use a continuous scanner (Trivy Operator, Snyk Container, Sysdig Secure) that watches running images and re-scans periodically. ### OS vs application CVEs Results usually fall into two buckets: **OS-level** (`alpine:3.20` → CVE in `libssl`): - Fix: bump base image (`alpine:3.21`), rebuild. - Or wait for the OS vendor to publish a patched version. **App-level** (`requests==2.30.0` → CVE in requests): - Fix: update the dependency in your `requirements.txt` / `package.json` / `go.mod`. - Often takes longer if the new version has breaking changes. Scanners detect both. The remediation path is different. ### Common mistakes **Scanning but not gating** ```yaml - run: trivy image myorg/api:${{ github.sha }} || true # ← always passes ``` Results shown but do not fail the build. Devs ignore. Within months you have hundreds of HIGH CVEs in production. **Always set `--exit-code 1` on a real severity threshold.** **Scanning only at build time** A HIGH CVE disclosed last night is sitting in your prod image right now. Your CI passed yesterday; reality changed overnight. Add nightly re-scans of running images. **Not maintaining `.trivyignore`** Real-world CVEs sometimes have no patch ("won't fix"). You either tolerate them or replace the package. Document the decision in `.trivyignore`: ``` # .trivyignore CVE-2023-12345 # No patch available, mitigated by network policy. Re-evaluate quarterly. ``` **Trusting one scanner** Different scanners have different DBs. A CVE in your image database might be missing from one scanner. For high-stakes work, run two scanners and union the results. **Ignoring the SBOM** A scanner result without an SBOM is just "these CVEs exist". With an SBOM (Software Bill of Materials), you have a precise list of what is in the image, queryable later: "which images contain log4j?". Generate SBOM during build (`docker buildx build --sbom=true`), store with the image. ### Real-world setup #### Minimal - Trivy in CI, gate on HIGH/CRITICAL. - `.trivyignore` for documented exceptions. - Done. #### Mature - Trivy in CI + scan stage in Dockerfile. - SBOM generation + storage. - Cosign attestation of scan results. - Admission controller verifies attestation. - Nightly re-scan of running images. - Trivy Operator on K8s. - Findings → Slack channel + ticket creation. ### Follow-up questions **Q:** What is the difference between a CVE and a vulnerability? **A:** A CVE (Common Vulnerabilities and Exposures) is a public ID for a known security issue. A vulnerability is the underlying bug. Most modern scanners report by CVE ID, with severity scored via CVSS. **Q:** Can scanners find zero-days? **A:** No — they only know about *publicly disclosed* vulnerabilities. Zero-days, by definition, are not yet known. Scanning is one layer; complement with runtime monitoring, sandbox testing, and code review. **Q:** What is an SBOM and why generate one? **A:** Software Bill of Materials — a list of every package and version in your image. SPDX or CycloneDX format. With an SBOM, you can later answer "which of my images contain $vulnerable_package" without re-pulling and re-scanning. **Q:** How do I handle CVEs with no fix available? **A:** Document in `.trivyignore` with rationale and re-evaluation date. Mitigate via runtime controls (network policy, capability drops). Re-check quarterly. **Q:** (Senior) How would you build a scanning pipeline that prevents regressions? **A:** Three gates: (1) CI build fails on new HIGH/CRITICAL — even fixing-an-unrelated-bug PR cannot merge if it introduces a new CVE. (2) Admission control rejects images without a fresh signed scan attestation. (3) Production runtime scanner alerts on new CVEs against running images and triggers a remediation workflow (auto-PR to bump the base image, ticket to dev team). The result: any CVE that reaches prod was either ignored deliberately (in `.trivyignore` with sign-off) or disclosed after deployment with a tracked response window. ## Examples ### Trivy in GitHub Actions ```yaml name: ci on: [push] jobs: build-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: docker build -t myorg/api:${{ github.sha }} . - name: Trivy scan (gate) uses: aquasecurity/trivy-action@master with: image-ref: myorg/api:${{ github.sha }} severity: 'HIGH,CRITICAL' ignore-unfixed: true exit-code: '1' - name: Trivy scan (full report, never fails) if: always() uses: aquasecurity/trivy-action@master with: image-ref: myorg/api:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'LOW,MEDIUM,HIGH,CRITICAL' - name: Upload SARIF if: always() uses: github/codeql-action/upload-sarif@v3 with: sarif_file: 'trivy-results.sarif' ``` Two passes: gating on HIGH+, full report uploaded for visibility. ### Multi-stage with embedded scan ```dockerfile # syntax=docker/dockerfile:1.7 FROM golang:1.23-alpine AS build WORKDIR /src COPY . . RUN CGO_ENABLED=0 go build -o /out/server ./cmd/server FROM aquasec/trivy:0.59.0 AS scan COPY --from=build /out /scan RUN trivy filesystem --severity HIGH,CRITICAL --exit-code 1 --ignore-unfixed /scan FROM scratch COPY --from=build /out/server /server USER 65532:65532 ENTRYPOINT ["/server"] ``` `docker build` fails if HIGH/CRITICAL CVEs found. Can be bypassed with `--target` for the runtime stage if necessary, but the default build path enforces the gate. ### Compare two scanners ```bash #!/bin/bash IMG=$1 echo "=== Trivy ===" trivy image --severity HIGH,CRITICAL $IMG --no-progress --quiet echo "=== Grype ===" grype $IMG --output table --quiet ``` Different scanners flag different things. For high-stakes images, the union is your true risk picture.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.