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:
- Non-reproducible builds.
node:22-alpinetoday andnode:22-alpinetomorrow are different bytes. A successful build today may fail the same build pipeline tomorrow when the upstream image rebuilds. - 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.
- 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
FROMline 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:
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:
DIGEST=$(docker buildx imagetools inspect node:22-alpine | grep -E '^Digest:' | awk '{print $2}')
# Edit the Dockerfile to read:
# FROM node:22-alpine@${DIGEST}