05-production-hardening

Supply Chain: Scanning, SBOMs and Build Provenance

Know what's inside what you ship. Docker Scout, SBOMs, provenance attestations — and how to triage findings without chasing an impossible zero.

September 13, 2026
dockerdocker-scoutsbomprovenancecvesupply-chainregistryslsa

You Ship More Than You Wrote

A typical application image contains a few hundred kilobytes of your code sitting on top of hundreds of packages you didn't write: a base image's operating system libraries, a language runtime, direct dependencies, and the much larger set of transitive dependencies those pulled in.

All of it ships. All of it runs with your application's privileges. And every one of those components has its own maintainers, its own release cadence, and its own vulnerability history.

Supply chain security is the practice of being able to answer three questions about an image:

  1. What's in it? — that's an SBOM.
  2. Does any of it have known vulnerabilities? — that's scanning.
  3. Where did it come from, and is it the artifact my pipeline built? — that's provenance.

Scanning With Docker Scout

Docker Scout compiles an inventory of an image's components and matches it against vulnerability databases. It's the current built-in tooling, and it superseded the older docker scan.

bash
docker scout quickview myapp:1.4.0        # summary, by severity
docker scout cves myapp:1.4.0             # the full list, with details
docker scout recommendations myapp:1.4.0  # suggested base image updates
docker scout compare --to myapp:1.3.0 myapp:1.4.0   # what changed between versions

quickview is the one to run habitually — it takes seconds and tells you whether anything needs attention.

recommendations is the most immediately useful, because it addresses the most common situation. Most findings aren't in your code at all; they're in the base image's OS packages. A newer base image tag frequently clears dozens of CVEs with a one-line change, and this command tells you which one.

compare is the underrated one. "Did this release make things worse?" is a much more tractable question than "is this image perfect?", and it's the right gate for CI — fail the build when a release introduces a new critical vulnerability, rather than when the total count is above zero.

Triage, Not Elimination

The instinct on seeing a scan report with 47 vulnerabilities is to try to reach zero. For most images that is neither achievable nor a good use of the effort — and chasing it teaches a team to ignore the scanner, which is worse than not running one.

Useful triage asks better questions than "how many?":

Is it reachable? A CVE in an image's XML parsing library matters if your application parses untrusted XML. If nothing in your code path ever calls it, the risk is theoretical. Presence in an image is not the same as exposure.

Is there a fix? A critical vulnerability with a patched version available is work you should do today. One with no fix released yet is something to track and mitigate, not something to stare at.

What's the actual exposure? A vulnerability requiring local access is different from one exploitable over the network by an unauthenticated user. Severity scores are a starting point, not the answer — they don't know your architecture.

Does a better base clear it? Run recommendations before doing anything else. Switching to a newer or slimmer base often removes whole categories of finding at once, because it removes the packages.

💡

The most effective vulnerability reduction isn't triage at all — it's the multi-stage build from phase 2. A distroless runtime image has no shell, no package manager and no OS utilities, so it simply doesn't contain the packages that generate most findings. You can't have a vulnerability in software you didn't ship.

Check yourself

A scan of a production image reports 60 vulnerabilities, including 3 critical. The team's deadline is tight. What's the most effective first action?

SBOMs: the Inventory

A Software Bill of Materials lists every component in an image — names, versions, licences. It's the record that lets you answer a question you'll eventually be asked urgently:

A critical vulnerability just dropped in library X. Which of our images contain it, and which versions?

Without an SBOM that means rebuilding and rescanning everything under time pressure. With SBOMs stored alongside your images it's a query.

BuildKit can attach one at build time:

bash
docker buildx build --sbom=true -t myregistry/myapp:1.4.0 --push .

And you read it back from the registry:

bash
docker buildx imagetools inspect myregistry/myapp:1.4.0 --format "{{json .SBOM}}"

Provenance: Where It Came From

A provenance attestation records how an image was built — the source repository and commit, the builder, the build parameters, the materials consumed.

This answers a different question from the SBOM. Not "what's inside?" but "is this the artifact my pipeline actually produced, from the commit I think it was?" Without it, an image in your registry is just bytes with a tag, and tags can be moved.

