Container Supply Chain: How I Stopped Trusting Base Images
Tags move underneath you and nobody tells you. Pinning digests, gating on real CVEs, emitting an SBOM, and signing what my pipeline actually built.
The image we shipped in March and the image we shipped in July had the same tag: app:1.4.2. They did not have the same contents. Between those two deploys someone upstream moved the base tag, a transitive package picked up a patch release, and a build cache quietly served a layer from a different architecture. Nobody changed our Dockerfile. The artifact changed anyway.
That is the supply chain problem for containers, and it is not theoretical — it is what happens when your inputs are mutable pointers.
This post is about making the artifact provable. It is not about image size; shaving layers is a different exercise with a different trade-off.
Pin the thing you actually build from
FROM node:22 is a floating reference. Pin by digest:
FROM node:22-slim@sha256:9f3c2a1e5b7d...c4e
Digests do not move. Tags do. Yes, this means you bump the digest deliberately, in a reviewed commit, instead of absorbing upstream changes silently at 3 AM. That is the point.
Do the same for anything you copy from a remote registry during the build. A curl | sh install step is an unpinned dependency wearing a disguise — download a checksum alongside the artifact and verify it.
Scan, but make the gate mean something
My CI runs Trivy against the built image before it is ever tagged for release:
- name: Scan image
run: |
trivy image --exit-code 1 \
--severity CRITICAL,HIGH \
--ignore-unfixed \
app:${{ github.sha }}
Three flags carry the whole policy:
--severity CRITICAL,HIGH— I am not paging anyone for a low-severity lib in a build stage.--ignore-unfixed— if the vendor has no patch, a red build teaches people to hit rerun, not to fix things.--exit-code 1— the scan is a gate, not a report nobody opens.
The mistake I made first was blocking on everything. Within two weeks the pipeline had a permanent red and the team had developed a reflex of bypassing it. A gate that fires on noise is worse than no gate, because it trains the workaround.
Emit an SBOM, even if nobody reads it yet
An SBOM is just a machine-readable inventory of what is inside. Syft produces one cheaply:
syft app:${GIT_SHA} -o spdx-json > sbom.spdx.json
Store it next to the image as a build artifact. The day you need it — a CVE announcement, an incident, a customer questionnaire — you will want the exact list for the exact image you ran, not an approximation from memory. Generating it takes seconds; reconstructing it after the fact is guesswork.
Sign it, then verify the signature
Building an SBOM proves what was in the image at build time. Signing proves your pipeline built it. With cosign:
cosign sign --yes ${IMAGE}@${DIGEST}
cosign attest --predicate sbom.spdx.json --type spdx ${IMAGE}@${DIGEST}
Signing the digest, not the tag, matters — a signature on a mutable tag is a signature on a promise.
Signing is only half of it. If nothing checks the signature, you have written a file nobody reads. The check belongs at admission, so an unverified image never reaches a node:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: verify-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences: ["registry.internal/*"]
attestors:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
What I would not attempt first
Attestation frameworks with a 12-step maturity model, provenance systems that need a dedicated owner, and anything requiring you to rebuild your pipeline from scratch. Digest pinning, one scanner with a sane threshold, one SBOM artifact, and one signature you actually verify will outperform an impressive diagram nobody operates.
Summary
Your base image is a dependency, and dependencies should be pinned, scanned, inventoried, and signed — the same way you would treat a library. Pin by digest so the input cannot drift, gate the scan on the severities you would genuinely stop a release for, keep an SBOM with the artifact, sign the digest rather than the tag, and make something at deploy time refuse to run images that are not signed. Four steps, all of them boring, all of them upstream of the moment an unknown package ships to production.
SDP Clouds Team
DevOps and cloud engineers writing practical, battle-tested guides on CI/CD, Kubernetes, infrastructure as code, and production operations — every article is based on real incidents and real pipelines, not docs-page rewrites.
More about us →