SLSA and Provenance: Your SBOM Says What's Inside, This Says Where It Came From

The Blast Radius runs on one conviction: prefer enforcing controls you can verify over promises you have to trust. Real security lives in invariants, the properties that hold against the worst case an adversary can construct, not in averages, vendor accuracy stats, or a model behaving itself. Design it in, make it deterministic, and measure it. This issue turns that lens on build integrity, and on the difference between a signature you can check and a badge you are asked to trust.

TL;DR

  • SBOM vs provenance: your SBOM says what is inside an artefact; provenance says where it came from and proves that origin cannot be forged. Different axes, and you want both.
  • Provenance is the foundation, not the whole of build integrity. It makes an artefact’s origin unforgeable and lets you reject anything at deploy time that did not come from the source and builder you expect. On its own it does not catch a compromised build that turns clean source into a dirty binary.
  • What catches that (the SolarWinds and XZ case) is reproducible builds and independent rebuilders, which SLSA only recommends, not requires. They were the old Level 4; Level 3’s isolation is not the same thing.
  • Verifying beats chasing levels. Generating provenance is easy and nearly useless on its own; verifying it, so an unverified artefact cannot deploy, is the half that actually protects you, and the half most teams skip.
  • No regulation mandates a SLSA level (not CRA, NIS2, DORA or FedRAMP 20x). They want demonstrable integrity; provenance is a means to evidence it, and the level is your risk decision.

In Your SBOM Data Has Been Gathering Dust, I made the case for treating your Software Bill of Materials (SBOM) as a live graph rather than a compliance artefact left to rot in a bucket. An SBOM is a powerful thing. It tells you what components are inside an artefact. But there is a question it cannot answer, and it is a question that has burned some very large organisations: how do you know the artefact you are running was actually built from the source you think it was?

That gap is where build provenance lives, and it is the subject of this post.

The Problem SBOMs Do Not Solve

Think about SolarWinds. The source code was fine. Code review was fine. The developers were not malicious. What went wrong was the build system: an attacker compromised the build pipeline and injected the SUNBURST backdoor into otherwise legitimate, signed software updates. Every downstream customer received a perfectly valid-looking artefact whose contents no longer matched the source it was supposed to have been built from.

An SBOM would not have caught that on its own, because the SBOM describes the components, not the integrity of the build that assembled them. You can have a flawless inventory of a poisoned artefact.

What SolarWinds really exposes is that the build pipeline is an attack surface in its own right, and one an SBOM cannot see. To reason about it at all you need a tamper-evident record that answers three questions about every artefact: which source produced it, which build system built it, and exactly what inputs and parameters went into it. That record is called provenance, and the framework that has emerged to standardise it is SLSA.

One caveat up front, because the rest of this post depends on it, and because I would rather not sell you something that does not exist. Provenance is where build integrity starts, not where it ends. It makes an artefact’s origin unforgeable and lets you reject anything at deploy time that did not come from the source and builder you expect, which shuts down a large class of substitution and impersonation attacks. What it does not do, on its own, is catch the specific SolarWinds move, a compromised build turning clean source into a dirty binary, because the provenance will faithfully record the clean source and the official builder and sign it. Actually closing that gap takes isolated builds and, above all, reproducible builds, which we come to later. So treat provenance as the foundation of build integrity, not the whole of it, and be suspicious of anyone who sells it as the latter.

What SLSA Actually Is

SLSA stands for Supply-chain Levels for Software Artifacts, and it is pronounced “salsa”. It started life at Google and is now maintained under the Open Source Security Foundation (OpenSSF). The first stable version, SLSA v1.0, arrived in April 2023, replacing the earlier v0.1 preview.

A useful thing to understand about v1.0 is what it deliberately left out. The earlier drafts had four levels, with the fourth and highest, Level 4, demanding hermetic and reproducible builds. Those turned out to be genuinely hard to achieve in practice, so v1.0 consolidated the model around a single Build track with levels 0 to 3, and treated hermetic and reproducible builds as recommended rather than mandatory. A Source track is still under development. This is a framework that has chosen to be achievable over being theoretically perfect, and I respect that.

