Chapter 29Lesson 02~155 minutes

Image Vulnerability Scanning, Dependency Hygiene, Base-Image Maintenance, and Remediation Workflows: Guided Hands-On Workflow and Core Operations

Build and scan a disposable old image, preserve machine-readable evidence, remediate through declared source, rebuild to a new image identity, and compare residual findings.

TrivyMachine-readable evidenceRebuildRescanResidual risk

Learning objectives

  • Scan a deliberately old disposable image with a free local scanner and save machine-readable results.
  • Bind before/after reports to Docker image identity, platform, scanner version, and database timestamp.
  • Select one actionable finding without assuming the top severity is automatically the best remediation target.
  • Update declared source, rebuild to a new identity, rescan, compare residual findings, and clean only lab-owned artifacts.

1. Lab topology and safety boundaries

This lab uses two small Alpine-based images and the native Trivy CLI. Nothing is pushed to a registry; no Docker daemon settings are changed; no credential is required; and the scanner runs as a normal host process rather than receiving elevated Docker-host authority. All generated files stay under a dedicated directory and all Docker objects use the da-ch29 prefix.

Current lab inputs. The dated example uses alpine:3.21.0 as the deliberately old starting point and alpine:3.24.2 as the current stable exact patch tag verified on 2026-09-22. If either tag is unavailable later, use an exact supported/older tag pair from the official Alpine image and record the substitution.

2. Preflight: versions, context, scanner, and evidence directory

First capture the execution environment. If Trivy is not installed, use its official package/release instructions and verify the release checksum according to your platform’s normal software-installation practice.

mkdir -p da-ch29/evidence
cd da-ch29

docker version > evidence/docker-version.txt
docker context show > evidence/docker-context.txt
docker info > evidence/docker-info.txt
trivy --version > evidence/trivy-version-before-db.txt

docker pull alpine:3.21.0
docker pull alpine:3.24.2
trivy image --download-db-only
trivy --version > evidence/trivy-version.txt

The two pulls are explicit input changes. Their repository digests and platform-specific local identities become part of the evidence packet; the database-only update separates advisory refresh from the scan itself.

3. Build the before image and freeze its identity

Create a tiny Dockerfile whose only meaningful difference between before and after will be its base-image line. We also install ca-certificates so the image contains a normal package dependency rather than being a completely empty demonstration.

cat > Dockerfile.before <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.21.0
RUN apk add --no-cache ca-certificates
LABEL devops-academy.lab="ch29"       devops-academy.variant="before"
CMD ["sh", "-c", "printf 'chapter29-before\n'"]
EOF

docker buildx build --load --progress=plain   -t da-ch29:before -f Dockerfile.before . 

docker image inspect da-ch29:before > evidence/before-inspect.json
docker image inspect da-ch29:before   --format '{{.Id}} {{.Os}}/{{.Architecture}}' | tee evidence/before-identity.txt

docker image inspect alpine:3.21.0   --format '{{.Id}} {{json .RepoDigests}}' | tee evidence/base-before.txt

The local image ID is immutable content identity for this local build. If you later push the image, also record the repository manifest/index digest; Chapter 13 explained why those digest types are related but not identical.

4. Scan once in JSON, then derive human views from preserved evidence

Machine-readable output is the source record. A terminal table is useful for humans, but it is not as reliable for later automation or comparison.

trivy image --format json --output evidence/before.json da-ch29:before
trivy image --severity HIGH,CRITICAL da-ch29:before   > evidence/before-high-critical.txt

python - <<'PY'
import json
r=json.load(open('evidence/before.json', encoding='utf-8'))
meta=r.get('Metadata', {})
print('artifact:', meta.get('RepoTags') or meta.get('ImageID'))
print('image-id:', meta.get('ImageID'))
for result in r.get('Results') or []:
    for v in result.get('Vulnerabilities') or []:
        if v.get('FixedVersion'):
            print('actionable:', v.get('VulnerabilityID'), v.get('PkgName'),
                  v.get('InstalledVersion'), '->', v.get('FixedVersion'),
                  'severity=', v.get('Severity'))
            raise SystemExit
print('No fixable finding was reported; document that result and use base maintenance as the remediation exercise.')
PY

Do not edit before.json. If the script prints a fixable finding, preserve its advisory ID/package/version. If no fixable finding appears, that is valid evidence; continue the declared base refresh and compare the changed inventory/advisory set rather than inventing a CVE.

5. Remediate through declared source, not the running container

