Ravindra BagaleCourses & study guides मराठी

Chapter 55: Docker Security Best Practices, Image Scanning with Trivy, Logging, Monitoring and Docker Swarm Basics

55.4 Scan images: Trivy and Docker Scout

Friends, every image carries hundreds of packages, and some of them have known security holes called CVEs. A scanner reads the list of packages in your image and matches it against public CVE databases. Today, step by step, we scan with Trivy and Docker Scout, read the report, fix real findings, and make the CI pipeline fail when a CRITICAL appears.

1. Start from a story

At Swiggy, a team shipped a Python service on an old python:3.8 base image that nobody had touched for a year.

During an audit, a security engineer named Neha ran one command, trivy image, on it. The report showed 9 CRITICAL and 61 HIGH vulnerabilities, mostly in OpenSSL and old system libraries the app never needed.

Neha changed the base to python:3.12-slim, rebuilt, and scanned again. CRITICAL dropped to zero and HIGH to two, both in a Python library with a fixed version available. One line in requirements.txt fixed those.

Then she added the scan to Jenkins so any new CRITICAL stops the build. Today we do exactly that.

Image layersOS packages and app librariesScanner builds a listpackage name and versionMatch with CVE databasesNVD, distro advisories, GitHub advisoriesReport by severityCRITICAL HIGH MEDIUM LOWFix and rescanupdate base or library
Picture 1: how a scanner works

2. Trivy step by step

Step 1: Run Trivy without installing

Why: Use the official container.

What: Trivy runs as a container. Mount the Docker socket so it can read local images, and a cache folder so the database downloads only once.

Example 1: Version

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 --version | head -1

You will see:

Version: 0.56.2

What it means: Works without any install.

Example 2: First scan

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q alpine:3.20 | head -3

You will see:

alpine:3.20 (alpine 3.20.3)

Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

What it means: A small, fresh base is clean.

Example 3: Cache saved

Type this:

du -sh $HOME/.cache/trivy

You will see:

72M /home/ubuntu/.cache/trivy

What it means: The CVE database, reused next time.

Step 2: Scan an old image

Why: See what age does.

What: Older tags were built with older packages, so they collect CVEs over time.

Example 4: Old Python

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q python:3.8 2>/dev/null | grep '^Total'

You will see:

Total: 1023 (UNKNOWN: 6, LOW: 571, MEDIUM: 330, HIGH: 107, CRITICAL: 9)

What it means: A full Debian base with old packages.

Example 5: Slim and new

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q python:3.12-slim | grep '^Total'

You will see:

Total: 38 (UNKNOWN: 0, LOW: 30, MEDIUM: 6, HIGH: 2, CRITICAL: 0)

What it means: Fewer packages, newer versions.

Example 6: Image sizes

Type this:

docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep -E 'python:3.(8|12-slim)'

You will see:

python:3.8 997MB
python:3.12-slim 130MB

What it means: Less software, less risk.

Step 3: Filter by severity

Why: Focus on what matters first.

What: Use --severity to show only HIGH and CRITICAL. --ignore-unfixed hides CVEs that have no fix yet.

Example 7: Only serious

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q --severity HIGH,CRITICAL python:3.12-slim | grep '^Total'

You will see:

Total: 2 (HIGH: 2, CRITICAL: 0)

What it means: Two to look at.

Example 8: Only fixable

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q --severity HIGH,CRITICAL --ignore-unfixed python:3.12-slim | grep '^Total'

You will see:

Total: 0 (HIGH: 0, CRITICAL: 0)

What it means: Nothing you can fix today in the base.

Example 9: Read one row

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q --severity HIGH python:3.12-slim | grep -m1 -E 'CVE-[0-9]+'

You will see:

│ libexpat1 │ CVE-2024-45491 │ HIGH │ affected │ 2.5.0-1 │ │ libexpat: Integer Overflow ... │

What it means: Library, CVE id, severity, status, installed and fixed version.

Step 4: Scan your own app image

Why: Libraries count too.

What: Trivy also reads requirements.txt, package-lock.json, jar files and Go binaries inside the image.

Example 10: Build an app with an old library

Type this:

