SBOMs, SPDX/CycloneDX, in-toto Attestations, SLSA Provenance, Signatures, and Verification Workflows: Guided Hands-On Workflow and Core Operations
Build a disposable image with SBOM and provenance attestations, inspect the evidence, sign the exact digest with a local test key, verify it, and demonstrate a deliberate subject mismatch.
Learning objectives
- Create a disposable local registry and isolated Buildx builder without changing daemon-wide registry or security configuration.
- Build a synthetic image with BuildKit SBOM and SLSA provenance attestations and capture the resulting immutable digest.
-
Inspect SBOM and provenance directly from the registry using
imagetools inspect. - Generate a throwaway Cosign keypair, sign the digest, and verify it with the matching public key.
- Demonstrate a safe failing verification when the subject digest changes.
1. Lab boundary and preflight
This lab uses three disposable resources: a loopback-only OCI
Distribution registry named dca30-registry, a Buildx
docker-container builder named
dca30-builder, and a synthetic image repository
localhost:5005/dca30/app. The mandatory path does not
expose a production registry, does not mount the Docker socket into
an untrusted container, and does not change daemon JSON.
127.0.0.1 and intentionally lacks
TLS/authentication. Keep it local and disposable. Do not copy this
topology to a shared host or production network.
set -eu
mkdir -p dca30-lab && cd dca30-lab
# Evidence-first preflight: record what actually exists on this host.
docker version
docker info
docker context show
docker buildx version
docker buildx ls
docker compose version || true
# The registry is disposable and loopback-only. Pin the human-readable version.
docker pull registry:3.1.1
docker run -d --name dca30-registry -p 127.0.0.1:5005:5000 --label devops.academy.chapter=30 registry:3.1.1
# Isolated builder: does not mutate daemon config.
docker buildx create --name dca30-builder --driver docker-container --use
docker buildx inspect --bootstrap
2. Create deterministic synthetic source
The image contains only a shell script and an Alpine base. The point is evidence mechanics, not application complexity. We record source content before the build so the evidence packet can later prove which input changed.
cat > app.sh <<'EOF'
#!/bin/sh
printf '%s
' 'dca30 synthetic app v1'
EOF
chmod +x app.sh
cat > Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.22.1
COPY --chmod=0555 app.sh /usr/local/bin/dca30-app
USER 65532:65532
ENTRYPOINT ["/usr/local/bin/dca30-app"]
EOF
sha256sum Dockerfile app.sh | tee source.sha256
The base is pinned to a version tag for readability, while the build metadata/provenance will record the resolved base material digest. In a production release definition, Chapter 13’s digest-pinning pattern is preferable when you require byte-for-byte base identity.
3. Build and push with SBOM + provenance
Attestations attach to an image index. The
docker-container driver avoids dependence on whether
the local daemon still uses the classic image store. We push
directly to the disposable registry so the registry retains the
image index and attestation manifests.
IMAGE=localhost:5005/dca30/app:1
docker buildx build \
--builder dca30-builder \
--progress=plain \
--sbom=true \
--provenance=mode=max \
--output type=registry,registry.insecure=true \
--metadata-file build-metadata.json -t "$IMAGE" .
# Preserve immutable identity from registry metadata.
docker buildx imagetools inspect "$IMAGE"
docker buildx imagetools inspect "$IMAGE" --format '{{json .Manifest}}' > manifest.json
cat build-metadata.json
--sbom=true asks BuildKit for an SBOM attestation.
--provenance=mode=max requests richer provenance than
the default minimal mode.
registry.insecure=true applies only to this build
output; it does not weaken the daemon globally.
--metadata-file captures build-result identity without
scraping terminal text.
4. Resolve the subject digest before signing
Do not sign the tag. Resolve the image/index digest first and freeze
it in the evidence packet. The exact JSON shape available to
imagetools is version-sensitive, so this workflow
records both human-readable inspect output and raw manifest JSON.
DIGEST=$(docker buildx imagetools inspect "$IMAGE" --format '{{.Manifest.Digest}}')
SUBJECT="localhost:5005/dca30/app@${DIGEST}"
printf 'subject=%s
' "$SUBJECT" | tee subject.txt
docker buildx imagetools inspect "$SUBJECT" --raw > subject-index.json
sha256sum subject-index.json source.sha256 build-metadata.json | tee evidence-files.sha256
.Manifest.Digest in the template you expect:
preserve the failure, inspect
docker buildx imagetools inspect "$IMAGE" and
--raw, then extract the registry-reported digest with a
supported field/tool. Do not silently substitute a local image ID.
5. Extract and read SBOM/provenance
Inspection is read-only. The important comparison is not “there is JSON”; it is whether the evidence’s subject and materials correspond to the subject you recorded.
docker buildx imagetools inspect "$SUBJECT" --format '{{json .SBOM}}' > sbom.json
docker buildx imagetools inspect "$SUBJECT" --format '{{json .Provenance}}' > provenance.json
# Preserve hashes before opening/editing evidence.
sha256sum sbom.json provenance.json | tee attestation-files.sha256
# Optional if jq is already installed:
jq '.SPDX.SPDXID, (.SPDX.packages // [] | length)' sbom.json || true
jq '.SLSA.buildType, .SLSA.builder, .SLSA.materials' provenance.json || true
BuildKit’s native SBOM is SPDX-based. If your organization needs CycloneDX, generate a separate CycloneDX BOM with a reviewed tool and bind that output to the same subject digest; do not relabel an SPDX document as CycloneDX.
6. Install or verify Cosign 3.1.3 safely
The course does not execute a remote installer script. Obtain Cosign
from the official Sigstore release channel for your platform, verify
the release asset as documented upstream, then record
cosign version. The chapter baseline is 3.1.3 because
that release fixes a legacy-bundle verification bypass.
cosign version
# Expected course baseline: v3.1.3 or a newer reviewed release.
If Cosign is not installed, you can still complete the BuildKit SBOM/provenance sections. Treat signature steps as blocked rather than replacing Cosign with an unreviewed binary.
7. Generate a throwaway local key and sign the digest
A local key demonstrates cryptographic subject binding without OIDC or cloud services. Use only fake/disposable key material. Cosign prompts for a password; choose a lab-only value and never reuse it. Keep the private key outside source control.
mkdir -m 700 -p keys
(
cd keys
cosign generate-key-pair
)
# Sign exactly the immutable digest reference. HTTP registry is testing-only.
cosign sign --yes --key keys/cosign.key --allow-http-registry --tlog-upload=false "$SUBJECT"
cosign verify --key keys/cosign.pub --allow-http-registry --insecure-ignore-tlog=true "$SUBJECT" | tee signature-verification.json
--tlog-upload=false deliberately keeps the local-key
exercise out of the public transparency log.
--insecure-ignore-tlog=true tells verification that
this test signature is not expected to have transparency-log
evidence. In a production keyless flow, define and verify the
certificate identity/OIDC issuer and transparency evidence instead
of copying these local-test flags.
8. Demonstrate subject mismatch instead of merely describing it
Change the source, produce a second digest, then intentionally verify the old signature expectation against the new subject. Preserve the failure output. The failure is the evidence that digest binding works.
printf '%s
' '# v2 evidence change' >> app.sh
IMAGE2=localhost:5005/dca30/app:2
docker buildx build --builder dca30-builder --sbom=true --provenance=mode=max --output type=registry,registry.insecure=true -t "$IMAGE2" .
DIGEST2=$(docker buildx imagetools inspect "$IMAGE2" --format '{{.Manifest.Digest}}')
SUBJECT2="localhost:5005/dca30/app@${DIGEST2}"
printf 'old=%s
new=%s
' "$SUBJECT" "$SUBJECT2" | tee subject-comparison.txt
# Expected to fail because SUBJECT2 has not been signed by this key.
set +e
cosign verify --key keys/cosign.pub --allow-http-registry --insecure-ignore-tlog=true "$SUBJECT2" 2>&1 | tee expected-verification-failure.txt
VERIFY_RC=${PIPESTATUS[0]}
set -e
printf 'expected_failure_exit=%s
' "$VERIFY_RC"
Do not “fix” this by signing a tag or disabling claim checks. The correct response is to decide whether the new digest should be promoted, generate/verify the required evidence for that digest, and update policy intentionally.
9. Cleanup is exact and bounded
Preserve the evidence files you want to study, then remove only resources created by this lab. There is no global prune.
docker buildx use default || true
docker buildx rm dca30-builder
docker rm -f dca30-registry
# Remove only disposable key material when you are done reviewing it.
rm -f keys/cosign.key keys/cosign.pub
# Keep or delete dca30-lab evidence files according to your lab retention decision.
10. Small challenge: choose the failing layer
Your SBOM extracts correctly, the signature verifies, but policy
says the provenance must name an approved builder and the observed
builder.id is empty or unexpected. Which layer should
you change first?
Answer after investigation: neither the runtime container nor the signature mechanism. First inspect the provenance generation/build platform and policy expectation. A valid signature over an image does not repair an insufficient builder claim.
Knowledge check
Why does the lab use a docker-container builder
and direct registry output?
Attestations require an image index. A docker-container builder can produce/push attestations regardless of whether the local Engine still uses the classic image store.
Why is registry.insecure=true preferable here to
changing daemon-wide insecure-registry configuration?
It scopes the exception to this disposable build output and avoids weakening unrelated registry traffic.
What proves that the signature is bound to a specific artifact?
The verified signature claims/reference cover the immutable subject digest, and verification fails for an unsigned changed digest.
Why is --tlog-upload=false acceptable in this lab
but not a universal production recommendation?
This lab demonstrates local-key cryptography without public infrastructure. Production policy may require transparency-log evidence, keyless identities, or KMS/HSM controls.
The second image has a valid SBOM and provenance but no matching signature. What is the correct interpretation?
Some evidence exists for the new digest, but the signature policy is unsatisfied. Do not inherit the first digest’s signature.
Official references and version notes
Lab assumptions: Linux containers; localhost TCP
port 5005 free; Docker can pull registry:3.1.1; Buildx
supports the docker-container driver; Cosign 3.1.3 is
installed from an official/reviewed source for signing sections.
Commands use Bash semantics such as PIPESTATUS. On
Windows, run the lab in WSL/Git Bash or translate shell-only
evidence-capture syntax without changing Docker semantics.
- Docker Docs — Build attestations — current BuildKit SBOM/provenance model and image-store requirements.
-
Docker Docs — SBOM attestations
— SPDX/in-toto output and
imagetools inspectworkflow. -
Docker Docs — Provenance attestations
—
mode=min|maxand optional SLSA provenance v1. - Docker Docs — buildx imagetools inspect — registry manifest, SBOM, and provenance inspection.
- Docker Engine 29 release notes — Engine 29 compatibility baseline and removal of Docker Content Trust from the CLI.
-
OCI — Image and Distribution Specifications 1.1
— artifact
subject,artifactType, and Referrers API. - SPDX Specification 3.0.1 — current SPDX specification reference.
- CycloneDX Specification Overview — current CycloneDX 1.7 object model and media types.
- SLSA Specification 1.2 — current SLSA model and provenance guidance.
- Sigstore Cosign v3.1.3 — pinned local signing/verifying tool used as the 2026-09-22 lesson baseline.
-
Docker Official Image — registry
— disposable local registry; lesson baseline pins
registry:3.1.1.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.