SBOM, build provenance, and signing: prove what you ship
An SBOM inventories every dependency so you answer "are we affected by CVE-X?" in minutes; provenance and SLSA prove who built the artifact and how; cosign keyless signing plus an attestation, verified at deploy, is what turns all of it from paperwork into a gate.
A CVE drops on a Friday: a backdoor in a widely-used compression library, the kind that activates only inside the build. Leadership asks the only question that matters — “are we shipping the affected version?” Your team spends the weekend grepping lockfiles across forty repos and ssh-ing into running containers to check what’s actually installed, because nobody can answer from memory. The team that had an SBOM for every released image answered in eight minutes with one query. The difference was not talent. It was a machine-readable inventory generated at build time, and the discipline to keep it.
SBOM: a machine-readable inventory you query, not a PDF you file
When the next xz-utils lands, you want to answer “are we affected?” in one query, not a weekend of grepping. That requires the SBOM to already exist, stored and queryable, for every released image — which means generating it at build time without thinking about it.
A Software Bill of Materials is a complete, machine-readable list of every component in an artifact — direct and transitive dependencies, versions, licenses, and the hashes that pin them. The two formats that matter are CycloneDX (OWASP, tuned for security and vulnerability correlation) and SPDX (Linux Foundation / ISO 5962, the natural fit for license and compliance reporting). You do not write an SBOM by hand. You generate it in CI from the built artifact with a tool like syft or cdxgen, which walks the image’s package databases and language manifests and emits the inventory.
The payoff is the Friday-night question answered in minutes. When a CVE lands, you do not grep forty repos — you query the SBOMs you already stored for every release and get back the exact set of images carrying the affected version and range. Pair syft with a scanner like grype and the SBOM becomes a standing CVE-correlation surface: the inventory is generated once, and every new advisory is checked against it automatically. The xz-utils backdoor (CVE-2024-3094) is the canonical motivation — a build-time injection into liblzma staged over two years — and the industry lesson was blunt: you cannot defend a dependency tree you cannot see, especially the transitive single-maintainer packages nobody remembers adding.
# Generate an SBOM from the built image, in CycloneDX JSON
syft your-registry/app:1.4.2 -o cyclonedx-json=sbom.cdx.json
# Later, when CVE-2024-3094 drops — answer "are we affected?" from the SBOM
grype sbom:sbom.cdx.json --fail-on highProvenance and SLSA: proving how the artifact was built
An SBOM says what is inside. Build provenance is the verifiable record of how the artifact came to be: which builder produced it, from which source commit, with which build parameters and inputs. The framework that grades this is SLSA (Supply-chain Levels for Software Artifacts), whose Build track runs L0 to L3:
| Level | What it guarantees | What you do to reach it |
|---|---|---|
| L0 | Nothing — no provenance exists. | The default. A local docker build with no record. |
| L1 | Provenance exists and describes how the build ran — but may be incomplete or unsigned. | Emit provenance from your build process. Documentation, not yet a guarantee against forgery. |
| L2 | Build runs on a hosted platform that signs the provenance — tamper-evident. | Move builds to a managed CI (GitHub Actions, etc.) that signs provenance for you. |
| L3 | Hardened, isolated builder; provenance is non-falsifiable even by someone who controls the build definition. | Use an isolated, ephemeral builder (e.g. a SLSA reusable workflow) where the signing key is unreachable from build steps. |
The leap that matters is L2 → L3. At L2 a signature proves the provenance was not altered after the fact, but a developer who controls the build steps could still make the build lie about itself. L3 isolates the build environment and puts provenance generation out of reach of the build’s own code, so the metadata is accurate even against an insider who owns the pipeline definition. That is the bar for code you actually trust at deploy.
Sigstore and cosign: keyless signing, recorded in a public log
Provenance and SBOMs are only as good as the signature that binds them to a specific artifact — and signing has always died on key custody. Sigstore removes the long-lived key entirely. With cosign keyless signing, your CI workflow authenticates over OIDC (its workload identity — “the release.yml job in repo X on tag v1.4.2”), Sigstore’s CA Fulcio mints a certificate that binds that identity to an ephemeral key and self-destructs in about ten minutes, you sign, and the signing event is written to Rekor, an append-only public transparency log. There is no private key to leak, rotate, or store in a vault.
The artifact is bound to its provenance and SBOM through an attestation — a signed statement (in-toto format) of the shape “for the artifact with digest sha256:…, here is its provenance / here is its SBOM.” GitHub’s attest-build-provenance action produces exactly this for a built image and signs it keylessly via the workflow’s OIDC identity. The whole point of Rekor is that the signer monitors the log for their own identity: an unexpected entry signed as you is a detected compromise, not a silent one.
# Keyless sign the image — OIDC identity, ephemeral cert, logged to Rekor
COSIGN_EXPERIMENTAL=1 cosign sign your-registry/app@sha256:abcd...
# Bind the SBOM to the image digest as a signed attestation
cosign attest --predicate sbom.cdx.json --type cyclonedx \
your-registry/app@sha256:abcd...
# At DEPLOY — refuse anything not signed by the expected workflow identity
cosign verify your-registry/app@sha256:abcd... \
--certificate-identity-regexp 'https://github.com/acme/app/.github/workflows/release.yml@.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com▸Why this works
In GitHub Actions you usually skip raw cosign and use actions/attest-build-provenance@v1, which generates SLSA-style provenance for an artifact and signs it keylessly using the job’s OIDC token — no cosign install, no key management. It needs permissions: id-token: write and attestations: write. The attestation is bound to the image digest, never a mutable tag, which is the whole point: a tag like :latest can be re-pointed after verification, so you sign and verify by digest end to end.
The failure mode: generating proof you never check
Every artifact in this lesson is worthless until something refuses to deploy without it. The dominant real-world failure is not a missing SBOM — it is an SBOM generated in CI and never queried, a signature produced and never verified, an attestation attached and the deploy step that ignores it. Signing without a verify-at-deploy gate is security theater: you have paid for the ceremony and bought none of the assurance. The gate is cosign verify in the pipeline, or an admission controller (Kyverno / Sigstore policy-controller) that rejects any image whose signature and provenance do not match the expected workflow identity — pinned by digest, never a mutable tag.
The other quiet failure is trusting a mutable base image: you sign your layers, but FROM someimage:latest re-resolves to different bytes between build and deploy, so your signed artifact sits on an unverified, swappable foundation. Pin base images by digest and verify them too, or the chain of custody has a hole at the bottom.
A team on hosted GitHub Actions wants to sign release images and gate deploys. Pick the signing and trust model.
Your pipeline generates an SBOM and signs every image with keyless cosign, but the deploy step just pulls and runs the image. A tampered image is pushed by a compromised token. What happens?
What is the key difference between SLSA Build L2 and L3?
- 01Walk through generate → sign → attest → verify, and name what makes each step trustworthy.
- 02Why is 'we generate an SBOM and sign every image' not enough, and what two gaps usually remain?
Securing the software supply chain is three separate claims, and only the last one is enforcement. An SBOM is a machine-readable inventory of every direct and transitive dependency, generated in CI from the built artifact with syft or cdxgen in CycloneDX or SPDX format, so when a CVE like the xz-utils backdoor lands you answer “are we affected?” by querying stored SBOMs in minutes instead of grepping repos. Build provenance is the verifiable record of how the artifact was built — which builder, which commit, which parameters — graded by SLSA from L1 (provenance exists) through L2 (a hosted platform signs it) to L3 (an isolated builder makes provenance non-falsifiable even by someone who controls the build definition). Sigstore’s cosign signs keylessly: the workflow authenticates over OIDC, Fulcio mints a ~10-minute certificate bound to that workload identity, and Rekor records the signing in a public append-only log, so there is no long-lived key to custody. An in-toto attestation binds the SBOM and provenance to the artifact’s immutable digest. But the failure mode that bites teams is generating all of this and never checking it: signing without a verify-at-deploy gate, or trusting a mutable base image or tag that can be re-pointed between sign and verify. The gate — cosign verify or an admission policy pinned to the expected workflow identity and the digest — is the only step that turns paperwork into a guarantee. Now when you see a green “image signed” checkmark in CI, ask the next question: what in the deploy path actually verifies that signature and refuses to run if it’s missing?
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.