The build levels, in plain English:

  • Level 0: no provenance. You are trusting on faith.
  • Level 1: provenance exists. The build produces a document describing how the artefact was made. It is not signed, so it proves intent more than integrity, but it is a real start.
  • Level 2: signed provenance, generated by a hosted build service. Now the provenance is authenticated, so tampering with the provenance itself becomes detectable.
  • Level 3: a hardened build platform that produces unforgeable provenance, generated by a trusted control plane the build steps cannot reach, in an isolated, single-use environment so one build cannot influence another. Mind the boundary, though: isolation is not reproducibility.

Level 3 does not require the reproducible builds that were in Level 4 of the old draft, and those are what would actually detect a tampered output. What Level 3 does and does not guarantee is worth its own section, which is next.

Image description

What Provenance Looks Like

SLSA provenance is a JSON document in the in-toto attestation format, wrapped in a DSSE (Dead Simple Signing Envelope). Strip away the ceremony and it captures:

  • the builder identity (for example, a GitHub Actions runner with its OIDC identity),
  • the source repository URL and commit SHA,
  • the build trigger (the workflow that kicked it off),
  • the build parameters (anything that influenced the output),
  • the invocation ID (a unique identifier for that specific build run), and
  • the output artefact digest (the SHA-256 hash of what came out).
Anatomy of a Provenance Attestation
Anatomy of a Provenance Attestation

When you go to deploy something, you can now verify that its digest matches a signed provenance statement that points back to a commit and a build you trust. If the artefact has been swapped or rebuilt somewhere along the way, the verification fails.

Provenance Chain Diagram
Provenance Chain Diagram

What Makes Provenance Unforgeable, and What It Still Cannot Promise

This is the part that deserves more than a hand-wave, because it is where the security actually lives, and where the naive version falls apart. Two fair questions get us there. If the signature is applied after the build, what stops someone tampering with the artefact during the build? And if the source can be changed before compilation, what is a signature over the output even worth?

The build has two halves, and they do not trust each other. A build platform is really two things: the build steps you define (compile this, package that, run this script), and a trusted control plane that orchestrates them. The whole game at Level 3 is keeping those two apart. The signing key lives only in the control plane; the environment running your build steps cannot reach it. And every field in the provenance is filled in by the control plane, not by your build. So the provenance is not the build signing a statement about itself, which would be worthless, because a compromised build would simply lie. It is the control plane attesting to what it observed: which commit it checked out, which workflow ran, what came out, and the SHA-256 of that output. That is why signing after the build is fine. The signer is not the build; it is the referee that watched the build.

How GitHub actually does this. The slsa-github-generator runs provenance generation as a reusable workflow hosted in a separate repository, pinned to a tag. GitHub Actions isolates a reusable workflow run from the workflow that calls it, so your build job cannot alter what the generator sees or records, even if that build job is fully compromised. The generator takes the OIDC token GitHub issues at runtime (which proves which repository, workflow and trigger ran), exchanges it at Fulcio for a short-lived certificate, signs the provenance, and logs it in Rekor. No long-lived key exists to steal, and the key that does the signing is never exposed to your build steps. That separation is the entire point, and it is why native in-job attestation is the easier but weaker option: the signing sits closer to the build.

So what does this actually stop? It makes the origin claim unforgeable. An attacker who compiles a malicious binary on their own machine cannot mint provenance saying it came from your official builder and your protected branch, because they do not have and cannot reach the signing identity. At deploy time you reject anything whose provenance does not verify against the exact source and builder you expect. That is a real, deterministic property: valid provenance from the expected origin, or no deployment.

And here is what it does not stop, which is the part people miss. Provenance attests to process, not to the safety of the result. If your build environment is subverted and injects a backdoor during compilation, the provenance will faithfully record “built from clean commit X, by the official CI” and sign it, because the source was that commit and the builder was official. The malice entered during the build, and the attestation is a truthful record of a compromised process. Unforgeability protects the integrity of the statement, not the integrity of the binary. This is why the SolarWinds comparison needs care: Level 3’s isolated, single-use environments make the persistent build-server foothold that SolarWinds relied on far harder to establish, and the provenance gives you an audit trail, but isolation alone does not guarantee you would detect a one-off injection.

Two consequences follow, and both are about layering rather than asking provenance to do a job it was never designed for.

