DevSecOps Part 5 of 5 · Software Supply Chain Security
Attestations and Provenance: Binding the SBOM to the Image
Turn the SBOM from part 2 into a Cosign attestation bound to the image digest, verify it and read the predicate back, list what hangs off an image with cosign tree, and see what SLSA build provenance adds.
On this page
Overview
Part 2 left the SBOM as a file next to the image. Part 4 signed the image. Neither connects the two: a verifier
who has the digest still has to trust that ci-demo-1.4.2.cdx.json describes it and has not been edited. An
attestation closes that gap. It is a signed statement whose subject is the image digest and whose predicate
is a document about it, the SBOM here, stored in the registry beside the image, verifiable with the same key.
The attestation section was run end to end against the local registry from part 4 with Cosign 3.1.3. The provenance section describes what a CI platform generates and how it is verified; it was not executed here, because build provenance is produced by the build platform, not by a command you run afterwards, and this site’s pipeline does not currently generate it.
Prerequisites
Attest the SBOM
cosign attest wraps a predicate document in an in-toto statement,
sets the subject to the image digest, signs the whole thing and pushes it to the registry as a referrer of the
image. --type cyclonedx sets the predicate type so verifiers know how to read it.
DIGEST=sha256:5743684a366b5d6e91df18c41f9f3a9ceab8452d62b2c3fea07015552ce1b636
cosign attest --key cosign.key --signing-config signing-config.json --allow-http-registry \
--type cyclonedx --predicate sbom.cdx.json "registry:5000/ci-demo@$DIGEST"
Using payload from: sbom.cdx.json
Signing artifact...
The SBOM is now in the registry, addressed by the image digest, and cannot be altered without invalidating a signature. It is also exactly the SBOM from part 3, which the scan gate ran on, so the scan result and the deployed image now refer to the same inventory.
Verify and read it back
verify-attestation checks the signature, checks that the subject is the digest you asked about, and prints the
signed envelope. The predicate is base64 inside it; decode and read:
cosign verify-attestation --key cosign.pub --allow-http-registry --insecure-ignore-tlog \
--type cyclonedx "registry:5000/ci-demo@$DIGEST" \
| python3 -c '
import json, sys, base64
env = json.loads(sys.stdin.readline())
stmt = json.loads(base64.b64decode(env["payload"]))
print("payloadType:", env["payloadType"])
print("predicateType:", stmt["predicateType"])
print("subject:", stmt["subject"][0]["name"], stmt["subject"][0]["digest"])
print("predicate bomFormat:", stmt["predicate"]["bomFormat"], "components:", len(stmt["predicate"]["components"]))
'
payloadType: application/vnd.in-toto+json
predicateType: https://cyclonedx.org/bom
subject: registry:5000/ci-demo {'sha256': '5743684a366b5d6e91df18c41f9f3a9ceab8452d62b2c3fea07015552ce1b636'}
predicate bomFormat: CycloneDX components: 105
Four facts, each of which a verifier can now rely on: the envelope is an in-toto statement, the predicate is a CycloneDX BOM, the subject is this digest and no other, and the BOM inside is the 105-component inventory of the hardened image (22 packages plus the file evidence Syft records). Feed the decoded predicate to Grype and you have rescanned the deployed image from the registry alone, without pulling it.
--type matters on the verify side too: an image can carry several attestations (an SBOM, a scan result, a
provenance statement), and asking for cyclonedx returns only the ones that claim to be one.
What hangs off the image now
cosign tree --allow-http-registry "registry:5000/ci-demo@$DIGEST"
📦 Supply Chain Security Related artifacts for an image: registry:5000/ci-demo@sha256:5743684a…
└── 🔗 https://cyclonedx.org/bom artifacts via OCI referrer: registry:5000/ci-demo@sha256:0e2ac577…
└── 🍒 sha256:e23892c3…
└── 🔗 https://sigstore.dev/cosign/sign/v1 artifacts via OCI referrer: registry:5000/ci-demo@sha256:77f56bdd…
└── 🍒 sha256:a6d9f7e8…
One signature, one SBOM attestation, both stored as OCI referrers of the image manifest. A registry that
supports the referrers API (Distribution 2.8+, most managed registries) keeps them with the image through copies
and promotions; cosign copy moves image, signature and attestations together.
Build provenance
The SBOM says what is in the image. Provenance says how it was made: which builder, from which source revision,
with which parameters, producing which digest. The standard predicate is SLSA
provenance, and the property that makes it worth having is who
generates it. An attestation the build script signs proves only that the script ran; provenance generated by
the build platform (GitLab CI’s artifact attestations, GitHub’s attest-build-provenance, or a SLSA generator
running outside the job) proves the platform saw the job build that digest from that commit, which is the SLSA
level-2 and level-3 distinction from part 1.
Verifying provenance is the same command with a different type, plus a policy on the contents:
# Configuration only: requires provenance generated by a build platform, which this lab did not produce.
cosign verify-attestation --type slsaprovenance1 \
--certificate-identity-regexp '^https://gitlab.com/example-group/ci-demo//' \
--certificate-oidc-issuer https://gitlab.com \
registry.example.com/ci-demo@sha256:…
The predicate’s buildDefinition.externalParameters then names the repository and ref; a deploy policy can
require that the builder is your CI project and the source is your protected branch before the image runs.
GitLab generates SLSA provenance for job artifacts when RUNNER_GENERATE_ARTIFACTS_METADATA is set on the
runner; attaching it to the image as a Cosign attestation is a cosign attest --type slsaprovenance1 --predicate
step in the same job. None of this was executed here.
Placing it in the pipeline
The whole path, as pipeline stages, in the order the artifacts are produced:
build: # docker build; push by tag; resolve and export DIGEST
sbom: # syft registry:$IMAGE@$DIGEST -o cyclonedx-json=sbom.cdx.json
scan: # grype sbom:sbom.cdx.json --fail-on medium --only-fixed
sign: # cosign sign $IMAGE@$DIGEST (keyless with id_tokens, or a KMS key)
attest: # cosign attest --type cyclonedx --predicate sbom.cdx.json $IMAGE@$DIGEST
deploy: # cosign verify + verify-attestation --type cyclonedx, then deploy $IMAGE@$DIGEST
DIGEST is resolved once, after the push, and every later stage uses it. Signing happens after the scan passes,
so a signature means “this digest passed the gate on this date”, which is a stronger claim than “this digest
exists”. The DevSecOps Pipeline path has the gate stages; this path adds the last
three.
Security Considerations
- Attestations are readable by anyone who can pull the image, and the SBOM is a map of vulnerable versions. Keep registry access as tight as the image warrants.
verify-attestationwith a key checks the signature, not the content. Policy on the content (a maximum severity in a scan attestation, an allowed builder in provenance) is a second step, in the deploy job or in an admission policy engine.- Removing an attestation from the registry leaves the image valid. A deploy policy that requires an SBOM attestation of a given type is what makes its absence a failure.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
verify-attestation finds nothing | Wrong --type, or the attestation was made for another digest | Match the type used at attest time; cosign tree lists what exists |
| Predicate is empty or the wrong document | --predicate pointed at the wrong file | Re-attest; attestations are additive, so also filter by type when verifying |
| Attestations disappear after copying the image | The copy tool ignored referrers | Use cosign copy, or a tool that copies referrers |
| Registry rejects the referrer push | Registry without OCI referrers/tag-fallback support | Upgrade the registry, or let Cosign fall back to the sha256-… tag scheme |
Conclusion
The image now carries its own evidence: a signature that says who vouched for the digest, and an attestation that binds the inventory the scan gate read to that same digest. A verifier with the public key and the digest can check both without trusting the pipeline’s word for it. That closes the chain from part 1: source and dependencies locked, build pinned, image inventoried and scanned, digest signed and attested, deployment verifying before it runs. The existing Trivy article and the SBOM lab are the places to practise the scanning half with a second toolchain.
References
Keep reading