Now create the after Dockerfile. The source-controlled change is obvious and reviewable: the base moves from the old exact patch tag to the current stable exact patch tag. The image is rebuilt from declaration rather than mutated in place.

cat > Dockerfile.after <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.24.2
RUN apk add --no-cache ca-certificates
LABEL devops-academy.lab="ch29"       devops-academy.variant="after"
CMD ["sh", "-c", "printf 'chapter29-after\n'"]
EOF

docker buildx build --pull --load --progress=plain   -t da-ch29:after -f Dockerfile.after .

docker image inspect da-ch29:after > evidence/after-inspect.json
docker image inspect da-ch29:after   --format '{{.Id}} {{.Os}}/{{.Architecture}}' | tee evidence/after-identity.txt

docker image inspect alpine:3.24.2   --format '{{.Id}} {{json .RepoDigests}}' | tee evidence/base-after.txt

--pull deliberately refreshes the named base tag before the build; the exact resulting base identity is recorded immediately. In a release workflow that requires byte-for-byte repeatability, promote the reviewed base digest into the Dockerfile rather than relying on a moving tag.

6. Rescan and compare residual findings

Use the same scanner installation and preserve another machine-readable report. The advisory database may still update between scans unless you intentionally freeze it; record timestamps so that difference is visible.

trivy image --format json --output evidence/after.json da-ch29:after
trivy image --severity HIGH,CRITICAL da-ch29:after   > evidence/after-high-critical.txt
trivy --version > evidence/trivy-version-after.txt

python - <<'PY'
import json

def ids(path):
    d=json.load(open(path, encoding='utf-8'))
    out={}
    for r in d.get('Results') or []:
        for v in r.get('Vulnerabilities') or []:
            out[(v.get('VulnerabilityID'),v.get('PkgName'))]=v
    return out
b,a=ids('evidence/before.json'),ids('evidence/after.json')
print('before matches:', len(b))
print('after matches :', len(a))
print('removed       :', len(set(b)-set(a)))
print('new/changed   :', len(set(a)-set(b)))
for key in sorted(set(b)-set(a))[:10]: print('removed:', *key)
PY

A lower count is useful evidence, not proof of security. A new report can add findings because the new base has different packages or because the advisory database changed. Review the actual package/finding deltas and the application’s tests before promotion.

7. Small challenge: choose the layer that owns the fix

For each case, decide where the correction belongs before touching anything:

  • The finding is in busybox inherited from Alpine: change/refresh the declared base image, then rebuild.
  • The finding is in a Python package locked by your application: update the dependency/lockfile in source, then rebuild.
  • The scanner reports a CVE with no fixed version: triage exposure and create an owned, expiring exception or mitigation—not a fake package upgrade.
  • The report and current tag now point to different image IDs: repair evidence identity first; do not argue about severity until you know which artifact was scanned.

8. Verification and exact cleanup

Verification checklist: before/after IDs differ; both Dockerfiles are preserved; both scanner JSON files exist; scanner/database version evidence exists; at least one finding or “no fix available” case is documented; the new image runs; and no production image/tag/registry was touched.

docker run --rm da-ch29:after
sha256sum evidence/before.json evidence/after.json > evidence/report-checksums.txt

docker image rm da-ch29:before da-ch29:after
# Keep da-ch29/evidence until you finish Lesson 5; remove only that directory afterward.
Cleanup is intentionally narrow. Do not use broad image/cache/system pruning for this lab. Exact lab tags and the dedicated evidence directory are sufficient.

Knowledge check

Why save JSON before looking only at a severity table?

What changed when the Dockerfile moved from Alpine 3.21.0 to 3.24.2?

Why can the after scan contain a finding that was absent before?

If no fixable finding exists, is the lab invalid?

Why avoid broad pruning at cleanup?

Next lesson

Next: Image Vulnerability Scanning, Dependency Hygiene, Base-Image Maintenance, and Remediation Workflows: Configuration, Design Choices, and Tradeoffs

Continue with the next lesson in the course sequence and carry forward the evidence-first Docker operating model.

Official references and version notes

Version/platform baseline, verified 2026-09-22.

Mandatory execution uses the native Trivy 0.74.0 CLI where available, with scanner/database version evidence captured before and after. The example old/new base tags are Alpine 3.21.0 and 3.24.2; 3.24 is the current stable branch verified on 2026-09-22. Scanner findings are intentionally not hard-coded because advisory data changes. The workflow is reproducible by preserving Docker image IDs/base RepoDigests, exact Dockerfiles, scanner JSON, scanner/database metadata, timestamps, and report checksums.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.