Source changes are a source problem. If someone lands a malicious commit and it gets built, the provenance records that commit, honestly. Catching it is the job of the Source track: branch protection, mandatory review, the two-person rule. Provenance tells you which commit produced the artefact; source controls decide whether that commit should ever have existed. You need both, and neither substitutes for the other.

Detecting a tampered build leans on reproducible builds. The thing that would actually catch the clean-source-but-dirty-output case is a reproducible build: rebuild the same source, independently, in a different environment, and compare the hashes. If they differ, something was added along the way. That is the deterministic integrity check provenance on its own does not give you. It is also the honest asterisk on Level 3, because SLSA v1.0 makes hermetic and reproducible builds recommended, not required. A project can be Level 3 and still not be independently reproducible.

How is that check actually run? You rebuild the same source on a different system and compare the two output hashes, and that is the whole method: a byte-for-byte hash comparison, not any clever analysis of the binary. The independence is the point. Two builds on the same compromised machine would carry the same backdoor and match happily, so the guarantee only appears when the second builder does not share the first one’s trust. And there is no escrow holding the artefact and adjudicating. The closest thing to a neutral third party is the transparency log, and that is a witness, not a custodian: Rekor proves a signed provenance statement existed, it does not rebuild anything. The real answer to “who checks” is independent rebuilders. The Reproducible Builds project runs exactly this at distribution scale, where rebuilderd continuously rebuilds Debian, Arch and Fedora packages on separate infrastructure and publishes a GOOD or BAD verdict per package (over 95% of Debian’s current release now reproduces, and Debian 14 will refuse to ship packages that do not). The security property is subtle but strong: reproducible builds do not prevent a compromised build machine from emitting a malicious binary, they make it detectable by anyone, which shifts the attacker’s job from compromising one pipeline to compromising every independent rebuilder at once. That is the multi-party version of the same deterministic check, and it is what actually closes the SolarWinds and XZ-style gap where the shipped artefact quietly diverges from the reviewed source. In your own pipeline the cheap starting point is humbler: build twice in different clean environments and fail the job if the hashes differ, with diffoscope to show you what moved.

Which is where obfuscation bites. Reproducibility demands determinism: the same inputs must yield byte-for-byte identical output. Many obfuscators do the opposite on purpose, seeding name mangling and control-flow flattening with randomness, or stamping in build timestamps and IDs. Turn that on naively and two honest rebuilds no longer match, and you have thrown away your ability to verify the artefact against its source. If you obfuscate and you want reproducibility, the obfuscation itself has to be made deterministic: a pinned seed, a fixed SOURCE_DATE_EPOCH, sorted inputs, no wall-clock timestamps. The same discipline applies to any non-determinism in the toolchain; obfuscation is just the most self-inflicted example.

Put the pieces together and the shape is the one that runs through this whole newsletter. Provenance is a deterministic proof of origin. Reproducible builds are a deterministic proof of integrity. Source controls are a deterministic gate on what gets in. Each is an invariant that covers one thing, and the mistake is asking any single one of them to cover all three.

The Tooling Is Real and Mostly Free

This is not a paper framework. It helps to see the toolchain as the same four stages the provenance itself moves through: generate the provenance, sign it, record it somewhere tamper-evident, then verify it before you deploy. The good news is that on the mainstream path the first three now happen in essentially one step, which means the work actually left to you is the step most teams skip: verification and enforcement.

Provenance Toolchain Map
Provenance Toolchain Map

The fastest path, if you build on GitHub. GitHub Artifact Attestations (the actions/attest-build-provenance action, now a thin wrapper over the more general actions/attest) will generate SLSA provenance for a built artefact, sign it, and record it, from a few lines in your workflow:

permissions:
  id-token: write        # keyless Sigstore signing
  attestations: write    # upload the attestation
  contents: read
steps:
  # ... your build steps produce ./dist/app ...
  - uses: actions/attest-build-provenance   # pin to the current major version
    with:
      subject-path: ./dist/app

The signing is keyless, which is what makes this painless: Fulcio issues a certificate that lives for about ten minutes, tied to the workflow’s own OIDC identity, and Rekor records it in a transparency log, so there are no long-lived keys to manage or leak. Public repositories use the public-good Sigstore instance; private and internal repositories use GitHub’s own private Sigstore, which also answers the transparency-log privacy concern I raise below. Artifact Attestations went GA in June 2024, and through 2025 and into 2026 GitHub has been shifting generation from opt-in towards on-by-default for public repositories, so this is quietly becoming table stakes rather than a nice-to-have.

