SBOMs and attestations with Syft and cosign, the secure way
An SBOM in a shared folder is a list someone typed, as far as anyone can prove. A signed attestation says which image it describes, who made it, and that nobody edited it since. The difference matters on the day you need to answer "do we ship this library?" in ten minutes.
The short answer
Generate the SBOM with Syft from the image digest your build pushed, not from the source tree or a tag. Attach it with cosign attest as an in-toto attestation signed by the build key, and have consumers run cosign verify-attestation with the expected key and predicate type before reading a single package from it.
On this page
What goes wrong
Teams generate SBOMs because a customer or a regulation asks for them, and then store them where nobody can tell what they describe:
- The SBOM was generated from the source tree, so it lists what the lockfile says, not what the image contains (base image packages are missing).
- It was generated from a tag, which pointed to a different digest by the time the image shipped.
- It sits next to the image as a plain file that anyone with write access can edit, so it proves nothing.
An attestation fixes the last two. It binds the SBOM to one image digest and signs the pair. Generating it from the pushed image fixes the first.
What the docs say
An in-toto attestation is authenticated metadata about one or more software artifacts
Source: in-toto attestation framework, specification
Note that there are also SBOM predicate types, but they are not recommended. Using a SBOM predicate type will result in the entire SBOM being included in the signature bundle, and so you will have to download the entire SBOM every time you want to verify the signature on the container.
Source: Sigstore docs, Signing other types
To be safe, your systems should be designed to fail closed rather than open.
Source: Sigstore docs, Verifying attestations
Syft's README lists output formats and describes syft attest, which the
Anchore docs mark experimental ("This feature is experimental and may change
in future releases."). This page uses Syft only to generate the SBOM and
cosign to attest it. On size: the SBOM below is 133 KB, which is fine to
embed. If yours are megabytes, follow Sigstore's advice and attest a small
predicate that points to an SBOM stored elsewhere by digest.
The secure configuration
Run this in the release job, right after the image is pushed, using the digest the build recorded.
IMAGE=registry.example.com/team-a/app
DIGEST=$(cat out/app.digest) # from kaniko --digest-file
# 1. SBOM of the exact pushed image, in both common formats.
syft "registry:${IMAGE}@${DIGEST}" \
-o spdx-json=sbom.spdx.json \
-o cyclonedx-json=sbom.cdx.json
# 2. Attach it as a signed in-toto attestation, bound to that digest.
cosign attest --yes \
--key cosign.key \
--signing-config signing-config.json \
--type spdxjson \
--predicate sbom.spdx.json \
"${IMAGE}@${DIGEST}"For keyless signing, drop --key and --signing-config; for key-based
signing without a public log, see the offline signing page.
Consumers verify before they read:
cosign verify-attestation \
--key cosign.pub \
--type spdxjson \
--insecure-ignore-tlog=true \
"${IMAGE}@${DIGEST}" > attestation.json # non-zero exit: stop, do not read
# The SBOM is base64 inside the DSSE envelope:
jq -r '.payload' attestation.json | base64 -d | jq '.predicate.packages[] | "\(.name) \(.versionInfo)"'In Kubernetes, the policy-controller can require the attestation at
admission (an attestations block with predicateType: spdxjson under the
authority), so an image without a signed SBOM does not start.
Prove it
Generate both formats from the image in the registry:
syft registry:5000/team-a/app@sha256:... -o spdx-json=sbom.spdx.json -o cyclonedx-json=sbom.cdx.jsonsbom.spdx.json SPDX-2.3 22 packages 133038 bytes
sbom.cdx.json CycloneDX 1.7 116 components, 94 of them files
e.g. apk-tools 2.14.10-r14; busybox 1.38.0-r2; glibc-2.44 2.44-r6; libssl3 3.6.4-r8(The summary lines come from a small script that reads the two files.) The base image's packages are there because Syft read the image, not the source.
Attest and verify:
cosign attest --key cosign.key --type spdxjson --predicate sbom.spdx.json registry:5000/team-a/app@sha256:...
cosign verify-attestation --key cosign.pub --type spdxjson --insecure-ignore-tlog=true registry:5000/team-a/app@sha256:...Using payload from: sbom.spdx.json
Signing artifact...
WARNING: Skipping tlog verification is an insecure practice that lacks transparency and auditability verification for the attestation.
Verification for registry:5000/team-a/app@sha256:144f35efa46bcceac6d302b0673835f7be8ec1375fb9beecee976a92353ded4e --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The signatures were verified against the specified public keyDecoded from the verified envelope:
payloadType: application/vnd.in-toto+json
predicateType: https://spdx.dev/Document
subject: registry:5000/team-a/app sha256:144f35efa46b...
packages in the signed SBOM: 22
busybox in the signed SBOM: ['1.38.0-r2']With a key that did not sign it, verification fails and there is nothing to read:
cosign verify-attestation --key other.pub --type spdxjson --insecure-ignore-tlog=true registry:5000/team-a/app@sha256:...Error: no matching attestations: failed to verify signature: could not verify envelope: accepted signatures do not match threshold, Found: 0, Expected 1As on the offline signing page, cosign lists "Existence of the claims in the transparency log was verified offline" even with the log skipped; the warning line above it is the accurate one.
Mistakes people make
SBOM from the source tree
syft dir:. lists your application dependencies and misses every package in
the base image, which is where most findings live. Scan the pushed image by
digest.
Reading the SBOM without verifying it
cosign download attestation returns whatever is attached, signed or not.
Always run verify-attestation with the expected key or identity, and fail
closed if it errors.
Attesting a tag
cosign attest ... app:1.0.0 resolves the tag at that moment. Attest the
digest from the build, so the SBOM and the image cannot drift apart.
Picking one format and forcing everyone to convert
Customers ask for SPDX or CycloneDX. Syft writes both in one run; attest the one your tools verify, and publish the other alongside.
SBOMs nobody queries
An SBOM earns its keep when a new CVE lands. Feed the verified SBOMs into a store you can search ("which running digests contain library X?"), and test that search before you need it.
Checklist
- SBOMs are generated from the pushed image digest, not from source or a tag.
- Both SPDX and CycloneDX are produced in the release job.
- The SBOM is attached with
cosign attestas an in-toto attestation, signed by the build identity. - Consumers run
cosign verify-attestationwith the expected key or identity and predicate type. - Verification failures stop the process; nothing reads unverified SBOMs.
- Admission policy requires the SBOM attestation where you need proof at deploy time.
- Large SBOMs are stored by digest and referenced from a small attested predicate.
- Verified SBOMs are indexed so you can search by package and version.
An unsigned SBOM is a list. A signed attestation on the right digest is evidence, and evidence is what people ask for at three in the morning.
H2-CSDE
Learn it on a live range
Signing, SBOM and attestation, in DevSecOps and Supply Chain: a real host in your browser, and every objective checked on the machine.
Start freeH2 Scanner
Want this caught before it merges?
The H2 Scanner runs in your CI and flags the weaknesses pages like this one warn about, on every pull request.
Talk to us