skip to content

In `docker buildx`, how do you attach an SBOM and provenance to an image, and where is that data stored?

level: middleimportance: must knowfreq 55%

answer

  1. Two extra flags on the build command
  2. The image gains no filesystem content
  3. Look at the index, not the layers
  4. A sibling manifest with a placeholder platform
  5. Annotation names it an attestation-manifest

basics

~20 s

Pass --sbom=true and --provenance=mode=max to docker buildx build. BuildKit writes each attestation as its own manifest inside the image's OCI index, next to the platform manifests — not as an extra image layer, so layers and config are untouched.

solid answer

~50 s

`docker buildx build --sbom=true --provenance=mode=max` makes BuildKit emit two extra documents about the image it just built: a component inventory and a record of how the build ran. Each one is an in-toto JSON statement stored as a blob and referenced by its **own** manifest, and buildx adds those manifests to the image **index** beside the real platform manifests. They are marked with the annotation `vnd.docker.reference.type=attestation-manifest` and a `vnd.docker.reference.digest` pointing at the manifest they describe, and they carry the placeholder platform `unknown/unknown` so ordinary clients skip them. Nothing goes into the filesystem: the platform manifest's digest is unchanged, and a plain `docker pull` never downloads the attestation blobs. Because the payload lives in an index, you need an exporter that can write one — a `docker-container` buildx builder pushing to a registry, or the containerd image store — rather than the classic Docker image store.

code

bash · 8 lines
bash
docker buildx create --name attested --driver docker-container --use

docker buildx build \
  --sbom=true \
  --provenance=mode=max \
  --platform linux/amd64,linux/arm64 \
  -t registry.example.com/billing/invoice-worker:2.7.3 \
  --push .

go deeper

for a junior

Know that docker buildx build can take --sbom=true and --provenance=..., and that these produce metadata about the image rather than changing what runs inside it. Being able to name the flags is enough at this level.

for a middle

Be ready to explain the storage mechanism: attestations become separate manifests in the image index, annotated as attestation manifests with the placeholder platform unknown/unknown, so the image's own layers and config are untouched.

for a senior

Expect to be asked why a build that works locally loses its attestations. Show that you know the exporter and builder constraints — docker-container builder plus --push, or the containerd image store — and that --load into the classic image store drops them.

for a principal

Own the distinction between producing metadata and relying on it. Be able to say what an unsigned attestation in a registry is worth, who can overwrite it, and why generating attestations is the cheap half of the problem.