Then verify, because generation on its own buys you nothing. This is the half people forget. A signed attestation that nobody checks is decoration. So verify the artefact before it ships:

gh attestation verify ./dist/app --owner your-org

For a portable check outside GitHub, slsa-verifier does the same job for container images and released binaries, reaching Rekor and Fulcio to validate the certificate chain and confirming the provenance points at the source repository and builder you actually expect, not just that some valid signature exists.

Getting to Level 3, whatever you build on. Native attestations give you signed provenance from a hosted build service, which is essentially Build Level 2. Level 3 asks for one more property, and it is worth stating in platform-neutral terms because it is a property of your pipeline, not a feature of anybody’s product: the provenance must be generated and signed by a trusted component that the build steps cannot reach or influence, and the build must run in an isolated environment that cannot be tampered with by the thing it is building. Concretely that means three things. The signing identity is unavailable to your build steps. The definition of the trusted component lives somewhere a person with write access to the project cannot edit. And each build gets a fresh, isolated environment, so one build cannot leave anything behind for the next.

Any platform that gives you those three properties can reach Level 3. GitHub Actions is simply the implementation most readers will have to hand, and slsa-github-generator is how you get there on it, so I will use it as the worked example. If you build somewhere else, hold on to the three properties above and read the example as a shape to match rather than a tool to adopt.

The GitHub instantiation. Here the trusted component is a reusable workflow whose definition lives outside the repository it builds, so that someone with write access to your repo cannot forge its own provenance. That is what the slsa-github-generator Level 3 workflows provide, and what slsa-verifier was written to check. In practice your pipeline splits into two jobs. The build job does its normal work and publishes exactly one thing the next job needs: the SHA-256 of each artefact, base64-encoded. The provenance job then calls the generator’s reusable workflow, which does the signing in its own isolated run:

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      hashes: ${{ steps.hash.outputs.hashes }}
    steps:
      # ... your normal build, producing ./dist/app ...
      - name: Hash the artefacts
        id: hash
        run: echo "hashes=$(sha256sum ./dist/app | base64 -w0)" >> "$GITHUB_OUTPUT"

  provenance:
    needs: [build]
    permissions:
      actions: read      # detect the build environment
      id-token: write    # keyless signing
      contents: write    # attach provenance to the release
    # pin to a released tag, not a branch
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
    with:
      base64-subjects: "${{ needs.build.outputs.hashes }}"
      upload-assets: true

Notice what the build job never gets to do. It never sees a signing key, it never writes the provenance, and it does not run in the same job as the generator. All it hands over is a list of hashes. Pin the generator to a released tag rather than a branch, because that pin is part of what a verifier checks when it decides whether the builder is one it trusts.

Verification is the other half, and the builder identity matters as much as the artefact hash:

slsa-verifier verify-artifact ./dist/app \
  --provenance-path provenance.intoto.jsonl \
  --source-uri github.com/your-org/your-repo \
  --source-tag v1.2.3

That checks the signature against the transparency log, confirms the provenance names the source repository and tag you expect, and confirms it came from a recognised SLSA builder. Change a single byte of the artefact after the build and it fails on a hash mismatch, which is precisely how you kill post-build substitution. There is an equivalent verify-image for container digests.

One distinction is worth knowing before you choose. The language-specific builders (Go, Node.js, Maven, and the container generator) compile your project inside the trusted workflow itself, so both the build and the provenance happen in the isolated environment. The generic generator shown above attests to artefacts your own job produced, so it makes the provenance unforgeable but does not isolate the compilation. If a builder exists for your stack, it is the stronger option; the generic generator is the universal fallback for everything else.

If you are not on GitHub. Take the three properties back to your own platform and ask where each one lives. Who holds the signing identity, and can a build step reach it? Where does the definition of the signing component live, and who can change it? Does every build get a clean, single-use environment? GitLab, Tekton Chains, Buildkite and the rest each answer these differently, and some answer them well. The generic point is that “our CI produces provenance” tells you nothing on its own: provenance generated by the same job that ran the build, using a key that job could read, is Level 1 or 2 dressed up, because a compromised build could have written whatever it liked. The in-toto and Sigstore projects are platform-agnostic and give you the primitives to build this yourself if your platform lacks it. And whatever you use, slsa-verifier and the SLSA provenance format are deliberately not GitHub-specific, so the verification half travels with you.