BuildKit adds provenance by default at a minimal level. For the full record:

bash
docker buildx build --provenance=mode=max -t myregistry/myapp:1.4.0 --push .
bash
docker buildx imagetools inspect myregistry/myapp:1.4.0 --format "{{json .Provenance}}"

The output follows the SLSA provenance format — the industry framework for describing build integrity — which is what makes it consumable by policy tools rather than only by humans.

Docker Hardened Images, mentioned in phase 2, are a concrete example of these pieces assembled: minimal images that run as non-root by default and ship with signed SBOMs and SLSA Build Level 3 provenance. Worth examining even if you don't adopt them — they're a working reference for what a hardened base image supply chain looks like.

Check yourself

A new critical CVE is announced in a widely used compression library. Your organisation runs 200 container images. What makes answering 'which of ours are affected?' fast rather than a multi-day scramble?

Registry Practice

The last mile: getting images to production safely.

Pin by digest. As phase 2 covered, tags move; digests don't. myapp@sha256:a1b2... is an exact artifact.

bash
docker buildx imagetools inspect myregistry/myapp:1.4.0   # shows the digest

Treat release tags as immutable. Reassigning v1.4.0 to different content makes your deployment history a work of fiction and defeats provenance entirely. Many registries can enforce this.

Authenticate in CI. Beyond access to private images, it raises your rate limits considerably.

⚠️

Docker Hub enforces pull rate limits on a rolling 6-hour window: 100 pulls for anonymous users (per IPv4 address or IPv6 /64 subnet), 200 for authenticated free accounts, and unlimited on paid plans. CI runners on shared NAT addresses hit the anonymous limit surprisingly fast, which surfaces as a 429 and a build that fails for reasons unrelated to your code. Authenticating in CI, or pulling through a registry mirror, is the fix.

Scan in the pipeline, gated sensibly. Fail on new critical findings introduced by a change, rather than on a total count — this is where docker scout compare earns its place. A gate developers can actually satisfy gets maintained; one that blocks every build gets disabled.

A Production Build Command

Everything from this phase, in one place:

bash
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --secret id=npmrc,src=$HOME/.npmrc \
  --sbom=true \
  --provenance=mode=max \
  --tag myregistry/myapp:1.4.0 \
  --push .
 
docker scout quickview myregistry/myapp:1.4.0
docker scout compare --to myregistry/myapp:1.3.0 myregistry/myapp:1.4.0

Multi-platform so it runs on ARM and x86. A secret mount so the registry token never enters a layer. An SBOM and full provenance attached. Then a scan, and a comparison against the previous release to see whether this change made things worse.

The Roadmap, End to End

Five phases, one thread:

  1. Foundations — a container is a process with a restricted view; its writable layer dies with it.
  2. Building images — layers cascade, so order matters; BuildKit gives you cache mounts, secret mounts and multi-platform builds; multi-stage builds discard everything you don't ship.
  3. Runtime — namespaces explain networking; volumes outlive containers; exec form decides whether your app ever hears SIGTERM.
  4. Compose and debugging — declare the stack, wait for readiness rather than start, and debug in a fixed order.
  5. Hardening — drop root, cap resources, keep secrets out of layers, and know what's inside what you ship.

The habit underneath all of it: verify behaviour instead of trusting configuration. docker inspect over the command that created it. docker history over the assumption a secret was cleaned up. Timing a docker stop over believing the signal handler is wired. docker scout over hoping the base image is current.

That habit is what makes the roadmap durable. Tooling changes — the old builder gave way to BuildKit, docker scan gave way to Scout, docker-compose gave way to docker compose. The instinct to check what actually happened, rather than what was supposed to, outlasts all of it.

What's Next

Containers are one machine's concern. Running them across a fleet — scheduling, self-healing, rolling updates, service discovery at scale — is orchestration, and that's what the Kubernetes roadmap picks up. Phase 1 there starts exactly where this roadmap ends: with containerd, the runtime you met in this phase's architecture guide.