### What an "attestation" is in this context An attestation is a JSON document that makes a claim **about** an artefact rather than being part of it. BuildKit can produce two kinds during a build: - an **SBOM attestation** — a machine-readable inventory of what a generator found inside the filesystem that the build produced; - a **provenance attestation** — a record of how the build ran: the builder's identity, start and finish timestamps, the frontend used, the source context, and (at `mode=max`) the full build definition. Both are wrapped as in-toto statements whose `subject` is the digest of the image manifest they describe, and they are stored as blobs with the media type `application/vnd.in-toto+json`. ### Turning them on ```bash docker buildx build \ --sbom=true \ --provenance=mode=max \ -t registry.example.com/billing/invoice-worker:2.7.3 \ --push . ``` `--sbom=true` and `--provenance=mode=max` are shorthands for the general form `--attest type=sbom` and `--attest type=provenance,mode=max`. The SBOM is not produced by the Dockerfile: BuildKit runs a separate generator image over the build result, and you can pin or replace it with `--sbom=generator=<image>`. On a multi-platform build (`--platform linux/amd64,linux/arm64`) you get one SBOM attestation **per** platform manifest, because each platform has a different filesystem. ### Where the data actually lands The key idea is that the pushed artefact stops being a single manifest and becomes an index. For a PHP-FPM invoice-rendering worker image built for one platform with both attestations on, the index has three entries: the real `linux/amd64` manifest, and two attestation manifests pointing back at it. ```json { "mediaType": "application/vnd.oci.image.index.v1+json", "manifests": [ { "digest": "sha256:4c91...", "platform": { "os": "linux", "architecture": "amd64" } }, { "digest": "sha256:b7e2...", "platform": { "os": "unknown", "architecture": "unknown" }, "annotations": { "vnd.docker.reference.type": "attestation-manifest", "vnd.docker.reference.digest": "sha256:4c91..." } } ] } ``` That `unknown/unknown` platform is deliberate: platform matching is how a client picks a manifest, so an entry that matches no real platform is invisible to a normal `docker pull`. Some registry web UIs display it as a stray "unknown/unknown" row, which surprises people the first time they see it. ### Why a sibling manifest and not a layer Putting the inventory into a layer would be the obvious thing to do, and it is wrong on three counts. First, layers are part of the image's identity, so the image digest would change whenever the document changed — you could no longer say "same bytes, extra metadata". Second, the document would then be shipped into every running container's filesystem; the invoice worker, capped at 128 MiB of memory and running as an unprivileged user, would carry a file it never reads and an attacker could. Third, a layer belongs to one platform manifest, so a multi-platform image could not carry one document per architecture cleanly. Keeping attestations as siblings in the index means the platform manifest's digest is byte-for-byte what it would have been without them; only the index digest differs. ### What has to be true for it to work Attestations require an image index, and the classic Docker image store cannot hold multi-manifest indexes. In practice that means one of: a `docker-container` buildx builder (`docker buildx create --use`) that pushes straight to a registry with `--push`; the OCI exporter writing a directory or tarball; or the containerd image store enabled in the engine. A build that ends with `--load` into the old image store, or an image round-tripped through `docker save`/`docker load`, does not keep them. `docker image inspect` and `docker history` will not show them either — they read the platform manifest and config, which is exactly the part attestations do not touch. ### Defaults and version behaviour Attestation support arrived with Buildx v0.10 / BuildKit v0.11. Recent Buildx versions also attach a **minimal** provenance attestation by default when a build pushes to a registry, which is why teams sometimes discover an index with an `unknown/unknown` entry they never asked for. Turn it off per build with `--provenance=false`, or globally with the environment variable `BUILDX_NO_DEFAULT_ATTESTATIONS=1`. ### What attaching does and does not buy you Attaching is not signing, and it is not verification. A plain registry push stores the attestation unsigned; anyone with push rights to that repository can overwrite the index with one carrying different claims. What you get is that the image now describes itself — what a generator saw inside it, and what the builder says it did — addressed by the same digest as the image, so an incident question like "which build produced these bytes" is answerable without a rebuild. Deciding who checks those claims, and what happens when the check fails, is a separate programme from producing them.

  • Your build ends with `--load` instead of `--push` and the attestations vanish. Why?
    `--load` writes into the classic Docker image store, which holds a single image manifest per tag and has no place for the extra attestation manifests, so they are dropped. Push to a registry from a `docker-container` builder, export with the OCI exporter, or enable the containerd image store in the engine — that store can hold a full index locally.
  • Does adding `--sbom=true` change the image's digest?
    Not the digest of the image manifest — its config and layers are identical with or without the flag. What changes is the index digest, because the index now lists an extra manifest. So a deployment pinned to the platform manifest digest is unaffected, while a pin to the tag's index digest will move.
  • Who generates the SBOM content, and can you control it?
    BuildKit runs a separate generator image against the build result rather than deriving it from the Dockerfile. `--sbom=generator=<image>` pins or replaces that image, which is worth doing in CI so the same build produces comparable output over time instead of silently changing when the default generator is updated.

It is a shipping label stapled to the outside of the crate rather than a sheet of paper packed inside it: the contents — and their weight — are unchanged, and anyone who only wants the goods never touches the label.

saying these in an interview costs you the question

  • Says the SBOM is added as an extra image layer
  • Thinks `--sbom=true` scans the image for vulnerabilities
  • Claims attestations change the image manifest's digest
  • Expects attestations to survive `docker save` and `docker load`
  • Believes an ordinary `docker pull` downloads the attestation blobs
  • Treats attaching an attestation as the same thing as signing it

context