printf 'flask==2.0.0\nrequests==2.25.0\n' > requirements.txt && printf '%s\n' 'FROM python:3.12-slim' 'COPY requirements.txt .' 'RUN pip install -q -r requirements.txt' > Dockerfile && docker build -q -t shop-api:1.0 . >/dev/null && echo built

You will see:

built

What it means: Deliberately old versions.

Example 11: Scan

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q --severity HIGH,CRITICAL shop-api:1.0 | grep -E 'Python|requests|flask|werkzeug' | head -4

You will see:

Python (python-pkg)
│ flask    │ CVE-2023-30861 │ HIGH │ fixed │ 2.0.0 │ 2.3.2, 2.2.5 │
│ requests │ CVE-2024-35195 │ MEDIUM...
│ werkzeug │ CVE-2023-25577 │ HIGH │ fixed │ 2.0.3 │ 2.2.3 │

What it means: App libraries with fixed versions.

Example 12: Fix and rescan

Type this:

printf 'flask==3.0.3\nrequests==2.32.3\n' > requirements.txt && docker build -q -t shop-api:1.1 . >/dev/null && docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q --severity HIGH,CRITICAL shop-api:1.1 | grep '^Total' | tail -1

You will see:

Total: 0 (HIGH: 0, CRITICAL: 0)

What it means: Clean after the update.

Step 5: Scan files and config, not just images

Why: Dockerfiles and folders too.

What: trivy fs scans a project folder for vulnerable libraries and secrets. trivy config checks a Dockerfile for bad practices.

Example 13: Folder scan

Type this:

docker run --rm -v $PWD:/src aquasec/trivy:0.56.2 fs -q --severity HIGH,CRITICAL /src | grep '^Total'

You will see:

Total: 0 (HIGH: 0, CRITICAL: 0)

What it means: Same rules, before you even build.

Example 14: Dockerfile check

Type this:

docker run --rm -v $PWD:/src aquasec/trivy:0.56.2 config -q /src | grep -E 'AVD-DS' | head -2

You will see:

AVD-DS-0002 (HIGH): Specify at least 1 USER command in Dockerfile with non-root user
AVD-DS-0026 (LOW): Add HEALTHCHECK instruction in your Dockerfile

What it means: It points out a missing USER.

Example 15: Fix it

Type this:

printf '%s\n' 'RUN useradd -r -u 10001 app' 'USER 10001' >> Dockerfile && docker run --rm -v $PWD:/src aquasec/trivy:0.56.2 config -q /src | grep -c AVD-DS-0002

You will see:

0

What it means: The HIGH finding is gone.

Step 6: Make a build fail on CRITICAL

Why: --exit-code turns a report into a gate.

What: --exit-code 1 makes Trivy return 1 when it finds anything at the chosen severity. CI treats that as a failure.

Example 16: Gate on the old image

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q --exit-code 1 --severity CRITICAL python:3.8 >/dev/null; echo exit $?

You will see:

exit 1

What it means: The pipeline would stop.

Example 17: Gate on the new image

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q --exit-code 1 --severity CRITICAL --ignore-unfixed shop-api:1.1 >/dev/null; echo exit $?

You will see:

exit 0

What it means: Safe to push.

Example 18: Jenkins stage

Type this:

printf '%s\n' "stage('Scan') {" '  steps {' '    sh "trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed shop-api:${BUILD_NUMBER}"' '  }' '}' > scan.groovy && cat scan.groovy

You will see:

stage('Scan') {
  steps {
    sh "trivy image --exit-code 1 --severity CRITICAL --ignore-unfixed shop-api:${BUILD_NUMBER}"
  }
}

What it means: Runs after build, before push.

Step 7: Accept a risk with .trivyignore

Why: Document exceptions, do not hide them.

What: Sometimes a CVE does not affect you and has no fix. Put its id in .trivyignore with a comment and a review date.

Example 19: Write the file

Type this:

printf '%s\n' '# libexpat not used by our code path; review 2026-12-01' 'CVE-2024-45491' > .trivyignore && cat .trivyignore

You will see:

# libexpat not used by our code path; review 2026-12-01
CVE-2024-45491

