Build and supply chain

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.

Updated Houssam Hammoudi, CTOTested with syft 1.52.0, cosign v3.0.6, registry:2

On this page
  1. What goes wrong
  2. What the docs say
  3. The secure configuration
  4. Prove it
  5. Mistakes people make
  6. Checklist

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.

bash
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:

bash
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:

bash
syft registry:5000/team-a/app@sha256:... -o spdx-json=sbom.spdx.json -o cyclonedx-json=sbom.cdx.json
text
sbom.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:

bash
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:...
text
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 key

Decoded from the verified envelope:

text
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:

bash
cosign verify-attestation --key other.pub --type spdxjson --insecure-ignore-tlog=true registry:5000/team-a/app@sha256:...
text
Error: no matching attestations: failed to verify signature: could not verify envelope: accepted signatures do not match threshold, Found: 0, Expected 1

As 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 attest as an in-toto attestation, signed by the build identity.
  • Consumers run cosign verify-attestation with 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 free

H2 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