The SLSA framework & levels
What each level buys, and what it costs.
You can read every line of a program's source code, approve it, and still hand your customers malware. That's what happened with SolarWinds in 2020. The attackers never touched the public source. They got into the build server (the machine that turns source code into the finished program) and quietly swapped in their own code while it compiled. The source stayed clean. The thing that shipped did not.
SLSA (Supply-chain Levels for Software Artifacts, said out loud as "salsa") is a framework from the OpenSSF (Open Source Security Foundation, part of the Linux Foundation) built for exactly this gap. It answers one question: how hard would it be for someone to tamper with your build without anyone noticing? The answer isn't yes or no. It's a ladder. Reviewing source code is like checking the ingredients before they go in the pot. Necessary, and not enough, because the tampering can happen at the stove.
Provenance, the receipt that travels with the build
When a courier delivers a package, a careful one carries a chain-of-custody slip: who packed it, where it came from, who handled it on the way, signed at each step. Provenance is that slip for a piece of software. It's a record that says this exact binary was built from this exact source commit, by this builder, with these parameters, at this time. If the slip is trustworthy, you can answer "where did this come from" with proof instead of a shrug.
SLSA doesn't invent its own file format for that slip. It reuses in-toto attestations (a standard way to write a signed statement about how a piece of software was produced). The statement names the artifact it describes (the subject) and carries a predicate of type https://slsa.dev/provenance/v1 that holds the actual build details. Sign that statement and you have provenance a machine can check.
The build track, level by level
SLSA v1.0 (released April 2023) splits its requirements into tracks. The Build track is the one with real tooling behind it today; a separate Source track, about how commits reach your repository in the first place, is much younger and has far less tooling behind it. So when people say "SLSA level" in 2026, they almost always mean the Build track. It runs from L0 to L3. (An old L4 from the 2021 draft was dropped.)
L0 is the ground floor: no provenance at all, no claims, nothing to check. L1 means the provenance exists. Your build process produces that slip and describes how the artifact was made. That's genuinely useful for inventory, because you can finally ask "what built this?" and get an answer. But the slip is forgeable. Nothing stops a developer, or a compromised build step, from writing whatever they like on it.
L2 is where the slip gets signed. A hosted build platform (a shared build service like GitHub Actions or GitLab CI, not someone's laptop) generates the provenance and signs it. Signing buys two things: it proves the slip really came from that platform, and it makes any later edit obvious, because a changed slip no longer matches its signature. You've moved the build off personal machines and made the provenance tamper-evident. The weakness that remains is that your own build steps still run in the same space as whatever produces the signature.
L3 closes that gap. The builder is hardened: each run is isolated and ephemeral (a fresh, throwaway environment, so one build can't poison the next), and the signing key is kept somewhere your build steps can't reach. That last part is the whole point. At L3, a malicious step inside your own pipeline still can't forge provenance that names your pipeline, because it never gets its hands on the pen that signs. This is the jump from "signed" to "can't be faked from the inside."
Producing provenance, the easy half
On GitHub, turning this on is a few lines. The workflow below builds a binary and then records signed SLSA provenance for it using Sigstore (a free public service that signs software so you don't have to run your own key infrastructure).
name: releaseon:push:tags: ['v*']permissions:contents: readid-token: write # ask Sigstore for a signing identity tied to this runattestations: write # record the signed attestation on this repojobs:build:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- name: Build the binaryrun: go build -o myapp ./cmd/myapp- name: Generate signed SLSA provenanceuses: actions/attest-build-provenance@v2with:subject-path: myapp
Those three permissions carry the weight. id-token: write lets the job ask Sigstore for a short-lived signing identity tied to this exact workflow run. attestations: write lets it store the result. What you get is signed provenance, which puts you around Build L2 on GitHub-hosted runners. To reach L3, the signing has to happen somewhere your build steps can't touch it, which is what the reusable SLSA generator workflows (like generator_generic_slsa3.yml) are for: your job hands them the artifact, and they produce and sign the provenance in isolation, out of reach of your build code.
Verifying it, the half that protects you
A signed slip nobody checks is decoration. This is the step that turns SLSA from a nice label into an actual control, and it's the one teams skip. On GitHub, one command does it, and you pin the identity you expect while you're at it:
gh attestation verify myapp \--owner my-org \--signer-workflow my-org/myapp/.github/workflows/release.yml
Loaded digest sha256:5f3c9d2a1b8e7f4c6a0d3e9b2c1f8a7d6e5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a for file://myappLoaded 1 attestation from GitHub APIThe following policy criteria will be enforced:- Predicate type must match:................. https://slsa.dev/provenance/v1- Source Repository Owner URI must match:.... https://github.com/my-org- Subject Alternative Name must match regex:. (?i)^https://github.com/my-org/myapp/\.github/workflows/release\.yml- OIDC Issuer must match:.................... https://token.actions.githubusercontent.com✓ Verification succeeded!sha256:5f3c9d2a1b8e7f4c6a0d3e9b2c1f8a7d6e5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a was attested by:REPO PREDICATE_TYPE WORKFLOWmy-org/myapp https://slsa.dev/provenance/v1 .github/workflows/release.yml@refs/tags/v1.4.0
For artifacts built with the SLSA generators, slsa-verifier does the same job and lets you pin the source repository and tag directly:
slsa-verifier verify-artifact myapp \--provenance-path myapp.intoto.jsonl \--source-uri github.com/my-org/myapp \--source-tag v1.4.0
Verified signature against tlog entry index 96521045 at URL: https://rekor.sigstore.dev/api/v1/log/entries/108e9186e8c5677a2a6ff9d0e4b3c1f...Verified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.0.0" at commit 3bd8a1f2c9e4d7a1b0c5e6f8a9d2b3c4e5f60718Verifying artifact myapp: PASSEDPASSED: Verified SLSA provenance
Both commands do three things under the hood: fetch the signed provenance, confirm the signature is valid (checking it against Rekor, Sigstore's public append-only log, which works like a notary's ledger of the signatures written to it), and confirm the artifact's real hash matches the one the provenance claims. If any of those fail, the command exits non-zero and your pipeline can stop before the artifact goes anywhere.
Reading what the provenance actually claims
It helps to open the slip and read it, because the fields inside are what your policy keys on. The provenance ships as a DSSE envelope (Dead Simple Signing Envelope, the wrapper that carries the signed JSON), so you decode the payload and look at the build definition:
jq -r '.payload' myapp.intoto.jsonl | base64 -d | jq '.predicate.buildDefinition'
{"buildType": "https://actions.github.io/buildtypes/workflow/v1","externalParameters": {"workflow": {"ref": "refs/tags/v1.4.0","repository": "https://github.com/my-org/myapp","path": ".github/workflows/release.yml"}},"internalParameters": {"github": {"event_name": "push","repository_id": "612893014"}},"resolvedDependencies": [{"uri": "git+https://github.com/my-org/myapp@refs/tags/v1.4.0","digest": { "gitCommit": "3bd8a1f2c9e4d7a1b0c5e6f8a9d2b3c4e5f60718" }}]}
Here's the defender's payoff. The provenance names the source repository, the exact workflow file that ran, and the git commit it was built from. So you can write a rule with teeth: only deploy an artifact whose provenance says the repository is my-org/myapp, the workflow is release.yml, and the builder is the hardened one you expect. Anything that doesn't line up fails closed.
Where to aim
The levels cost more as you climb. L1 is a habit: switch provenance generation on in your CI (continuous integration, the automated system that builds and tests your code on every change). L2 needs a hosted build platform that signs, which GitHub, GitLab, and Google Cloud Build increasingly give you out of the box. L3 needs genuinely hardened builders, isolated runs with the signing key out of reach, which usually means reusable workflows or the SLSA generators and a bit more plumbing. Most teams get their biggest jump by reaching L2 across everything they ship, then reserve L3 for the artifacts with the largest blast radius: released binaries, base container images, anything that runs in production or on a customer's machine. SLSA is a roadmap, not a badge you either have or don't. Match the level to what would burn if that artifact were subverted.
So the single line to add to your release checklist is the pinned verify, run in the step that decides whether an artifact ships: gh attestation verify $ARTIFACT --owner my-org --signer-workflow my-org/myapp/.github/workflows/release.yml. Put it there and a forged origin fails closed instead of sailing into production.
Try this
Run jq -r '.payload' myapp.intoto.jsonl | base64 -d | jq '.predicate.buildDefinition' on a scratch host or disposable cluster and read the output against what this lesson described. Then change one input so it fails, and re-run: the error you get is the one you will meet in production.
Takeaway
The trap worth remembering here: a valid signature is not a trusted origin. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.