What it means: A reason and a date.

Example 20: Scan with it

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $PWD:/w -w /w aquasec/trivy:0.56.2 image -q --severity HIGH python:3.12-slim | grep '^Total'

You will see:

Total: 1 (HIGH: 1, CRITICAL: 0)

What it means: One fewer than before.

Example 21: Review regularly

Type this:

grep -c CVE .trivyignore

You will see:

1

What it means: Keep this list short.

Step 8: Reports for audits and SBOMs

Why: Machine-readable output.

What: Use -f json or -f table, and produce an SBOM, the full list of packages, in CycloneDX or SPDX format.

Example 22: JSON report

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q -f json shop-api:1.1 > report.json && python3 -c "import json;print(len(json.load(open('report.json'))['Results']))"

You will see:

2

What it means: Results per OS and per language.

Example 23: SBOM

Type this:

docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q -f cyclonedx shop-api:1.1 > sbom.json && grep -c '"name"' sbom.json

You will see:

142

What it means: About 142 components listed.

Example 24: Scan the SBOM later

Type this:

docker run --rm -v $PWD:/w aquasec/trivy:0.56.2 sbom -q /w/sbom.json | grep '^Total' | tail -1

You will see:

Total: 0 (UNKNOWN: 0, LOW: 4, MEDIUM: 1, HIGH: 0, CRITICAL: 0)

What it means: Re-check old releases without the image.

3. Docker Scout step by step

docker buildimage shop-api:Ntrivy image --exit-code 1stop on CRITICALdocker scout recommendationsbetter base imagedocker push to ECRonly clean imagesECR scan on pushsecond opinion
Picture 2: scanning in the pipeline

Step 9: Docker Scout quickview

Why: Built into the Docker CLI.

What: Docker Scout is in Docker Desktop and recent Docker CLI. Log in to Docker Hub first; a free account works for a few repos.

Example 25: Quick summary

Type this:

docker scout quickview python:3.8

You will see:

Target   │  python:3.8  │    9C   107H   330M   571L
  digest │  2f3a...     │
Base image │  buildpack-deps:bullseye │

What it means: Counts by severity.

Example 26: CVE list

Type this:

docker scout cves --only-severity critical python:3.8 | grep -m2 CVE

You will see:

✗ CRITICAL CVE-2023-38545
✗ CRITICAL CVE-2024-5535

What it means: The worst ones.

Example 27: Your image

Type this:

docker scout quickview shop-api:1.1 | head -2

You will see:

Target   │  shop-api:1.1  │    0C     0H     6M    30L

What it means: Matches the Trivy result.

Step 10: Scout recommendations

Why: It suggests a better base.

What: Scout knows which base images have fewer CVEs and tells you what to change.

Example 28: Ask

Type this:

docker scout recommendations python:3.8 | grep -A2 'Recommended'

You will see:

Recommended fixes
Tag 3.12-slim   │ Benefits: image is 870 MB smaller, 9 fewer critical

What it means: Points to the same fix Neha made.

Example 29: Compare two images

Type this:

docker scout compare shop-api:1.1 --to shop-api:1.0 | grep -E 'vulnerabilities' | head -1

You will see:

vulnerabilities │    0C  0H  6M  30L │    0C  4H  7M  30L

What it means: New versus old side by side.

Example 30: Policy check

Type this:

docker scout policy shop-api:1.1 2>&1 | grep -m1 -i 'critical'

You will see:

✓  No fixable critical or high vulnerabilities

What it means: Green.

Step 11: Registry scanning in Amazon ECR

Why: Scan on push as a second check.

What: ECR can scan each pushed image. Basic scanning is free; enhanced scanning uses Amazon Inspector.

Example 31: Turn on scan on push

Type this:

aws ecr put-image-scanning-configuration --repository-name shop-api --image-scanning-configuration scanOnPush=true --region ap-south-1 --query imageScanningConfiguration

You will see:

{
    "scanOnPush": true
}

What it means: Every push is scanned.

Example 32: Read findings

Type this:

aws ecr describe-image-scan-findings --repository-name shop-api --image-id imageTag=1.1 --region ap-south-1 --query 'imageScanFindings.findingSeverityCounts'

