Chapter 29Lesson 05~170 minutes

Checkpoint Lab — Image Vulnerability Scanning, Dependency Hygiene, Base-Image Maintenance, and Remediation Workflows

Produce a before/after vulnerability-remediation packet bound to two immutable image identities, scanner/database evidence, one documented fix, residual risk, and an expiring exception format.

Checkpoint labBefore/afterDigest bindingAccepted riskVerification

Learning objectives

  • Produce a before/after packet bound to two immutable local image identities and exact base references.
  • Capture scanner version/database state, machine-readable reports, and report checksums.
  • Document one remediation and one residual/non-fixable risk without overstating scanner certainty.
  • Define an accepted-risk record with owner and expiry, then verify cleanup affects only lab resources.

1. Scenario and success criteria

You are preparing a release-review packet for a tiny synthetic service. Version A uses an older base; version B updates the base through a declared Dockerfile change. The reviewer must be able to prove which two artifacts were scanned, which scanner/database snapshot was used, what changed, and what residual risk remains.

Success is not “the number went to zero.” Success is a traceable decision: exact A identity → exact scan → declared remediation → exact B identity → exact rescan → residual findings/exception → verification.

2. Preflight and assumptions

The mandatory path requires Docker Engine/Desktop capable of Linux containers, Buildx integrated with the Docker CLI, Trivy installed locally, basic shell utilities, and network access only to pull public base images/scanner databases. No registry account, Docker Scout subscription, privileged container, or production endpoint is required.

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

docker version > evidence/docker-version.txt
docker context show > evidence/context.txt
docker buildx version > evidence/buildx-version.txt
trivy --version > evidence/trivy-version-pre.txt
trivy image --download-db-only
trivy --version > evidence/trivy-version.txt

docker pull alpine:3.21.0
docker pull alpine:3.24.2

Record any substitution if your platform cannot use these tags. Do not silently switch architecture or base family; that changes the evidence question.

3. Prediction ledger before changes

Write two predictions before building:

  1. Changing the base from 3.21.0 to 3.24.2 will produce a different immutable image ID and package inventory.
  2. The after scan will differ from the before scan, but the direction/count of findings is not guaranteed because both inventory and advisory data can change.

Add a third platform-specific prediction if useful, such as the expected linux/amd64 or linux/arm64 platform. The point is to test state claims rather than narrate them afterward.

4. Build and scan artifact A

Create the before Dockerfile, build, preserve immutable identity and base evidence, then scan to JSON. The scan output itself is never edited.

cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
ARG BASE_IMAGE=alpine:3.21.0
FROM ${BASE_IMAGE}
RUN apk add --no-cache ca-certificates
LABEL devops-academy.lab="ch29-checkpoint"
USER 65532:65532
CMD ["sh", "-c", "printf 'checkpoint\n'"]
EOF

docker buildx build --pull --load --progress=plain   -t da-ch29-checkpoint:a . 

docker image inspect da-ch29-checkpoint:a > evidence/a-inspect.json
docker image inspect da-ch29-checkpoint:a   --format '{{.Id}} {{.Os}}/{{.Architecture}}' | tee evidence/a-identity.txt
docker image inspect alpine:3.21.0   --format '{{.Id}} {{json .RepoDigests}}' | tee evidence/a-base.txt
trivy image --format json -o evidence/a-scan.json da-ch29-checkpoint:a
sha256sum evidence/a-scan.json > evidence/a-scan.sha256
Why numeric non-root USER? It keeps the synthetic runtime least-privilege and avoids requiring a passwd entry merely to print a line. The scanning exercise remains about image content, not runtime privilege.

5. Select and document one finding

Extract the first fixable finding if one exists. If there is no fixable finding, select one residual finding and explicitly mark it “no fixed version reported” rather than forcing a fake remediation claim.

python - <<'PY' | tee evidence/selected-finding.txt
import json
r=json.load(open('evidence/a-scan.json', encoding='utf-8'))
allv=[]
for result in r.get('Results') or []:
    for v in result.get('Vulnerabilities') or []:
        allv.append(v)
fix=next((v for v in allv if v.get('FixedVersion')), None)
v=fix or (allv[0] if allv else None)
if not v:
    print('NO_MATCHES: scanner reported no vulnerabilities for this artifact/database snapshot')
else:
    for k in ['VulnerabilityID','PkgName','InstalledVersion','FixedVersion','Severity','Status']:
        print(f'{k}={v.get(k)}')
PY

Add a short context note: package source (base OS or application dependency), whether the application plausibly uses the affected component, exposure, and whether the scanner reports a fix. Do not erase uncertainty.

6. Declared remediation and artifact B

