CertGrid CertGrid
Best Practices·Docker

Docker Image SBOMs and Provenance

You cannot answer "are we affected by this CVE" without knowing what is inside your images. Generate a real SPDX bill of materials at build time, read the package list back out of the registry, and see where the provenance record lives.

Security and Production Guide 38 of 46 Advanced

Tested on the versions above. Attestations need a BuildKit builder. Package lists and digests differ for every image - match the shape, not the strings.

One Docker host is all this guide needs. Nothing here depends on a second machine, and the hardware above is modest on purpose - a 2 core, 4 GB VM runs everything in this path.
Server NameIP AddressOSRolesCPURAMHDD
DOCKER01192.168.0.21Ubuntu 26.04 LTSDocker Host2 Core4 GB50 GB

Before you start

  1. The question you need to be able to answer in minutes

    When a vulnerability lands in a common library, the only question that matters is which of your images contain it. Without a bill of materials you answer that by rebuilding and grepping, image by image, under time pressure. An SBOM is that answer recorded at build time, when the information is free.

    bash
    cat DockerfileFROM alpine:3.22RUN apk add --no-cache curlCMD ["curl","--version"]# two instructions - and 26 packages in the result, as the SBOM will show

    Expected resultA trivial Dockerfile.

    Success conditionYou accept that you do not know what is in this image. Neither does anyone, until it is measured.

  2. Build with attestations attached

    --sbom=true and --provenance=true make BuildKit record what went into the image and how it was built, then push both alongside it. They are attached to the image rather than stored separately, so they travel with it through every registry copy.

    bash Example session
    docker buildx build --builder cg-builder --sbom=true --provenance=true -t localhost:5000/cg-ci:1 --push .#9 exporting attestation manifest sha256:67cb2ddf8e16dc650662c0f4a38ff8e8f41195c2d718ace2d42e5d16af86e506 0.0s done#9 exporting manifest list sha256:2dd2a150c294f6984e676418b7230fe26b7b1c3aaa7845c362c1179fe7bcfe1d 0.0s done#9 DONE 0.6s

    Expected resultAn attestation manifest exported alongside the image manifest.

    Success conditionThe build produced an extra manifest. That is the attestation - if you do not see this line, the flags did not take effect.

  3. See how they are stored

    The tag now points at an index containing two manifests: your image for linux/amd64, and a second with platform unknown/unknown. That odd platform is deliberate - it keeps older clients from trying to run it. The annotation ties the attestation to the exact image digest it describes.

    bash Example session
    docker buildx imagetools inspect localhost:5000/cg-ci:1MediaType: application/vnd.oci.image.index.v1+json  Name:        localhost:5000/cg-ci:1@sha256:063a3b8b5f85429a3e3430a1ebfe151fb9d9a0f15cd8989e5c5673094604bed2  Platform:    linux/amd64  Name:        localhost:5000/cg-ci:1@sha256:67cb2ddf8e16dc650662c0f4a38ff8e8f41195c2d718ace2d42e5d16af86e506  Platform:    unknown/unknown  Annotations:    vnd.docker.reference.digest: sha256:063a3b8b5f85429a3e3430a1ebfe151fb9d9a0f15cd8989e5c5673094604bed2    vnd.docker.reference.type:   attestation-manifest

    Expected resultTwo manifests, the second marked as an attestation-manifest.

    Success conditionYou can point at where the SBOM lives. unknown/unknown is expected, not a fault - and it is why a plain docker pull ignores it.

  4. Read the bill of materials

    The SBOM is SPDX, a standard format that scanners and licence tools understand. You do not need any of them to get value from it - a template query lists every package and version in the image, which is exactly what you need when comparing against an advisory.

    bash Example session
    docker buildx imagetools inspect localhost:5000/cg-ci:1 --format "{{ range .SBOM.SPDX.packages }}{{ .name }}@{{ .versionInfo }}{{ println }}{{ end }}" | head -8alpine-baselayout@3.7.0-r0alpine-baselayout-data@3.7.0-r0alpine-keys@2.5-r0alpine-release@3.22.5-r0apk-tools@2.14.10-r0brotli-libs@1.1.0-r2busybox@1.37.0-r20busybox-binsh@1.37.0-r20

    Expected resultA package list with versions, and a count.

    Verify it worked

    bash Example session
    docker buildx imagetools inspect localhost:5000/cg-ci:1 --format "{{ json .SBOM.SPDX.packages }}" | grep -c SPDXID26

    Success conditionTwenty-six packages from a two-line Dockerfile. That gap between what you wrote and what you shipped is the reason the SBOM exists.

  5. The provenance record

    Provenance answers a different question: not what is inside, but how it got made. It follows the SLSA schema and records the build type, the source of the Dockerfile and the parameters used. Note the nesting - buildType sits under buildDefinition, and a template that guesses the path returns nothing rather than erroring.

    bash Example session
    docker buildx imagetools inspect localhost:5000/cg-ci:1 --format "{{ json .Provenance }}" | head -c 300{  "SLSA": {    "buildDefinition": {      "buildType": "https://github.com/moby/buildkit/blob/master/docs/attestations/slsa-definitions.md",      "externalParameters": {        "configSource": {          "path": "Dockerfile"

    Expected resultA SLSA document naming the build type and the Dockerfile it came from.

    Success conditionYou can see how the image was produced. In CI this records the repository, commit and workflow, which is what makes a build auditable.

  6. Scanning is a separate tool, and this host has none

    An SBOM lists packages; it does not tell you which are vulnerable. That comparison against a vulnerability database is a scanner's job - Docker Scout, Trivy, Grype and others all consume the SBOM you just produced. Scout is a CLI plugin and is not installed everywhere, as this host demonstrates.

    bash
    docker scout versiondocker: unknown command: docker scout# not installed here - the SBOM above is still complete and portable# any SPDX-consuming scanner can read it, which is the advantage of a standard format

    Expected resultThe plugin missing on a stock Engine install.

    Success conditionYou understand the split. Generating the SBOM is a build concern and costs nothing; scanning it is a separate tool you choose.

  7. Reduce what there is to scan

    The cheapest supply-chain improvement is having less in the image. Every package in that list of 26 is something that can have a CVE. A multi-stage build that ships only a binary has almost nothing to report - which is a security property, not just a size one.

    bash Example session
    docker images --filter reference=cg-multi --format "table {{.Repository}}:{{.Tag}}\t{{.Size}}"REPOSITORY:TAG     SIZEcg-multi:1         112kBcg-multi:builder   253MB# the 112kB image has no shell, no package manager and no packages to enumerate

    Expected resultThe runtime image orders of magnitude smaller than the builder.

    Success conditionYou can connect image size to attack surface. See guide 19 for how that image was produced.

Troubleshooting

Official sources