Skip to main content
RunBook Academy

Docker & ContainersXII · Supply ChainSBOM

SBOM — software bill of materials for images

Intermediate⏱ ~24 mindocker

What you'll learn

  • Generate SBOMs with BuildKit and with Syft, and know where each one ends up
  • Distinguish SPDX, CycloneDX and the in-toto envelope that carries them
  • Verify that an image actually carries an SBOM attestation and that the SBOM is plausible
  • Build the retroactive query that is the entire point of having SBOMs

Prerequisites

Verified against Docker Engine 29.x · Docker Engine 28.x · Docker Compose 2.x · containerd 2.x · runc 1.2.x · BuildKit 0.20+ · Linux kernel 5.15+ · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · 2026-08-12

Not yet marked complete on this device.

An SBOM is a structured list of every component in a piece of software. For an image that means every package, library and — where the format is present — every binary the cataloguer could identify.

The promise is that “what versions of openssl are running in production?” becomes a query rather than an archaeology project. The promise is real. It is also conditional on something almost nobody does at the same time as turning the flag on.

Generating

BuildKit, at build time

Configuration changebuild with an SBOM attestation
docker buildx build \
--sbom=true \
--provenance=mode=max \
--tag registry.example.com/myorg/myapp:1.0.0 \
--push .

BuildKit’s SBOM is generated from what the builder observed while building, and is attached “to the final image as a JSON-encoded SPDX document, using the format defined by the in-toto SPDX predicate”.

Syft, for images you did not build

Read-only / Safegenerate SBOMs in both formats
IMG=registry.example.com/myorg/myapp@sha256:REPLACE_ME

syft "$IMG" \
-o spdx-json=./sbom.spdx.json \
-o cyclonedx-json=./sbom.cdx.json

Syft catalogues a filesystem after the fact. That is the right tool for a third-party image, a base image you are evaluating, or an artefact whose build you do not control.

The formats

FormatStewardHeritageWhere it fits
SPDXLinux FoundationLicense and complianceRegulatory reporting, licence obligations, the format BuildKit emits
CycloneDXOWASPSecurityVulnerability management, and it carries VEX natively

Those are the two SBOM formats. Both express the same core idea — components, versions, identifiers, relationships — and good tooling converts between them with acceptable fidelity.

Choose SPDX when licence and compliance reporting is the driver, or when you want the format BuildKit produces natively. Choose CycloneDX when vulnerability management is the driver and you want VEX statements in the same document. If you have no strong reason, produce both — the marginal cost is one extra output flag.

Verifying an image actually has one

This is the check that would have caught the “a third of our SBOMs are empty” problem in the story above, and it takes one command.

Read-only / Safedoes this image carry an SBOM?
IMG=registry.example.com/myorg/myapp:1.0.0

docker buildx imagetools inspect "$IMG" \
--format '{{ json .SBOM.SPDX }}' > /dev/null \
&& echo 'OK: SBOM attestation present' \
|| echo 'FAIL: no SBOM attestation on this image' >&2
OK: SBOM attestation present

Illustrative output

Presence is not enough. An SBOM listing four packages for a Debian-based image is a cataloguer failure, not a minimal image, and it will pass every “does an SBOM exist” check ever written.

Read-only / Safeis the SBOM plausible?
IMG=registry.example.com/myorg/myapp:1.0.0

docker buildx imagetools inspect "$IMG" \
--format '{{ range .SBOM.SPDX.packages }}{{ .name }}@{{ .versionInfo }}{{ println }}{{ end }}' \
| tee /tmp/sbom-packages.txt \
| wc -l
412

Illustrative output

Assert a floor in CI. A debian:bookworm-slim base yields a few hundred packages; if the count drops to single digits between one build and the next, something in the build changed in a way nobody intended. That assertion is cheap and it is the difference between an SBOM programme that works and one that is discovered to be hollow during an incident.

Acting on it

The workflow that turns SBOMs from paperwork into an answer:

  1. Generate at build time, attached to the image, so the SBOM and the artefact cannot become separated.
  2. Store keyed by image digest. Not by build number, not by tag. The digest is the only identifier that is stable and that a running container can be resolved to.
  3. Index the component list into something queryable. A database table of (image digest, package name, version) is enough; this does not need a product.
  4. Re-scan on a schedule against a current vulnerability database — the SBOM is fixed, the database is not, and the whole value is in re-matching an unchanged component list against new knowledge.
  5. Join against what is deployed. An SBOM index that cannot be joined to running containers by digest tells you what you have built, not what you are running.
  6. Rehearse the query on a normal day, so the first time you run it is not during an incident.

Step four is the one that makes the whole thing worthwhile and the one people skip. The vulnerability that matters is almost never known when the image is built — it is disclosed months later, against a component list that has not changed. Scanning at build time catches what was known then; re-scanning the stored SBOM catches what is known now, without pulling or rebuilding anything.

Read-only / Safere-scan a stored SBOM against today's database
trivy sbom ./sbom.spdx.json --severity HIGH,CRITICAL --ignore-unfixed
sbom.spdx.json (debian 12.5)
Total: 2 (HIGH: 2, CRITICAL: 0)

Illustrative output

Read-only / Safethe query the whole programme exists for
PKG=libssl3
SBOM_DIR=/srv/sbom-index

grep -l "\"name\": \"${PKG}\"" "$SBOM_DIR"/*.spdx.json \
| while IFS= read -r f; do
    basename "$f" .spdx.json
  done
sha256:4f2ac19d00000000000000000000000000000000000000000000000000000000
sha256:81e077b300000000000000000000000000000000000000000000000000000000

Illustrative output

Those digests join directly to docker inspect --format '{{.Image}}' across the fleet, which is the entire chain from “a CVE was announced” to “these seventeen containers on these five hosts”. If that chain has a broken link anywhere, the SBOM programme is not finished.

Knowledge check

Knowledge check · 5 questions

  1. Q1. Where does a BuildKit SBOM attestation physically live?

  2. Q2. What is in-toto, in relation to SPDX and CycloneDX?

  3. Q3. Which are required before an SBOM can answer "which running images contain package X" quickly? Select all that apply.

  4. Q4. Building with `--sbom=true` and `--load` on a daemon using the classic image store produces an image with an SBOM attestation.

  5. Q5. Re-scanning a stored SBOM on a schedule finds vulnerabilities that a build-time scan could not have found.

Passing score: 75%. Answers are checked in this browser.