You will see:

{
    "MEDIUM": 6,
    "LOW": 30
}

What it means: No HIGH or CRITICAL.

Example 33: Rescan old tags

Type this:

printf 'Images in ECR keep aging. Enhanced scanning rescans them when new CVEs are published.\n'

You will see:

Images in ECR keep aging. Enhanced scanning rescans them when new CVEs are published.

What it means: Yesterday's clean image can be today's risk.

Step 12: Keep images clean over time

Why: Scanning is not one-time.

What: New CVEs appear every day. Rebuild on a schedule so base updates flow in, and rescan what is running.

Example 34: Pull the newest base

Type this:

docker build --pull -q -t shop-api:1.2 . && echo rebuilt

You will see:

sha256:9c0d...
rebuilt

What it means: --pull fetches the latest patch of the base tag.

Example 35: Scan running images

Type this:

docker ps --format '{{.Image}}' | sort -u | xargs -I{} sh -c 'docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/.cache/trivy:/root/.cache aquasec/trivy:0.56.2 image -q --severity CRITICAL {} | grep ^Total | sed "s|^|{} |"'

You will see:

shop-api:1.2 Total: 0 (CRITICAL: 0)
nginx:1.27-alpine Total: 0 (CRITICAL: 0)

What it means: Check what is actually live.

Example 36: Weekly cron

Type this:

printf '0 3 * * 1 cd /srv/shop && docker build --pull -t shop-api:weekly . && trivy image --exit-code 1 --severity CRITICAL shop-api:weekly\n' | crontab - && crontab -l

You will see:

0 3 * * 1 cd /srv/shop && docker build --pull -t shop-api:weekly . && trivy image --exit-code 1 --severity CRITICAL shop-api:weekly

What it means: Monday 3 AM rebuild and scan.

4. Common mistakes and how Neha avoids them

Mistake one: scanning once and never again. Neha scans in CI and rebuilds weekly with --pull.

Mistake two: panicking about every LOW finding. She fixes CRITICAL and HIGH with a fix first, and documents the rest.

Mistake three: ignoring app libraries. She reads the language sections of the report, not only the OS part.

Mistake four: a huge base image. She uses slim, alpine or distroless bases so there are fewer packages to patch.

Mistake five: a .trivyignore that grows forever. Every entry has a reason and a review date.

Check yourself

Q1. What does a scanner compare?

The packages and versions in your image against CVE databases.

Q2. Which Trivy flag makes CI fail?

--exit-code 1 with a --severity filter.

Q3. What does --ignore-unfixed hide?

Vulnerabilities that have no fixed version yet.

Q4. Why rebuild images on a schedule?

New CVEs appear daily and base images get patched; --pull brings the fixes in.

Friends, scan every image with Trivy, read both the OS and the library sections, fix CRITICAL and HIGH that have a fix, and gate the pipeline with --exit-code 1. Use Scout recommendations to pick a better base, and rebuild weekly. Did you understand? Let us try it once more. Scan python:3.8 and python:3.12-slim, compare the totals, and fix one library in your own image.

Ravindra Bagale's Tip

The fastest way to remove a hundred CVEs is to change one line: the FROM line. Pick a slim, current base before you chase single packages.

Lab: try at home

  1. Run Trivy from its container on alpine:3.20.
  2. Compare python:3.8 and python:3.12-slim totals.
  3. Build shop-api with old Flask, find the HIGH CVEs, and fix them.
  4. Run trivy config on your Dockerfile and fix the USER finding.
  5. Gate a build with --exit-code 1 and see exit 1, then 0.
  6. Make a CycloneDX SBOM and scan it.
  7. Try docker scout quickview and recommendations.

Recap

  • Scanners match packages to CVE databases.
  • Trivy scans images, folders, Dockerfiles and SBOMs.
  • Filter with --severity and --ignore-unfixed.
  • --exit-code 1 makes CI fail on CRITICAL.
  • Scout recommends a better base.
  • Rebuild with --pull and rescan regularly.

Ravi Sir's Cards for this lesson

मराठी cards