Change only the declared base input, rebuild, and preserve B evidence. The exact patch tag makes the human-readable change auditable; the resulting local image ID and pulled base RepoDigest provide immutable evidence.

python - <<'PY'
p='Dockerfile'
s=open(p, encoding='utf-8').read()
s=s.replace('alpine:3.21.0','alpine:3.24.2')
open(p,'w',encoding='utf-8').write(s)
PY
cp Dockerfile evidence/Dockerfile.after

docker buildx build --pull --load --progress=plain   -t da-ch29-checkpoint:b .

docker image inspect da-ch29-checkpoint:b > evidence/b-inspect.json
docker image inspect da-ch29-checkpoint:b   --format '{{.Id}} {{.Os}}/{{.Architecture}}' | tee evidence/b-identity.txt
docker image inspect alpine:3.24.2   --format '{{.Id}} {{json .RepoDigests}}' | tee evidence/b-base.txt
trivy image --format json -o evidence/b-scan.json da-ch29-checkpoint:b
trivy --version > evidence/trivy-version-post.txt
sha256sum evidence/b-scan.json > evidence/b-scan.sha256

Also preserve the before Dockerfile before editing in a real workflow. For this lab, copy it into evidence before replacement if you want the packet fully self-contained.

7. Compare, write residual-risk record, and verify runtime

Create a concise comparison and an exception template for a finding that remains without a current fix. The exception is not automatically approved; it is the structure a reviewer would evaluate.

python - <<'PY' | tee evidence/comparison.txt
import json

def vulns(p):
    d=json.load(open(p, encoding='utf-8'))
    return {(v.get('VulnerabilityID'),v.get('PkgName')):v
            for r in d.get('Results') or []
            for v in (r.get('Vulnerabilities') or [])}
a,b=vulns('evidence/a-scan.json'),vulns('evidence/b-scan.json')
print('A findings:',len(a))
print('B findings:',len(b))
print('Removed:',len(set(a)-set(b)))
print('Remaining/common:',len(set(a)&set(b)))
print('New in B:',len(set(b)-set(a)))
PY

cat > evidence/accepted-risk-template.txt <<'EOF'
artifact_identity=<exact B image ID or repository digest>
platform=<os/arch>
finding=<CVE/advisory + package + installed version>
scanner_and_db=<Trivy version + DB timestamps/status>
fix_status=<none/currently unavailable/deferred>
context=<reachability/exposure/feature use>
compensating_controls=<if any>
owner=<named role/person>
approved_by=<reviewer>
created_at=<UTC timestamp>
review_or_expiry=<date>
EOF

docker run --rm da-ch29-checkpoint:b | tee evidence/runtime-smoke.txt
sha256sum evidence/* > evidence/evidence-checksums.txt

If B still contains the selected finding, explain whether the base update failed to address it, whether the finding is from another package ecosystem, or whether the advisory data changed. That explanation is more valuable than forcing a “green” result.

8. Final checklist, cleanup, and bridge to Chapter 30

Your packet should contain: Docker/context/Buildx versions; Trivy/database metadata; A/B Dockerfiles or source diff; A/B image identities/platforms; base image identities; A/B scanner JSON and checksums; one selected finding; before/after comparison; runtime smoke evidence; and an assumptions/limitations note. Verify A and B identities differ and that every report names or can be joined to the intended artifact.

docker image rm da-ch29-checkpoint:a da-ch29-checkpoint:b
# Keep evidence/ for review. After review, remove only da-ch29-checkpoint/.

This chapter adds digest-bound vulnerability/remediation evidence to the production Docker operating model. Chapter 30 extends the same “exact subject digest” discipline to SBOMs, SPDX/CycloneDX, in-toto/SLSA provenance, signatures, and verification workflows.

Knowledge check

What proves artifact A and artifact B are different releases in this local lab?

Why is it acceptable if the after scan still reports vulnerabilities?

What fields make the accepted-risk record safe to review later?

A teammate rescans da-ch29-checkpoint:b next month and gets different results. Is that automatically suspicious?

What is the key bridge from Chapter 29 to Chapter 30?

Next lesson

Next: SBOMs, SPDX/CycloneDX, in-toto Attestations, SLSA Provenance, Signatures, and Verification Workflows: Concepts, Architecture, and Mental Model

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.

Checkpoint assumptions are dated 2026-09-22. Docker Engine 29.8.1, Trivy 0.74.0, and Alpine 3.24.2 are the current baselines used for explanatory notes; the lab records the actually installed scanner/Engine and resolved image identities. Grype 0.119.0 and Docker Scout are optional comparison paths only. No paid service, external secret, privileged scanner access, production registry, broad prune, or mutable-only release identity is required.

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.