ADR-0009: Pin Every Container Image By Tag And Digest Across The Workspace
Status
Section titled “Status”Accepted
Context
Section titled “Context”Container tags such as latest, 8.5-fpm-alpine or main are mutable: the image they
point to can change at any time without a corresponding repository update. A mutable tag
means today’s build, today’s CI run and today’s deployment can each pull a different
binary even when the configuration is unchanged. For a DevSecOps portfolio this breaks
reproducibility, undermines vulnerability scan results and turns supply chain attacks into
silent compromises.
The workspace already runs Plumber as the supply chain analyzer. Plumber’s configuration
in weather-app/.plumber.yaml enables three relevant controls:
containerImageMustNotUseForbiddenTagsrejectslatest,dev,development,staging,main,master.containerImagesMustBePinnedByDigestrequires every image reference to carry a@sha256:digest.containerImageMustComeFromAuthorizedSourcesenforces an allowlist of registries.
A non compliant reference fails the pipeline before the image is built.
Decision
Section titled “Decision”Every container image referenced anywhere in the workspace uses the immutable form
<registry>/<namespace>/<image>:<version>@sha256:<digest>. This applies to:
FROMlines in every Dockerfile.image:andservices.image:fields in every GitLab CI job.image:fields in every Kubernetes manifest, Helm values file and Helm chart template.imagefields in every docker compose file.- Image references quoted in documentation when they describe what the runtime pulls.
The digest is resolved at the time the reference is written or refreshed with
docker buildx imagetools inspect <image>:<tag> --format '{{.Manifest.Digest}}'. The
output is pasted into the reference verbatim. No reference is committed with a placeholder
or with an unresolved digest.
When an image needs to be bumped, the new version’s digest is resolved the same way and replaces both the version tag and the digest atomically in one commit. The old image identifier appears in the commit diff so reviewers can correlate the upgrade with a known CVE fix or release notes.
The digest is bound to a specific version. A digest that changes for an unchanged tag is not a maintenance event: it is a supply chain incident signal (possible image retag or upstream compromise). The corresponding investigation runbook treats that case as an incident, not a routine bump.
Each repository adopts the same .plumber.yaml controls as weather-app/.plumber.yaml,
so CI rejects regressions on any branch.
Version upgrades are driven by Renovate (or Dependabot on GitHub mirrors). The renovate
config under renovate.json5 watches every image registry referenced in the workspace,
detects new releases, opens a merge request that bumps both the tag and the new digest in
a single change, and triggers the pipeline so Trivy and Plumber gate the new version
before merge.
Consequences
Section titled “Consequences”- Builds are bit identical across machines and over time, until the operator explicitly bumps an image.
- Vulnerability scans (Trivy) operate on a known binary. A clean scan today stays valid until the digest changes.
- Supply chain attacks that rely on retagging a published image are blocked: the tag may move but the digest does not.
- Image upgrades become a deliberate commit, not a silent pull. Reviewers can ask “why this digest” and read the answer in the commit message.
- Maintenance cost shows up as the Renovate flow: a scheduled bot opens version bump MRs with the new tag and digest pinned together, and the operator merges after reviewing the corresponding changelogs and the fresh Trivy report.
- Local development requires
docker buildxavailable to resolve digests during version bumps. The Makefile documents the resolve command so contributors do not paste tags without digests when manually editing image references. - A digest mismatch on an unchanged tag is treated as a supply chain incident, not a bump. The investigation runbook documents the response.