Inspecting Multi-Platform Docker Images, SBOMs, and SLSA Provenance With Buildx

  • Thread starter Thread starter Sangharsh Agarwal
  • Start date Start date
S

Sangharsh Agarwal

Guest
An image tag is convenient for deployment, but it does not tell me which architectures are inside the image or what went into building them. For a small Go service, I used Docker Buildx to publish an image for linux/amd64 and linux/arm64, attaching a software bill of materials (SBOM) and build provenance to each platform. Then I inspected the result in the registry. I wanted to answer a concrete question: if a build says it produced an SBOM and provenance, what can I verify from the image that was actually published?

The example repository contains a small HTTP service, a multi-stage Dockerfile, and a docker-bake.hcl file. The Dockerfile builds a Go binary for the requested target architecture and copies it into an Alpine runtime image. The Bake configuration defines both target platforms and requests SBOM and minimal provenance attestations:

Code:
target "image" {
  context    = "."
  dockerfile = "Dockerfile"
  tags       = [IMAGE]
  platforms  = ["linux/amd64", "linux/arm64"]
  attest     = ["type=sbom", "type=provenance,mode=min"]
}

A separate publish target inherits this configuration and sends the result to the registry:

Code:
target "publish" {
  inherits = ["image"]
  output   = ["type=registry"]
}

The publish target inherits the platforms and attestations from image and adds a registry output. A separate local target builds a host-platform image for testing. GitHub Actions runs the build on a push to main, using QEMU and Buildx for the two architectures. Pull requests build and smoke-test the host-platform image without publishing it. The published image is tagged with the commit SHA. The successful first workflow run published this image:

Code:
ghcr.io/sangharshcs/buildx-attest-demo:b3b80496f97ddeead517d430d00c60686c3186a3

Start With the Image Index​


I set IMAGE to that tag and inspected the published result:

Code:
export IMAGE="ghcr.io/sangharshcs/buildx-attest-demo:b3b80496f97ddeead517d430d00c60686c3186a3"
docker buildx imagetools inspect "$IMAGE"

The output showed an OCI image index with digest sha256:586591bacfbed1313559919bade7aefecce51ff60355f688dfded857f7a6d30d. Under it were separate runnable manifests for linux/amd64 and linux/arm64. The index also contained two unknown/unknown entries annotated as attestation manifests, each pointing to one of the platform images. Those entries are metadata, not failed or extra runnable platforms.

The index digest is useful because it identifies the precise published index inspected here. A tag can later point somewhere else, so a reader reproducing the inspection should record the digest of their own build. This is also why I inspected the registry result after publishing instead of relying only on the build configuration to tell me what should have happened.

Read the SBOMs Per Platform​


An SBOM can be inspected from the registry without running either image. To extract both platform SBOMs:

Code:
docker buildx imagetools inspect "$IMAGE" \
  --format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sbom-amd64.json
docker buildx imagetools inspect "$IMAGE" \
  --format '{{ json (index .SBOM "linux/arm64").SPDX }}' > sbom-arm64.json

I then extracted and sorted each SBOM's package names and versions:

Code:
jq -r '.packages[]? | "\(.name)@\(.versionInfo // "")"' \
  sbom-amd64.json | sort > amd64-packages.txt
jq -r '.packages[]? | "\(.name)@\(.versionInfo // "")"' \
  sbom-arm64.json | sort > arm64-packages.txt
diff -u amd64-packages.txt arm64-packages.txt

Both lists contained 19 packages, and diff -u produced no differences. That is a narrow observation: the reported package names and versions matched in this build. It does not imply that the full SBOMs or platform-specific binaries have identical content, and it is not a guarantee Buildx or the SBOM attestation makes for any other build. In particular, this comparison does not compare package checksums, architecture-specific binaries, or anything the scanner did not inventory.

This check is useful in a release pipeline. It lets a team notice when the package inventory unexpectedly differs by architecture, while keeping the evidence tied to the actual published image. An SBOM is an inventory generated by a scanner; it does not, by itself, establish that the inventoried packages are safe.

Check What Provenance Recorded​


The same inspection command can export the build provenance:

Code:
docker buildx imagetools inspect "$IMAGE" \
  --format '{{ json .Provenance }}' > provenance.json

The resulting JSON contained separate linux/amd64 and linux/arm64 entries. For amd64, the SLSA-format record contained buildDefinition and runDetails. I extracted the fields I wanted to examine:

Code:
jq '."linux/amd64".SLSA | {
  buildType: .buildDefinition.buildType,
  builder: .runDetails.builder.id,
  dependencies: .buildDefinition.resolvedDependencies
}' provenance.json

Its resolved dependencies included the BuildKit SBOM scanner, alpine:3.22, and golang:1.25-alpine, each with a SHA-256 digest for the amd64 build. Those digests are more specific than the tags in the Dockerfile: they record which artifacts were resolved for this run. An operator investigating a release can use those records to identify the resolved inputs instead of guessing from the tags in source control.

There is an important limit to this example. The inspected runDetails.builder.id was an empty string. I would describe the result as attached build provenance recording dependencies, not as independent proof of the GitHub Actions runner's identity or of a particular SLSA level. The workflow checked that provenance existed; it did not perform a separate signature or trust-policy verification. Reading metadata and deciding whether to trust it are different tasks.

What I Would Carry Into a Production Pipeline​


The exercise gave me three concrete checks to keep: inspect the published index for both architectures, inspect each platform's SBOM, and inspect the provenance for resolved dependencies. For a production release, I would add an explicit verification policy for the expected builder and source, plus a process for reviewing newly introduced packages and vulnerabilities. Those are additional controls, not properties granted automatically by adding attest to Bake.

The practical lesson is to keep the claim as precise as the evidence. In this run, one image index contained two platform images; both platform SBOMs listed the same 19 package names and versions; and the provenance recorded three resolved dependencies for amd64. Each observation came from the published artifact, where a reader can check it independently. That is a stronger starting point for a release discussion than assuming a green build means every platform has the same contents or that an attached attestation settles the trust question.

Docker's documentation has more detail on Buildx Bake, SBOM inspection, and provenance formats.]

Author’s note: This article, based on my own repository and the command outputs I generated, is AI-assisted. I verified the technical claims and results before publishing.
 

Thread statistics

Created
Sangharsh Agarwal,
Replies
0
Views
11
Back
Top