Disciplines · Runbooks

Docker base image SHA pinning

The audit pass 3 sweep found 100+ Dockerfiles in the repo that ship

6sections2 minread

On this page

Owner: platform team Last reviewed: 2026-05-28 Audit reference: pass 3 #145 (B4)

Why pin#

The audit pass 3 sweep found 100+ Dockerfiles in the repo that ship FROM <image>:<tag> without a content-addressable digest. The implications:

  1. Non-reproducible builds. node:22-alpine today and node:22-alpine tomorrow are different bytes. A successful build today may fail the same build pipeline tomorrow when the upstream image rebuilds.
  2. Supply-chain attack surface. If the upstream registry is compromised, or a tag is silently re-pushed to point at a malicious layer, every downstream build pulls the new bytes — no audit trail, no warning.
  3. No drift detection. Without digests, the lockfile tells you nothing about what was actually built; the image hash is the only ground truth.

The fix is to rewrite FROM image:tag into FROM image:tag@sha256:<digest>. Renovate then handles bumping the digest on a schedule.

How we pin#

This repo's renovate.json extends docker:pinDigests, which:

  • On first run, opens one PR per Dockerfile that rewrites every FROM line to include the current registry digest for the named tag.
  • On every weekly run, opens a single grouped PR ("docker base image digests") that bumps any digest whose upstream image has been republished.

The first-pass PRs need a human reviewer to merge in batches. We do NOT auto-merge because:

  • A digest bump for an alpine base sometimes ships a soname ABI break that breaks our native-binding builds.
  • We want the security team to glance at any digest change to a base image that runs untrusted user code.

What the audit's grep would find#

Run locally to verify which Dockerfiles still need pinning:

bash
grep -L "^FROM .*@sha256:" $(find . -name "Dockerfile*" -not -path "*/node_modules/*")

Anything listed is missing a pinned digest. Expected steady state after Renovate has run a full cycle: empty.

Per-image policy#

Base image family Pinning required? Why
node:* (slim, alpine, base) Yes Used in 70+ services
python:* (slim, alpine) Yes Used by psyche, lilith ml workers
nvidia/cuda:* Yes Bigger ABI surface; harder to debug a regress
runpod/* base images Yes Stand-in for our own bases; pin to be safe
scratch No No content to pin

Recovery#

If a pinned digest goes missing from the registry (rare — Docker Hub does GC dangling tags after 6 months), docker pull will fail. Renovate's next weekly run will surface the missing digest in a PR with an updated valid digest from the next-newest publish. Until the PR merges, builds that depend on the missing digest fail loudly — which is the correct behaviour versus silently pulling new bytes.

Manual one-off pin#

If a developer wants to pin a single Dockerfile ahead of Renovate's schedule, the workflow is:

bash
DIGEST=$(docker buildx imagetools inspect node:22-alpine | grep -E '^Digest:' | awk '{print $2}')
# Edit the Dockerfile to read:
#   FROM node:22-alpine@${DIGEST}