So the promotion path is concrete: start at Level 2 with native attestations everywhere, then for your highest-value artefacts pick the language-specific builder if one exists, wire in the two-job pattern above (or its equivalent on your platform), and add the verify step to whatever promotes a build towards production.

Beyond GitHub, and beyond CI. Package ecosystems are wiring this in at source: npm publish --provenance produces a Sigstore-backed attestation tying a package to its build and commit, verifiable by anyone who installs it. GitLab and other CI systems have their own provenance and attestation features; the thing to check is what assurance level each actually gives you, because “we produce provenance” can mean anything from unsigned metadata to a genuine Level 3.

Enforce it at the boundary, or none of the above matters. Verification only bites if an unverified artefact physically cannot ship. In Kubernetes, GitHub’s Sigstore Policy Controller (install via Helm, point its trust root at GitHub’s certificate authority, then label the namespaces you want enforced) rejects any image without a valid attestation at admission time. Kyverno does the same, and for non-Kubernetes pipelines you can feed gh attestation verify --format json into OPA or Cue to gate a deploy on rules like “built from main, by an approved workflow, in an approved repository.” This is the verify-before-deploy gate from earlier in the post made concrete: no valid provenance, no deployment, enforced deterministically rather than left to whether someone remembers to look.

A minimal end-to-end, then, looks like this: add the attestation step to your build, turn on gh attestation verify (or slsa-verifier) in the job that promotes an artefact, and put a Policy Controller or Kyverno rule in front of your cluster so unverified images are refused. Three changes, all using free and mostly managed pieces, and you have gone from “we sign some things” to “nothing deploys unless its origin checks out.”

Where This Fits With Everything Else

Let me connect the three legs of the stool, because individually they each have a blind spot and together they are genuinely strong:

  • The SBOM tells you what is inside the artefact.
  • SLSA provenance tells you where it came from and that it has not been tampered with.
  • The supply-chain trust score from Threat Modeling Your Dependencies tells you how much you should trust the source in the first place.

A high trust score on a component you cannot prove the provenance of is a component you are taking on faith. A perfect provenance chain for a component with a trust score of 2.0 is a verifiable delivery of something you should not be using. You want all three.

And notice the two different kinds of thing at work here, because it is exactly the distinction this newsletter keeps returning to, and it is worth heading off any confusion. Provenance and signature verification are deterministic: the digests match and the signature checks out, or they do not, and no adversary can argue them out of that answer. The trust score is deliberately probabilistic: it weighs evidence to inform a design-time choice about which components you should allow near your build in the first place. That is not a contradiction, it is the right tool for each job. You verify what you can and weigh what you cannot, and the discipline is never to mistake the second for a boundary. The probabilistic score decides what you let in; the deterministic boundary makes sure only the thing you approved actually ships.

Which SLSA Level Do CRA, NIS2 and DORA Require?

The honest answer, and it surprises people: none of them. Not the Cyber Resilience Act, not NIS2, not DORA names a SLSA level, or SLSA at all. These regimes are written in outcome terms, deliberately technology-neutral, and that is not an oversight to read around.

What they require maps onto provenance cleanly even so. The CRA’s essential requirements (Annex I) oblige manufacturers to secure the design, development and production of a product, to handle vulnerabilities, and to keep technical documentation of how the product is built and maintained for ten years. That is a documented, tamper-resistant account of your build, which is exactly what signed provenance produces as a by-product. The detail will firm up in the harmonised standard on secure development requested under mandate M/606, but that standard is still in draft (expected around mid-to-late 2026 and likely cited in the Official Journal in 2027), and until it is cited you demonstrate conformity directly against Annex I. Whether it names SLSA specifically remains to be seen; the build-integrity outcome it encodes will be satisfiable with it either way.

