Safeguard
Engineering

Container Image Digests vs Tags: Why Pinning Matters

A tag is a mutable pointer; a digest is the image. Pinning by digest is the difference between deploying what you tested and deploying whatever the registry says today.

Priya Raman
Staff Security Engineer
6 min read

A container image tag is a mutable pointer that can be retargeted at any time, while a digest is a content-addressed SHA-256 hash of the image manifest — pinning by digest is the only way to guarantee that the image you deploy is byte-for-byte the image you tested. Everything else in this post is working out the consequences of that one sentence.

Tags lie by design

When you write FROM node:20-alpine, you're not naming an image. You're naming whatever the registry currently associates with that string. The association changes constantly and silently:

  • node:20-alpine gets rebuilt every time upstream patches the Alpine base or bumps the Node patch release — often weekly.
  • :latest retargets on every publish, obviously.
  • Even "immutable-looking" tags like node:20.11.1-alpine3.19 can be re-pushed. Docker Hub does not enforce tag immutability; some registries (ECR with tag immutability enabled, GitLab with protected tags) can, but it's opt-in and rare in practice.

This mutability is a feature for patch delivery and a liability for reproducibility. The same docker build on Monday and Thursday can produce different images with different CVE sets, and your CI logs will show identical commands. When an incident review asks "what exactly was running," a tag is not an answer.

It's also an attack vector. An attacker with push access to a registry namespace — leaked token, compromised CI, or a takeover of an abandoned Docker Hub org — can retarget a popular tag and every downstream consumer pulls the payload on their next build. No package manager, no CVE, no alert.

Digests are the image

A digest names content, not a location in a namespace:

FROM node:20-alpine@sha256:0b1e5b3fd0f4b0a8f6a9f9e2f7a6f1cb9c73dcf29e9c3a1e5b3fd0f4b0a8f6a9

Keep the tag in front of the @ — the runtime ignores it for resolution but humans need it for readability. To get the digest for what you're currently running:

docker buildx imagetools inspect node:20-alpine
# or, lighter-weight and scriptable:
crane digest node:20-alpine

crane (from go-containerregistry) is worth having in CI anyway; it resolves digests without pulling the full image, which matters when your base image is 400 MB and your runner minutes aren't free.

In Kubernetes, the same principle applies to manifests:

image: registry.example.com/api@sha256:7d0f4b0a8f6a...

One caveat: with a digest reference, imagePullPolicy: Always becomes nearly free (the kubelet just verifies the digest matches), so there's no performance argument against it anymore.

"But then I never get security patches"

This is the standard objection, and it's correct as far as it goes: a pinned digest freezes you on whatever CVEs the base image had that day. The answer isn't to unpin — it's to automate the repinning:

{
  "extends": ["config:recommended", ":pinDigests"],
  "packageRules": [
    {
      "matchDatasources": ["docker"],
      "schedule": ["before 6am on monday"]
    }
  ]
}

Renovate's :pinDigests preset rewrites every FROM line to a digest and then opens a PR whenever the upstream tag moves, showing you the old and new digest side by side. Dependabot does the same for Dockerfiles and GitHub Actions. Now base image updates flow through the same review-test-merge pipeline as every other dependency change, instead of happening invisibly at build time. The identical logic applies to CI actions themselves — see pinning GitHub Actions by SHA vs tag — because actions/checkout@v4 is exactly as mutable as node:20-alpine.

The operational rhythm that works: weekly digest-bump PRs, auto-merged if the image scan comes back clean and the test suite passes. Teams running this pattern typically absorb base image patches within 3–7 days of upstream release, which beats the median unpinned team, who absorb them at some random future rebuild — or never, if their layer cache is warm.

Digests compose with signing and SBOMs

Everything downstream of the digest gets stronger:

  • Signature verification. cosign sign and cosign verify operate on digests. A signature on a tag would be meaningless — the tag can move to unsigned content. Kyverno and Sigstore policy-controller admission policies verify the digest of the exact image entering the cluster.
  • SBOM binding. An SBOM attached to sha256:7d0f... describes that image forever. An SBOM "for api:v2.3" describes a moment in time that the tag has since abandoned. When you diff SBOMs across releases in SBOM Studio, the digests are the join key that makes the diff trustworthy.
  • Admission control. A policy of "only digests, no tags" is mechanically enforceable:
kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.image}{"\n"}{end}' | grep -v '@sha256:'

Any output is a policy violation. Run it in a cronjob; the list should be empty and stay empty.

Rollout order that doesn't hurt

If you're starting from an unpinned estate, sequence it like this:

  1. Inventory. Extract every FROM and every Kubernetes image: reference. Grep the repo; query the cluster. Expect to find 30–80 distinct base references in a mid-size org, half of them variants of the same image.
  2. Pin production manifests first. That's where a retargeted tag does the most damage.
  3. Pin Dockerfiles, converting each tag to tag@digest.
  4. Turn on Renovate/Dependabot digest updates the same day you pin. Pinning without automation is how images fossilize.
  5. Add the admission gate last, once the PR flow is proven, so you're not blocking deploys on day one.

An SCA pipeline that scans images continuously — the model behind Safeguard's SCA product — closes the loop: it tells you when a pinned digest has accumulated enough new CVEs that the pending bump PR should jump the queue.

Frequently asked questions

Do digests protect against a compromised registry?

Yes, for content integrity. The digest is verified client-side on pull, so a compromised registry can refuse to serve you or serve stale content, but it cannot substitute different bytes under a pinned digest without the pull failing. Combine digests with cosign signatures if you also need to authenticate who produced the image.

Won't pinned digests break multi-arch images?

No, if you pin the manifest list digest (what crane digest returns for a multi-arch tag) rather than a platform-specific manifest digest. The manifest list digest resolves correctly on both amd64 and arm64 nodes. Pinning the amd64-specific digest and then scheduling onto Graviton nodes is the classic self-inflicted outage.

How often should digest bump PRs merge?

Weekly is the sweet spot for most teams: fast enough that base image CVE exposure stays measured in days, slow enough that review isn't noise. Bump immediately out-of-band when a critical CVE lands in your base image — your scanner should page the bump, not the calendar.

Is :latest ever acceptable?

In throwaway local experiments, sure. In anything with a deploy pipeline, no — :latest combines maximal mutability with zero information about what you're running. Even dev environments benefit from pinning, because "works on my machine" bugs are frequently two engineers resolving the same tag to different digests.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.