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.
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.
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
busyboxinherited 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.
Knowledge check
Why save JSON before looking only at a severity table?
JSON preserves structured package/finding metadata for later comparison, while terminal tables are optimized for immediate human reading.
What changed when the Dockerfile moved from Alpine 3.21.0 to 3.24.2?
The declared build input changed, so the rebuilt image has a new immutable identity and potentially a different package inventory. The running-container writable layer was not used as remediation.
Why can the after scan contain a finding that was absent before?
The package set may differ, or the vulnerability database/matching logic may have changed. That is why scanner/database timestamps are part of the evidence packet.
If no fixable finding exists, is the lab invalid?
No. Record the evidence honestly, perform the declared base-maintenance rebuild, and document residual non-fixable findings/limits instead of inventing a fix.
Why avoid broad pruning at cleanup?
It can delete unrelated images/caches and destroys evidence ownership. Exact lab tags and paths are safer and auditable.
Official references and version notes
- Docker Engine 29 release notes — current Engine/CLI behavior; Engine 29.8.1 is the current release baseline used in this chapter.
-
Docker build best practices
— rebuild cadence,
--pull, digest pinning, and auditable base-image updates. - Docker Scout quickstart — package/vulnerability analysis and remediation workflow; used here as an optional Docker-native comparison path.
- docker scout cves reference — supported artifact types and local/registry resolution semantics.
- Trivy installation — current free local scanner installation; current documented release is 0.74.0.
- Trivy databases — vulnerability-database retrieval, update controls, and cache behavior.
- Trivy image reference — image scanning, JSON output, severity filters, and fixed/unfixed filtering.
- Grype vulnerability database — database freshness, update behavior, and age validation for the optional Grype path.
- Grype releases — current Grype release evidence; v0.119.0 was released 2026-09-17.
- Alpine release branches — support windows for the exact base-image family used in the disposable lab.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.