It is tempting to read that SBOM mandate as an implicit SLSA requirement. It is not, and the reason is worth being precise about. An SBOM is a composition requirement: it tells you what is inside. SLSA levels are a build-integrity requirement: they tell you how unforgeably the thing was built. Those are different axes, and you can ship a flawless, fully compliant SBOM with no provenance at all. Nor do these regimes mandate build provenance in the SLSA sense: the CRA’s “attestation” is the EU Declaration of Conformity, a legal statement that the product meets the essential requirements, not a signed record of how a binary was produced. And SLSA levels are defined by properties (signed by the build platform, generated in an isolated control plane, unforgeable by the build steps) that none of these regulations specify. No properties, no implied level. What the regulations actually want is demonstrable integrity, which provenance happens to evidence well. That is a means you choose, not a tier they impose.

NIS2 does the same at the organisational level: Article 21 makes supply-chain security a duty for essential and important entities without prescribing how. DORA does it for the financial sector’s ICT third-party risk. In every case the regulator specifies the property (integrity, secure development, supply-chain control, the ability to show how software was produced) and leaves the mechanism to you.

So do not wait for a mandated level, because one is not coming. Choose your target by the criticality of the product and the risk it carries, not by a compliance number. A payment terminal’s firmware earns Level 3 and reproducible builds; an internal dashboard probably does not. The one place a level is inching toward explicit is the US side, where NIST’s Secure Software Development Framework and the executive-order self-attestation regime lean directly on build provenance, and SLSA is designed to align with it, but even there the ask is the practice, not a number.

Note to engineering leaders: Treat provenance as compliance evidence you generate automatically, not a scramble you mount before an audit. And when a vendor tells you a regulation “requires SLSA Level 3”, ask them to cite the article. They cannot, because it does not exist. What the regulations require is that you can show, credibly, how your software was built; the level is your risk decision. I will go through what CRA, NIS2 and DORA actually demand of an engineering pipeline in the next issue, CRA, NIS2 and DORA for Engineers.

Be Honest About the Limits

In keeping with how I try to write these, here is where it gets uncomfortable.

Self-attestation can be fabricated. A producer claiming “we are SLSA Level 3” is, by itself, just a claim. The value comes from provenance you can independently verify, not from a badge on a README. Trust the cryptography, not the assertion.

Level 3 is hard. Getting to a genuinely hardened, isolated build platform is real work, and many organisations will sit at Level 1 or 2 for a long time. That is fine. Level 1 across your whole estate beats Level 3 on one repo and nothing everywhere else.

Sigstore’s transparency logs are public. Keyless signing through Rekor writes to a public log. For most open-source work that is a feature. For some private or sensitive contexts it is a consideration you need to think through before adopting it wholesale.

Provenance proves origin, not innocence. As covered above, it records which commit and process produced the artefact; it does not vouch for the commit, the integrity of the build, or the safety of the code. Those need source controls, reproducible builds, and the rest of your review pipeline working alongside it.

Note to platform and CI/CD teams: The highest-leverage move is not chasing Level 3. It is turning on verification. Generating provenance that nobody checks is security theatre. Put slsa-verifier or an admission policy in the path between your registry and production, so that an artefact without valid provenance simply cannot deploy. That single gate changes provenance from a document into a control.

Final Thoughts

The supply-chain attacks that have hurt us most were not always about vulnerable components. Several were about trusted build systems producing untrusted output, and an SBOM, on its own, cannot see that class of attack. Provenance and reproducible builds, together, can: one makes the build’s origin unforgeable, the other makes any divergence from source detectable.

SLSA is not a product, a scanner or a silver bullet. It is a maturity model and an attestation format, and the tooling around it is mature enough that there is no longer a good excuse to ship artefacts you cannot trace back to a commit. Start at Level 1, get provenance flowing for everything, turn on verification so it actually gates deployment, and climb from there.

Your SBOM tells you what is in the box. SLSA tells you the box is the one you packed. You want to be able to say both with confidence. A signature you verify is a deterministic control; a self-attested badge is a promise, and I would rather trust the first. That is the same line I draw for AI guardrails in Threat Modeling the Model Context Protocol: prefer the boundary you can check over the assurance you are asked to take on faith.

Stay tuned!


Tags: Supply Chain Security, SLSA, SBOM, Provenance, Sigstore, DevSecOps, Software Composition Analysis, CRA, Trustworthyness, Vulnerability Management, Secure by Design