Rule 2.3 — Pin container image digests
Tier 2 · Hardened
Every reference to a container image — in Dockerfile, compose.yml, devcontainer.json, and any GitHub Action that pulls an image — uses name:tag@sha256:<digest>. Plain name:tag and bare name are forbidden; latest is forbidden.
Why it matters
Section titled “Why it matters”A container image tag is mutable in exactly the same way a git tag is. python:3.13 today is a different content-addressable artifact than python:3.13 next week — the registry advances the tag as new patch builds ship. For build reproducibility this means even a clean rebuild of an old git checkout produces different binaries; for supply-chain integrity it means a registry compromise or a typosquat (pyhton:3.13 resolving to an attacker-controlled name) goes undetected.
Adding @sha256:<digest> makes the reference content-addressable. The image’s manifest hash is the identity; any tampering — at the registry, on the CDN, in flight — produces a different digest and the pull fails. This is exactly the SHA-pinning model from Rule 1.2, applied to containers. SLSA Level 3 effectively requires it for the build base image.
How to do it
Section titled “How to do it”In a Dockerfile:
# BadFROM python:3.13
# GoodFROM python:3.13.1-slim@sha256:f3614d98f38b0f8d8b07d8d6f6e0fc6e1c34a8b94b8b6b76e6d97c0db1c5e3a9You can keep the tag on the line alongside the digest. The registry serves the image identified by the digest; the tag is there for human readability. (Some scanners — including deterministic-deps’ containers/image-digest rule — accept either name@sha256:... or name:tag@sha256:....)
In docker-compose.yml:
services: api: image: python:3.13.1-slim@sha256:f3614d98f38b0f8d8b07d8d6f6e0fc6e1c34a8b94b8b6b76e6d97c0db1c5e3a9In devcontainer.json:
{ "image": "mcr.microsoft.com/devcontainers/python:1.2.3-3.13@sha256:..."}In a GitHub Action’s uses: docker://:
- uses: docker://ghcr.io/some/action@sha256:...Finding the digest for a tag
Section titled “Finding the digest for a tag”# Public registries (Docker Hub, ghcr.io, quay.io, mcr.microsoft.com):docker buildx imagetools inspect python:3.13.1-slim | head -5
# Or with crane (smaller, no Docker daemon needed):crane digest python:3.13.1-slim# -> sha256:f3614d98f38b0f8d8b07d8d6f6e0fc6e1c34a8b94b8b6b76e6d97c0db1c5e3a9For multi-platform images (linux/amd64, linux/arm64), crane digest returns the digest of the manifest list — the right thing to pin. Docker honors the list and selects the platform-specific image at pull time, all under one digest.
Keeping digests current
Section titled “Keeping digests current”Renovate opens PRs that bump the digest when the underlying tag advances; Dependabot supports container digest updates for Dockerfile and docker-compose.yml. Pick one, enable it, and let the digests roll over on a cadence. The Tier 1 principle — bounded updates, not stasis — applies.
How to verify
Section titled “How to verify”-
deterministic-depsrule (see the rule catalogue):containers/image-digest(medium / high) — Dockerfile, Compose, and devcontainer image references must usename:tag@sha256:<digest>.latestand untagged references are high severity; tagged-but-undigested references are medium.
-
Alternatives: Trivy flags un-pinned images alongside its vulnerability scan; OpenSSF Scorecard’s
pinned-dependenciescheck covers Dockerfile image pinning as part of its broader pin assessment. -
Manual:
Terminal window # Any FROM line without @sha256:grep -nE '^FROM\s+[^[:space:]]+(\s|$)' Dockerfile | grep -v '@sha256:'
Common pitfalls
Section titled “Common pitfalls”- Pinning the digest but forgetting the tag.
FROM python@sha256:...works but loses the human-readable version label. Keep both:FROM python:3.13.1-slim@sha256:.... The digest is the security mechanism; the tag is what you scan-read later. - Pinning a platform-specific digest instead of the manifest list. If you pin the
linux/amd64digest, the same Dockerfile fails onlinux/arm64. Use the manifest list digest (whatcrane digest <tag>returns by default) so multi-arch builds still resolve. - Forgetting
docker-compose.ymland devcontainers. Thecontainers/image-digestrule scans all three; CI in production scans Dockerfile only is a common oversight. - Trying to pin private registry images without an automated bump path. Manual digest updates rot. Either give Renovate or Dependabot the registry credentials, or run a scheduled job that bumps digests via PR.
- Pinning images that change beneath you in the registry. Most public images are immutable per-digest, but some private registries allow digest deletion. Verify your registry of choice marks digests as immutable.
Real example
Section titled “Real example”No public Ozark-Security-Labs project currently ships a Dockerfile (the org’s portfolio is CLI binaries, libraries, and GitHub Actions — none of which need a runtime container today). For a strong external example, see Distroless’ reference Dockerfiles or the actions/runner-images generated Dockerfiles.
When an OSL project that publishes a container image lands (in scope for late 2026 per the org roadmap), this section will get a first-party real example.