Chapter 30Lesson 02~180 minutes

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.

BuildKitSPDXSLSACosignDigest identity

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.

Testing-only HTTP registry. The registry is bound to 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
If your Buildx version does not expose .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.

Next

Turn mechanics into design choices

Lesson 3 compares evidence formats, provenance depth, key models, storage choices, and policy scope.

Knowledge check

Why does the lab use a docker-container builder and direct registry output?

Why is registry.insecure=true preferable here to changing daemon-wide insecure-registry configuration?

What proves that the signature is bound to a specific artifact?

Why is --tlog-upload=false acceptable in this lab but not a universal production recommendation?

The second image has a valid SBOM and provenance but no matching signature. What is the correct interpretation?

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.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.