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
The example repository contains a small HTTP service, a multi-stage Dockerfile, and a
A separate
The
I set
The output showed an OCI image index with digest
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.
An SBOM can be inspected from the registry without running either image. To extract both platform SBOMs:
I then extracted and sorted each SBOM's package names and versions:
Both lists contained 19 packages, and
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.
The same inspection command can export the build provenance:
The resulting JSON contained separate
Its resolved dependencies included the BuildKit SBOM scanner,
There is an important limit to this example. The inspected
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
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.
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.