Verify Docker image provenance
Dockerfile / compose.yml# Not this — the tag can be repointed at any timeFROM node:22-alpine
# This — the digest is the image, permanentlyFROM node:22-alpine@sha256:9bcd4a8c0b1e9c9b1f2e1f8e0d3c4b5a6978859f0e1d2c3b4a5968778695a4b3# DOCKER_CONTENT_TRUST=1 # retired in 2025 — do not useDocker Content Trust is retired
Section titled “Docker Content Trust is retired”Nearly every Docker hardening guide tells you to set DOCKER_CONTENT_TRUST=1.
That advice is dead.
Docker retired Content Trust, with the retirement beginning 8 August 2025 as the oldest DCT signing certificates for Docker Official Images started expiring. It depended on the upstream Notary v1 server, which is no longer maintained. Docker’s own figure for its usage before retirement: fewer than 0.05% of Docker Hub image pulls.
So DOCKER_CONTENT_TRUST=1 is not a control you’re missing. It is a variable that
does nothing useful and, once the certificates expire, mostly produces failures
for images whose signatures can no longer be validated. Docker’s guidance is to
move to Sigstore/Cosign or the Notary Project’s Notation.
If your CI sets it, remove it. If your hardening checklist lists it, the checklist predates August 2025 and everything else on it deserves a second look.
Digest pinning is the control that actually works
Section titled “Digest pinning is the control that actually works”Before reaching for signatures, use the thing that requires no infrastructure at all.
A tag is a mutable pointer. node:22-alpine today and node:22-alpine
tomorrow can be different images. That’s normally benign — it’s how you get
patches — but it means your build is not reproducible, and a compromised registry
account or a namespace takeover repoints the tag under you.
A digest is the image. @sha256:... is a content hash: it cannot be
repointed, and Docker verifies it on pull. If the bytes don’t match the hash, the
pull fails. That is integrity with no keys, no CA, and no transparency log.
The trade-off is real: pinned digests don’t get patches automatically. That’s what Dependabot and Renovate are for — both understand digest pins and will open a PR when the tag moves, which turns “silently updated” into “reviewed update”. That’s the version you want.
If you need signatures: Cosign
Section titled “If you need signatures: Cosign”cosign sign --key cosign.key myregistry.io/myapp:1.4.2cosign verify --key cosign.pub myregistry.io/myapp:1.4.2Or keyless, which is the interesting part — no key to manage or leak:
cosign sign myregistry.io/myapp:1.4.2 # OIDC identity, ephemeral certcosign verify myregistry.io/myapp:1.4.2 \ --certificate-identity=ci@example.com \ --certificate-oidc-issuer=https://token.actions.githubusercontent.comKeyless signing uses your OIDC identity, gets a short-lived certificate from the Fulcio CA, and records the signature in the Rekor transparency log. There is no private key sitting in CI to steal.
Both Cosign and Notation store signatures alongside the image in any OCI-compliant registry — which is precisely what DCT couldn’t do, since it needed a separate Notary server. That’s why the ecosystem moved.
Signing only means something if something verifies. A signature nobody checks is decoration — the verification belongs in your deploy pipeline or an admission controller, not in a README.
Related
Section titled “Related”- Protect the Docker socket — read before running socket-mounting scanners.
- Container escape — where a malicious image goes next.