SLSA v1.2 in Production: Verifiable Build Provenance from Source to Admission Control
A comprehensive production guide to implementing SLSA v1.2: Build and Source Tracks, in-toto provenance, Sigstore keyless signing, and Kubernetes admission-time policy enforcement.
Executive Summary
SLSA — Supply-chain Levels for Software Artifacts, pronounced “salsa” — is a vendor-neutral framework that answers one question precisely: is the artifact you are about to run the same artifact a specific build system produced from a specific source revision, under documented controls? It does not scan for CVEs (that is SCA’s job), it does not inventory components (SBOM), and it does not assert whether a known vulnerability is reachable (VEX). It produces tamper-evident, cryptographically verifiable evidence — provenance — that nothing unexpected was introduced between source and binary, and it grades the strength of that evidence on a numbered ladder so producers and consumers share a common vocabulary.
Originally seeded by Google in June 2021 and now an OpenSSF project under the Linux Foundation, SLSA reached its first stable form as v1.0 in April 2023, which scoped the spec to a single Build Track with levels L0–L3. v1.1 (April 2025) was a clarity release that hardened the Verification Summary Attestation. The current approved specification, v1.2 (approved 12 November 2025), is structural: it promotes the Source Track from experimental to normative, finally covering the authoring-and-review side of the chain that the original v0.1 only sketched. A Build Environment Track and a Dependency Track remain in draft.
The enterprise value is concrete and immediate. SLSA Build L1 gives you traceability — when an incident hits, you can answer “where did this binary come from” in minutes instead of days. Build L2 adds a hosted build platform and a signature from that platform’s identity, so a developer’s compromised laptop can no longer mint a release. Build L3 isolates the signing material from user-controlled build steps, so even a malicious build script or a poisoned dependency executing inside the build cannot forge the provenance or tamper with another tenant’s build. Each rung closes a strictly larger set of supply-chain threats, and crucially, the framework is designed for incremental adoption — you do not redesign your pipeline, you tighten it.
This guide is written for engineers who will actually wire this up: platform engineers standing up a signing-and-verification path, SREs gating deployments on attestations, security architects mapping SLSA to EO 14028 / SSDF / EU CRA obligations, and cloud architects deciding where the roots of trust live. It walks the full path — in-toto attestation internals, the DSSE envelope, the Sigstore keyless flow (Fulcio, Rekor), generating provenance on GitHub Actions and other platforms, and enforcing it at Kubernetes admission time with policy-controller and Kyverno — with real commands, real manifests, and an honest account of what SLSA does not stop.
Why This Matters
The central trust assumption of every modern software program is that the artifact running in production was built from the source the developer reviewed. Without provenance, that assumption is unverifiable — and a string of high-impact incidents have shown attackers attacking precisely the gap between “the source we reviewed” and “the binary we shipped.”
SolarWinds (2020) is the canonical case. The SUNSPOT implant did not modify the published source; it swapped source files during the build and let the legitimate, fully-trusted code-signing certificate sign the result. Roughly 18,000 organizations, including U.S. federal agencies, installed the backdoored Orion update. The lesson that drove SLSA’s design: a digital signature proves only two things — “the key holder signed this” and “it has not changed since.” It says nothing about whether the build that produced the signed bytes was honest. Build provenance fills exactly that hole.
Codecov (2021) modified a fetched CI helper script (the bash uploader) to exfiltrate environment variables — CI secrets and tokens — from thousands of downstream pipelines. That is a distribution-channel attack on a build-time dependency, the kind of thing resolvedDependencies in provenance is meant to surface.
xz-utils / CVE-2024-3094 (2024) is the most instructive of all, and worth understanding precisely because it shows SLSA’s edges. An attacker operating the long-lived “Jia Tan” maintainer identity spent roughly two years building trust on an under-resourced compression library, then in v5.6.0 shipped a malicious m4 macro that injected obfuscated object code into liblzma. Because systemd links liblzma and many distributions patch sshd to depend on systemd notifications, sshd indirectly loaded a payload that hooked RSA_public_decrypt via a GNU IFUNC resolver and gated remote code execution on a specific Ed448 key. The library was a dependency of nearly 30,000 Debian and Ubuntu packages; it was caught — by luck and a sharp engineer chasing a 500ms latency regression — before the backdoor reached stable releases. The critical detail for this guide: the malicious build logic lived in the distributed release tarball, not in the corresponding state of the git repository. A SLSA discipline of building releases from the canonical tagged commit (not a maintainer-uploaded tarball) plus reproducible builds is exactly the control that makes a tarball-vs-repo divergence visible. SLSA would not have stopped a trusted maintainer from committing malice — that is threat “A,” and even two-party review can be defeated by a patient insider — but it would have collapsed the place the attacker chose to hide, and it would have turned the post-disclosure blast-radius investigation from weeks into minutes.
The trend lines justify the investment. Sonatype’s State of the Software Supply Chain has tracked more than 700,000 malicious packages across npm, PyPI, and Maven since 2019, with a 156% year-over-year jump in 2024. And the regulatory floor is rising: U.S. Executive Order 14028 drove the CISA Secure Software Development Attestation Form (which reads against the NIST SSDF), and the EU Cyber Resilience Act — Regulation (EU) 2024/2847, with vulnerability-reporting duties from September 2026 and full conformity by December 2027 — carries fines up to €15M or 2.5% of global turnover. SLSA is the build-integrity scaffold the rest of that compliance story sits on.
What breaks without it: you cannot prove an artifact’s origin, you cannot detect build-time tampering, your incident response is archaeology, and your “we follow secure practices” attestations are unbacked assertions. What becomes possible with it: deploy gates that cryptographically reject anything not built by your trusted pipeline from your trusted source, audit evidence that maps cleanly onto SSDF practices, and a blast-radius query that returns in seconds.
Core Concepts
Before the architecture, lock down the vocabulary. SLSA is precise about these terms and verification logic depends on the distinctions.
Provenance. Verifiable metadata describing where, when, and how an artifact was produced — the source repository and revision, the build platform, the build entry point, the resolved dependencies, and timestamps. Provenance is the core deliverable of the Build Track.
Attestation. An authenticated, machine-readable statement about one or more artifacts. SLSA provenance is one kind of attestation. Attestations are expressed in the in-toto Attestation Framework: an outer Statement binds a predicate (the actual claims) to one or more subjects (artifacts, each identified by name and cryptographic digest). SLSA defines the predicate type https://slsa.dev/provenance/v1; in-toto provides the envelope and the verification framework. The division of labor matters: SLSA defines the requirements and threat model; in-toto defines the data format.
DSSE (Dead Simple Signing Envelope). The signing wrapper around an attestation. It signs over both the payload and the payload type using a Pre-Authentication Encoding (PAE), which closes a class of attacks where a payload is reinterpreted under a different content type. The envelope is { payloadType, payload (base64), signatures[] }.
Tracks and Levels. A track focuses on one aspect of the supply chain; levels within a track are incremental, each building on the last. As of v1.2 there are two approved tracks — Build (integrity of source→artifact) and Source (integrity of the authoring/review process). Higher levels mean stronger guarantees at higher cost; lower levels are deliberately cheap to adopt as an on-ramp.
Build platform, control plane, data plane, tenant. The build platform is the hosted service that executes builds (GitHub Actions, Google Cloud Build, GitLab CI). Inside it, the control plane manages execution and generates provenance; it is isolated from the data plane where user-defined build steps run. A tenant is a user/project on the platform (in GitHub Actions, each repo’s workflow is a tenant). SLSA Build L3’s entire promise rests on the control plane being unreachable from the data plane — so that a malicious build step cannot reach the signing key or another tenant’s build.
builder.id. The identity of the build platform, representing the transitive closure of everything trusted to faithfully run the build and record provenance. Verifiers maintain a root of trust: a map from (builder identity) → (maximum SLSA Build level that builder is trusted up to).
externalParameters. The external, untrusted inputs to a build. This field is the crux of verification: a consumer compares the provenance’s externalParameters (source repo, ref, workflow path) against their expectations for that package. If the values match what should produce an official release, the artifact is accepted; if not, rejected.
VSA (Verification Summary Attestation). An attestation that some verifier checked an artifact (and its attestation bundle) against a policy and reached a verdict. A VSA lets a consumer delegate complex policy evaluation to a trusted party and simply trust the verdict, without needing every underlying attestation — and lets a publisher keep pipeline details confidential while still proving verification happened.
Sigstore (Fulcio, Rekor, Cosign). The de-facto signing toolchain. Fulcio is a certificate authority that issues short-lived X.509 certificates bound to an OIDC identity. Rekor is an append-only transparency log recording signing events. Cosign signs and verifies artifacts and attestations. Together they enable keyless signing: authenticate via OIDC (e.g. a GitHub Actions workload identity token), Fulcio issues an ephemeral certificate tied to that identity, you sign with the ephemeral key, the event lands in Rekor, and the private key is discarded. There is no long-lived key to leak.
A useful mental model for how these layers stack: an SBOM is the ingredient list (the what); SLSA provenance is the tamper-evident manufacturing record (the how); Sigstore is the notary that signs both; and a VSA is the inspection certificate a trusted lab issues so downstream consumers do not each have to re-run the inspection.
Architecture Overview
SLSA is not a single service; it is a discipline imposed across five stages — source, build, attest, publish, verify — with cryptographic evidence flowing left to right and policy decisions enforced at the right edge. The end-to-end flow:
┌─────────────────────────────────────────────────────────────────────────┐
│ SOURCE TRACK (v1.2) │
│ Developer ──PR──> Code Review (2 authorized approvers) ──merge──> │
│ SCS (GitHub/GitLab/gittuf) ──> tagged revision @ gitCommit abc123… │
│ emits: Source Provenance + Source VSA (sourceLevels: SLSA_SOURCE_L3) │
└──────────────────────────────────┬────────────────────────────────────────┘
│ source URI + digest
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ BUILD PLATFORM (Build Track) │
│ │
│ ┌─────────────── CONTROL PLANE (trusted, isolated) ────────────────┐ │
│ │ • pulls SOURCE directly from SCS @ exact commit │ │
│ │ • orchestrates the build, records what actually happened │ │
│ │ • generates PROVENANCE (predicateType slsa.dev/provenance/v1) │ │
│ │ • requests OIDC token ──> Fulcio ──> ephemeral signing cert │ │
│ │ • DSSE-signs provenance; key NEVER exposed to data plane │ │
│ └───────────────────────────────┬───────────────────────────────────┘ │
│ ▲ isolation boundary │ (L3: data plane cannot reach key) │
│ ┌────────┴──────────────── DATA PLANE (untrusted tenant) ───────────┐ │
│ │ user-defined steps: compile / test / package │ │
│ │ resolves DEPENDENCIES ──> recorded in resolvedDependencies[] │ │
│ └───────────────────────────────────────────────────────────────────┘ │
└──────────────────┬───────────────────────────────┬────────────────────────┘
artifact + digest DSSE(provenance)
│ │ tlog entry
▼ ▼
┌──────────────────────────────┐ ┌─────────────────────────────────────┐
│ REGISTRY / PACKAGE INDEX │ │ REKOR (transparency log, append-only)│
│ OCI registry / npm / PyPI │ │ records cert + signature, publicly │
│ artifact + attached │ │ auditable, monotonic │
│ attestation (OCI referrer) │ └─────────────────────────────────────┘
└──────────────┬───────────────┘
│ pull artifact + provenance
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ CONSUMER / VERIFIER │
│ 1. resolve artifact digest │
│ 2. fetch provenance, verify DSSE signature against ROOT OF TRUST │
│ (builder.id ∈ trusted builders, max trusted Build level) │
│ 3. compare externalParameters (source repo, ref) to EXPECTATIONS │
│ 4. (optional) emit a VSA so downstream need not re-verify │
│ ENFORCEMENT: K8s admission (policy-controller / Kyverno) BLOCKS deploy │
│ if no valid provenance from the expected builder + source. │
└─────────────────────────────────────────────────────────────────────────┘
Walking the request path. A developer opens a pull request; the Source Track requires it to be reviewed and merged through controls the producer configured (branch protection, required reviews). The merged revision has a stable identity — a git commit digest. The build platform’s control plane checks out that exact revision directly from the source control system (not from a developer’s working copy or an uploaded tarball — this is the property that defeats threat “C” and would have surfaced the xz tarball divergence). User-defined steps run in the data plane; whatever dependencies they resolve are recorded. When the artifact is produced, the control plane — which the tenant cannot influence — generates the provenance, requests an OIDC-bound ephemeral certificate from Fulcio, DSSE-signs the provenance, and logs the event to Rekor. The artifact and its attestation are published (the attestation typically attached as an OCI referrer to the image, or hosted by the package index). At the right edge, the consumer resolves the artifact’s digest, verifies the signature against a configured root of trust, and compares the provenance’s claimed source against its expectations. In production the consumer is usually not a human running a CLI — it is a Kubernetes admission controller that blocks any pod whose image lacks valid provenance from the expected builder and repo.
The key architectural insight: trust flows from identity, not from possession of a key. At L3 the signing identity is the build platform’s control plane, verifiably isolated from the code being built, so the strongest thing an attacker who fully owns a build step can do is produce a correct provenance describing their malicious build — which then fails the consumer’s expectations check because the source repo or parameters do not match.
Deep Technical Breakdown
The Build Track levels, precisely
The previous (v0.1) model used a single unnamed track numbered SLSA 1–4. v1.0 deliberately dropped the source aspects to focus on the Build Track and renumbered to L0–L3; the old “L4” (hermetic + reproducible) was not carried forward as a hard requirement — hermeticity and reproducibility are now strongly recommended practices, not a level gate. Getting this right matters because a lot of stale content still cites “SLSA 1–4.”
Build L0 — No guarantees. No provenance. Appropriate only for local dev/test builds run and consumed on the same machine.
Build L1 — Provenance exists. The build platform automatically generates provenance describing how the artifact was built, following a consistent build process, and distributes it to consumers. This is enough to build an inventory, debug, and trace origin. It is explicitly trivial to forge — there is no requirement that the provenance be signed by anyone other than the producer, so L1 prevents mistakes, not adversaries. Its value is the cheap on-ramp: large fleets can migrate to provenance-emitting workflows before doing the harder isolation work.
Build L2 — Hosted build + signed provenance. The build runs on a hosted build platform (not a developer workstation), and that platform generates and digitally signs the provenance with its own identity. Now a consumer can detect tampering of the artifact or provenance after the build (threat “F”) because the signature breaks, and the attack surface is reduced to the build platform itself. GitHub’s actions/attest-build-provenance and npm publish --provenance land you here with near-zero effort. What L2 does not give you: protection against tampering during the build, or isolation between the build steps and the signing material.
Build L3 — Hardened, isolated build. Everything in L2, plus the platform must (a) prevent build runs from influencing one another, even within the same project, and (b) prevent secret material used to sign provenance from being accessible to user-defined build steps. The bar is that forging the provenance or evading verification requires exploiting a vulnerability in the build platform itself — beyond most adversaries. This is where the control-plane/data-plane isolation becomes load-bearing. Reaching L3 usually requires real changes to the build platform; on GitHub you get there via the slsa-github-generator reusable workflows, which run the provenance generation in an isolated, tenant-inaccessible context. Note the explicit carve-out in the spec: Build L3 does not cover compromise of the build platform itself, e.g. by a malicious platform insider. That risk is managed by which builders you admit to your root of trust, not by the level number.
The Source Track (new in v1.2)
The Source Track covers threats from authoring, reviewing, and managing source code — everything before a tagged commit. v1.2 reorganized the levels around history and continuous enforcement:
- Source L1 — the revision’s history is retained and identifiable.
- Source L2 — focuses on history and provenance: changes are tracked, the revision has source provenance, and the platform records how revisions came to be.
- Source L3 — focuses on continuous enforcement of technical controls: the controls the producer relies on (e.g. required reviews, protected branches) are continuously enforced and that enforcement is itself attested, so a consumer can verify it without trusting the forge’s UI state at a single point in time.
SLSA approximates “the change reflects the producer’s intent” as approval from two authorized representatives — two-party review. The Source Track’s threat catalog is sober about how that gets subverted: getting a change reviewed in one context then transferring it to another (mitigation: context-specific approvals), the “X then Y” trick where a reviewer approves a delta that hides an earlier bad commit (mitigation: only the exact reviewed revision counts as approved), and colluding reviewers in a fork (mitigation: the merge into upstream still requires review). Tooling is young: GitHub’s actions/attest-build-provenance emits VSAs carrying a sourceLevels field when run against a branch with required reviews; GitLab Build Cloud emits equivalents; and gittuf (OpenSSF Incubating since June 2025) offers a platform-agnostic path to Source L3 by storing policy and enforcement records in a Reference State Log (RSL) inside the repository, so a verifier can prove enforcement without trusting the forge.
Anatomy of a SLSA provenance attestation
This is the artifact everything else operates on. The in-toto Statement (v1) wrapping the SLSA Provenance v1 predicate:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "my-app-v1.2.3-linux-amd64",
"digest": { "sha256": "3f7a…e91c" }
}
],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"ref": "refs/tags/v1.2.3",
"repository": "https://github.com/acme/my-app",
"path": ".github/workflows/release.yml"
}
},
"internalParameters": {
"github": { "event_name": "push", "runner_environment": "github-hosted" }
},
"resolvedDependencies": [
{
"uri": "git+https://github.com/acme/my-app@refs/tags/v1.2.3",
"digest": { "gitCommit": "abc123…def" }
}
]
},
"runDetails": {
"builder": {
"id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.1.0"
},
"metadata": {
"invocationId": "https://github.com/acme/my-app/actions/runs/1234567890/attempts/1",
"startedOn": "2026-06-23T09:14:02Z",
"finishedOn": "2026-06-23T09:18:41Z"
},
"byproducts": []
}
}
}
Field by field, with the verification implications:
subject[].digestbinds the predicate to exact bytes. Verification always starts by resolving the artifact you hold to its digest and matching it here. A digest mismatch is an immediate reject.buildDefinition.externalParametersis the untrusted external input and the single most important field for verification. A verifier compares these to expectations. The spec’s guidance is sharp: if you cannot form meaningful expectations aboutexternalParameters, yourbuildTypeis too low-level. The classic anti-pattern is abuildTypewhose external parameters are “the list of shell commands to run” — that changes every build and is impractical to verify. The correct design records only the source repository inexternalParametersand keeps the command list in a config file in that repo, so the parameter is constant across builds and verifiable by a simple equality check. Verifiers SHOULD reject unrecognized fields inexternalParameters— defaulting to deny.internalParametersare inputs chosen by the build platform itself (not the tenant); they are informational for verification.resolvedDependencies[]records the actual resolved inputs — source, base images, build tools — asResourceDescriptors (uri,digest,name,downloadLocation,mediaType,content,annotations). SLSA v1.0+ imposes no requirement to verify these for the Build level itself, but recording them is what lets you later detect a poisoned dependency (threat “E”) and is the seed of the future Dependency Track.runDetails.builder.idis the trust anchor. It must resolve to an entry in the verifier’s root of trust at or above the claimed level.metadata.invocationIdties the attestation to a specific, auditable build run.
The DSSE envelope and PAE
The Statement above is never signed raw. It is wrapped in a DSSE envelope:
{
"payloadType": "application/vnd.in-toto+json",
"payload": "<base64(statement)>",
"signatures": [
{ "keyid": "", "sig": "<base64-signature>" }
]
}
The signature is computed over the PAE (Pre-Authentication Encoding), not the raw payload:
DSSEv1 <len(payloadType)> <payloadType> <len(payload)> <payload>
Concretely the signed bytes are "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body. Binding the type into the signed material prevents an attacker from taking a payload signed as one type and presenting it as another. This is why you cannot verify SLSA provenance by naively hashing the JSON — verification tooling reconstructs the PAE. (Google Cloud’s gcloud artifacts docker images describe … --show-provenance documents the exact DSSEv1 28 application/vnd.in-toto+json … reconstruction for manual verification.)
The Verification Summary Attestation (VSA)
A VSA lets a trusted verifier collapse a pile of attestations into a single verdict:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{ "name": "my-app", "digest": { "sha256": "3f7a…e91c" } }],
"predicateType": "https://slsa.dev/verification_summary/v1",
"predicate": {
"verifier": { "id": "https://verifier.acme.internal" },
"timeVerified": "2026-06-23T09:20:00Z",
"resourceUri": "pkg:oci/my-app@sha256:3f7a…e91c",
"policy": { "uri": "https://policy.acme.internal/slsa-l3", "digest": { "sha256": "…" } },
"verificationResult": "PASSED",
"verifiedLevels": ["SLSA_BUILD_LEVEL_3", "SLSA_SOURCE_LEVEL_3"],
"dependencyLevels": { "SLSA_BUILD_LEVEL_3": 42, "SLSA_BUILD_LEVEL_0": 7 }
}
}
Why this exists: it lets consumers make a trust decision without access to every underlying attestation or transitive dependency, delegating complex policy to a trusted party; and it lets publishers keep pipeline internals confidential (legally — to hide a supplier — or for security, e.g. not revealing that an embargoed patch shipped). dependencyLevels is a histogram of how many dependencies were verified at each level, which is the practical handle for the “trim transitive L0 deps” problem every real verifier hits.
Step-by-Step Implementation
Three realistic paths, in increasing order of assurance, plus the verification and enforcement that make any of them worth doing. Provenance you never verify is theater.
Path 1 — Build L2 in five lines (GitHub Artifact Attestations)
The lowest-friction entry. actions/attest-build-provenance generates Sigstore-signed SLSA Build L2 provenance for any artifact — binary, container, npm/NuGet/Maven package, or zip — and stores it as an OCI referrer or in the GitHub attestations store.
name: release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write # required: mint the OIDC token for Sigstore keyless signing
contents: read
attestations: write # required: write the attestation back to GitHub
steps:
- uses: actions/checkout@v4
- name: Build artifact
run: |
make build
# produces ./dist/my-app-linux-amd64
- name: Generate SLSA provenance
uses: actions/attest-build-provenance@v2
with:
subject-path: './dist/my-app-linux-amd64'
Verify it from anywhere with the GitHub CLI. Note that verification is monotonic (once an artifact has one valid provenance attestation, adding a bad/malformed one cannot flip the result) and the CLI defaults to the provenance predicate — verifying an SBOM requires --predicate-type explicitly, so a stray SBOM cannot satisfy a provenance gate:
gh attestation verify ./my-app-linux-amd64 \
--repo acme/my-app \
--format json
# Asserts: signed by GitHub's Fulcio identity, built by the acme/my-app workflow,
# tied to the originating run, artifact digest matches.
For containers, attest by digest (never by mutable tag):
- name: Build and push image
id: push
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/acme/my-app:${{ github.ref_name }}
- name: Attest image provenance
uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/acme/my-app
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
Path 2 — Build L3 (slsa-github-generator)
L2 signs provenance from within your own workflow context, which a sufficiently clever malicious step could influence. L3 moves provenance generation into an isolated reusable workflow the tenant cannot reach. The generator runs as a separate job; your build job only hands it the artifact digests.
name: release-slsa3
on:
push:
tags: ['v*']
permissions: read-all
jobs:
build:
runs-on: ubuntu-latest
outputs:
digests: ${{ steps.hash.outputs.digests }}
steps:
- uses: actions/checkout@v4
- run: make build # -> ./dist/my-app-linux-amd64
- name: Compute subject digests
id: hash
run: |
set -euo pipefail
cd dist
echo "digests=$(sha256sum my-app-linux-amd64 | base64 -w0)" >> "$GITHUB_OUTPUT"
provenance:
needs: [build]
permissions:
actions: read # read the workflow run for provenance
id-token: write # keyless signing via Sigstore
contents: write # attach provenance to the release
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: ${{ needs.build.outputs.digests }}
upload-assets: true
The crucial property: the provenance job’s builder.id is the generator’s reusable-workflow path pinned to a tag, and that workflow runs in a context your build job cannot tamper with. Verify with slsa-verifier, which checks the builder identity against its built-in list of known L3 builders and pins the expected source:
slsa-verifier verify-artifact my-app-linux-amd64 \
--provenance-path my-app-linux-amd64.intoto.jsonl \
--source-uri github.com/acme/my-app \
--source-tag v1.2.3 \
--builder-id 'https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@refs/tags/v2.1.0'
# PASSED: SLSA verification passed
Always pass --source-uri (and ideally --source-tag). Verifying the signature without pinning the expected source is the most common way teams fool themselves: the provenance is genuine, it just attests a build from evilfork/my-app.
Path 3 — Native package-ecosystem provenance
npm generates Build L2 provenance natively when you publish from a supported CI with OIDC:
- uses: actions/setup-node@v4
with:
node-version: 22
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
# needs permissions: id-token: write
Consumers see a verified “Built and signed on GitHub Actions” badge linked to the source commit and the Sigstore log entry.
PyPI implements PEP 740. If you publish via Trusted Publishing (OIDC, no long-lived token), attestations are generated by default — there is nothing extra to configure as of pypa/gh-action-pypi-publish@v1.11.0 (default since 29 October 2024):
publish:
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # Trusted Publishing + PEP 740 attestations
steps:
- uses: actions/download-artifact@v4
with: { name: dist, path: dist/ }
- uses: pypa/gh-action-pypi-publish@release/v1
# attestations: true is the default; provenance is emitted automatically
PyPI binds the distribution’s name and digest to its provenance and exposes a provenance URL in the JSON Simple API, so downstreams can verify the file came from the expected GitHub repo.
Keyless signing of arbitrary artifacts with Cosign
When you are not on a turnkey platform, drive the Sigstore flow directly. Cosign 2.x does keyless signing and verifies the SCT (certificate-transparency proof) by default:
# Sign a container image keyless (OIDC identity -> Fulcio cert -> Rekor)
COSIGN_EXPERIMENTAL=1 cosign sign \
ghcr.io/acme/my-app@sha256:3f7a…e91c
# Attach an SBOM as an attestation
syft ghcr.io/acme/my-app@sha256:3f7a…e91c -o spdx-json > sbom.spdx.json
cosign attest --predicate sbom.spdx.json \
--type spdxjson \
ghcr.io/acme/my-app@sha256:3f7a…e91c
# Verify SLSA provenance, pinning the workflow identity AND the OIDC issuer
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp '^https://github.com/acme/my-app/\.github/workflows/release\.yml@refs/tags/v.*$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
ghcr.io/acme/my-app@sha256:3f7a…e91c
The two --certificate-* flags are the whole point of keyless verification: you are asserting which identity (a specific workflow on a specific repo) and which issuer signed this. Omit them and you have verified only “some Sigstore identity signed this,” which is nearly worthless.
Enforcement at Kubernetes admission time
Verification belongs at the deploy gate, automated, failing closed. Two production options.
Sigstore policy-controller — a dedicated admission controller. Install via Helm, then declare a ClusterImagePolicy that requires a SLSA provenance attestation from the expected identity:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-slsa-provenance
spec:
images:
- glob: "ghcr.io/acme/**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "^https://github.com/acme/.*/\\.github/workflows/release\\.yml@refs/tags/v.*$"
ctlog:
url: https://rekor.sigstore.dev
attestations:
- name: must-have-slsa-provenance
predicateType: https://slsa.dev/provenance/v1
policy:
type: cue
data: |
predicateType: "https://slsa.dev/provenance/v1"
predicate: buildDefinition: externalParameters: workflow: repository: =~"^https://github.com/acme/"
The CUE block does the expectations check — it is not enough that a provenance exists; its externalParameters must show the build came from an acme/ repo. A pod referencing an image without a matching attestation is rejected at admission.
Kyverno — if you already run Kyverno for policy, use verifyImages rather than adding a second controller:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-slsa-provenance
spec:
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: check-image-provenance
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "ghcr.io/acme/*"
attestations:
- type: https://slsa.dev/provenance/v1
attestors:
- entries:
- keyless:
issuer: https://token.actions.githubusercontent.com
subject: "https://github.com/acme/*/.github/workflows/release.yml@*"
rekor:
url: https://rekor.sigstore.dev
conditions:
- all:
- key: "{{ buildDefinition.externalParameters.workflow.repository }}"
operator: AnyIn
value: ["https://github.com/acme/my-app"]
Test the gate explicitly — a signed image admits, an unsigned one is denied at the webhook:
kubectl run good --image=ghcr.io/acme/my-app@sha256:3f7a…e91c --dry-run=server # admitted
kubectl run bad --image=ghcr.io/acme/my-app:latest --dry-run=server # denied
# Error: admission webhook denied the request: ... failed image verification
Production Architecture
Single-workflow examples are the easy 80%. The operational reality at enterprise scale is about where the roots of trust live, how verification stays available, and how you avoid one bad attestation taking down every deploy.
Roots of trust as a managed, versioned asset. The verifier’s root of trust — the map from builder identity to maximum trusted Build level — is the single most security-critical config you own. Treat it like a CA bundle: store it in source control, require two-party review to change it (modifying the verifier’s expectations is itself a documented SLSA threat), and roll it out through the same GitOps pipeline as everything else. A common topology: a central platform team owns the org-wide root of trust (which builders are trusted to L3) and publishes it as a signed config; product teams layer additional per-package expectations (this image must come from this repo) on top.
Public vs. private Sigstore. The public-good Sigstore instance (Fulcio + Rekor) is free and fine for open source and many enterprises, but it introduces an availability dependency: if your admission controller must reach rekor.sigstore.dev to verify a Rekor inclusion proof and that path is down, you either fail closed (deployments blocked) or fail open (security disabled). For air-gapped, regulated, or high-availability environments, run a private Sigstore stack — deploy Rekor, Fulcio, and a TUF root via the Sigstore Helm charts, and point Cosign/Kyverno at your own --tufRoot/--tufMirror. This also keeps your build identities out of a public transparency log if that is a confidentiality requirement. The trade-off is that you now operate a CA and an append-only log with their own HA and key-ceremony demands.
High availability of the verification path. Bundle the attestation with the artifact (OCI referrers travel with the image; PEP 740 hosts provenance at the index) so verification does not depend on a second network hop at the worst moment. Cache Rekor inclusion proofs. Configure admission webhook failurePolicy deliberately: Fail (closed) is correct for security but means the policy controller is now on your deployment critical path — run it with multiple replicas, a PodDisruptionBudget, and generous webhookTimeoutSeconds, and exempt the controller’s own namespace and kube-system to avoid bootstrap deadlock.
Multi-region / multi-tenant. Replicate the private Rekor log across regions (it is append-only, which simplifies replication) and front Fulcio with regional endpoints. For multi-tenant platforms, the builder.id already encodes tenant isolation at L3; the org-level concern is ensuring each tenant’s identities are distinct and that one team cannot register expectations that admit another team’s namespace. Per-namespace ClusterImagePolicy scoping (via image globs and namespaceSelector) keeps blast radius contained.
Gradual rollout. Never flip enforcement on globally first. Run the admission policy in warn/audit mode, collect the set of images that would be rejected, drive that set to zero by onboarding workflows to provenance generation, and only then switch validationFailureAction to Enforce namespace by namespace. The dependency histogram in your VSAs (dependencyLevels) tells you how much of your transitive surface is still L0 and where to invest.
Security Considerations
SLSA is a security control, but standing it up introduces its own attack surface and demands specific hardening. “Use IAM” is not a plan; here is the actual threat model and the concrete mitigations.
Authentication / signing identity (the OIDC trust chain). Keyless signing is only as strong as the OIDC issuer and the identity binding. The signing identity is a workflow’s federated token, so two things must be locked: (1) the OIDC issuer must be exactly your build platform’s token service (https://token.actions.githubusercontent.com), pinned in every verification — never accept “any issuer”; and (2) the subject must be the specific workflow on the specific repo (…/release.yml@refs/tags/v*), not a permissive acme/*. The most common silent failure is verifying the signature is valid Sigstore but not pinning identity — which admits anything anyone ever signed through public Fulcio.
Authorization / least privilege in the build. Scope workflow permissions to the minimum: id-token: write only on the job that signs, contents: read elsewhere, attestations: write only where attestations are produced. Do not grant org-wide id-token: write defaults. On the consuming side, restrict who can edit ClusterImagePolicy/Kyverno policies (these are your security posture) via RBAC, and require review on changes to the root of trust.
Secret management. The headline benefit of keyless is that there is no long-lived signing key to steal — the ephemeral key exists for seconds and is discarded. If you must use key-based signing (private Sigstore without workload identity, or HSM-backed keys for a release root), store keys in a KMS/HSM (AWS KMS, GCP KMS, Azure Key Vault), never in CI variables, rotate on a schedule, and — at Build L3 — ensure the key is reachable only by the control plane, never by user-defined build steps. Wiz’s 2025 report found 61% of organizations had secrets exposed in public repos; a signing key in that set is catastrophic because it forges trust, not just leaks data.
Encryption in transit and at rest. Provenance and Rekor entries are integrity-protected by signatures, not confidentiality-protected — assume they are world-readable on public Sigstore. If externalParameters or internalParameters would leak sensitive internal repo structure or embargoed-patch signals, use a VSA to publish only the verdict, or run private Sigstore.
The build-platform-insider gap. Reiterate the spec’s own carve-out: Build L3 does not defend against a malicious insider at the build platform itself. Your mitigation is the root of trust — admit only build platforms whose operational security you actually trust (and whose builder.id you can pin). For the highest assurance, the in-development Build Environment Track adds remote attestation of the build VM (Secure Boot, vTPM measurements, TEE) so the control plane can prove the environment was bootstrapped from an expected image with remote-access agents disabled.
Compliance mapping. SLSA is not itself a regulation, but its artifacts are the cleanest evidence for several. Against NIST SSDF (SP 800-218): PS.1 (protect code from unauthorized tampering) is evidenced by L2+ signed provenance plus build-config review history; PS.2 (verify release integrity) is signed provenance plus published verification instructions; PS.3 (archive each release) is satisfied by retaining the in-toto attestation alongside the artifact. SSDF is what the CISA Secure Software Development Attestation Form (driven by EO 14028 / OMB M-22-18) reads against. For the EU CRA, SLSA provenance plus SBOMs forms the build-integrity half of the conformity evidence; OpenSSF explicitly positions SLSA + Sigstore + SBOM tooling as the machine-readable due-diligence signal set. NIST SP 800-161r1 (C-SCRM) and CSF 2.0’s Govern function (GV.SC) provide the strategy spine these artifacts read against.
Performance Optimization
SLSA adds work to the build and the deploy. The bottlenecks are predictable and the levers are real.
Build-time overhead. L2 attestation is cheap — generating and DSSE-signing the provenance is milliseconds plus one round-trip to Fulcio and Rekor (typically sub-second). The L3 cost is structural, not computational: slsa-github-generator runs provenance in a separate isolated job, which adds a runner spin-up (tens of seconds to a couple of minutes) per release. Mitigation: only attest on release/tag events, not on every PR build; attest the final artifacts by digest rather than re-running the build inside the generator where possible.
Verification at admission. This is the latency that can hurt, because it is on the pod-scheduling path. Each verification may involve a Fulcio cert check, a Rekor inclusion-proof lookup, and policy evaluation. Three levers, in order of impact: (1) bundle attestations with the image (OCI referrers / --new-bundle-format) so the verifier does not make a separate network call to fetch them; (2) cache Rekor proofs and Fulcio roots — the policy controller can verify inclusion against a cached log checkpoint instead of hitting Rekor on every admission; (3) scope policies tightly with image globs so you are not evaluating every authority against every image. Set webhookTimeoutSeconds high enough (20–30s) that a slow Rekor lookup does not cause a spurious denial under failurePolicy: Fail, but not so high that a genuinely-down controller stalls scheduling for minutes.
The trade-off to name explicitly: fail-closed availability vs. integrity. failurePolicy: Fail means a verification outage blocks deploys; Ignore means an outage silently disables your supply-chain control. There is no free lunch — the right answer is fail-closed plus an HA controller (multiple replicas, PDB, regional Rekor) so the outage rarely happens, with a documented, audited break-glass for when it does.
Reproducible builds vs. throughput. Hermetic/reproducible builds (Bazel, Nix) are the strongest integrity signal and the seed of independent verification, but they are slower to set up and can reduce cache hit rates if done naively. They are a recommended SLSA L3+ practice, not a level gate — adopt them where the artifact’s risk justifies the build-time cost (release binaries, base images), not everywhere.
Cost Optimization
SLSA’s direct compute cost is negligible; the real costs are operational and avoidable.
- Don’t attest every build. Generating L3 provenance on every PR burns runner-minutes for evidence nobody verifies. Gate provenance generation on tag/release events. For a busy monorepo this alone can cut attestation runner-minutes by 20–50× versus naive per-push attestation.
- Self-hosted Rekor egress vs. public Sigstore. Public Sigstore is free but every admission verification that hits
rekor.sigstore.devis cross-internet egress and a rate-limit exposure. At scale, a private Rekor inside your VPC removes the egress and the dependency — but adds the run cost of a small HA stateful service. The crossover is roughly when verification QPS or availability requirements make the public instance’s rate limits and latency a problem; below that, public Sigstore is the cheaper choice. - Attestation storage. Storing attestations as OCI referrers means they live in your registry and count against registry storage; they are small (KBs) but multiply across every artifact and every rebuild. Apply registry retention/GC to attestations of superseded digests the same way you GC old image layers.
- Cross-AZ verification traffic. If your admission controller and registry are in different AZs, every image-pull-for-verification is cross-AZ data transfer. Co-locate the policy controller with the registry mirror, or use a pull-through cache, to keep verification traffic intra-AZ.
- The hidden cost is engineer time, not cloud spend. The documented adoption blockers (Tamanna et al.’s analysis of 1,500+ GitHub issues found toolchain gaps, ambiguous spec language, and organizational inertia as the top three) mean the dominant cost is integration and enablement, not infrastructure. Budget for a platform engineer to own the golden-path workflow and the root of trust, and for developer documentation — that investment dwarfs the compute line item.
Monitoring & Observability
Treat the supply-chain controls as a production system with golden signals.
Latency — admission-webhook verification duration (p50/p95/p99). Alert if p99 exceeds ~5s sustained; that usually means Rekor lookups are not cached or the controller is under-replicated. Traffic — attestation generation rate (per build) and verification rate (per admission). A sudden drop in generation often means a workflow lost its id-token: write permission. Errors — verification failure rate, split by reason: signature-invalid, identity-mismatch, no-attestation-found, source-mismatch. These have very different meanings — a spike in “no-attestation-found” is usually a rollout regression (a team shipped without attesting), while a spike in “identity-mismatch” or “source-mismatch” is a potential attack or a misconfigured root of trust and should page. Saturation — policy-controller CPU/memory and webhook queue depth; a saturated controller under failurePolicy: Fail stalls all scheduling.
What to alert on, and at what threshold:
- Page: any admission denial with reason
source-mismatchoridentity-mismatchon a production namespace (potential tampering — these should be ~0 in steady state). - Page: policy-controller availability < 2 healthy replicas (you are one failure from blocking all deploys under fail-closed).
- Ticket:
no-attestation-founddenial rate > 0 in an enforced namespace (a team regressed; onboard them). - Track, don’t page:
dependencyLevelsL0 count trend — your transitive-dependency coverage gap; a slowly rising L0 share signals new unattested dependencies entering the graph.
Logs and tracing. Aggregate the policy controller’s per-admission decision logs (the GitHub CLI and policy-controller both now emit which policies were evaluated and why a verdict was reached — capture that). The Rekor transparency log is itself an audit trail: every signing event is queryable, so “show me every artifact signed by this workflow identity in the last 24h” is a Rekor query, not a log-scraping exercise. For deeper supply-chain queries — “which running images depend on a component from this compromised SBOM” — feed attestations and SBOMs into GUAC, which builds a queryable graph across provenance, SBOMs, and vuln data.
Troubleshooting Guide
Real failures, pulled from the GitHub issue trackers, the spec’s own threat catalog, and production incident patterns.
| Problem | Symptoms | Root Cause | Solution |
|---|---|---|---|
| Keyless verify finds nothing | cosign verify (keyless) fails: “no matching signatures: empty key” | Image was signed with a long-lived key, not keyless; no Fulcio cert or Rekor bundle attached | Re-sign keyless (OIDC → Fulcio → Rekor), or verify with the correct --key; migrate to the keyless + Rekor pattern admission tools expect |
| Provenance valid but build was malicious | Verification PASSES yet artifact is bad | Verified the signature but did not pin --source-uri / identity; provenance honestly attests a build from evilfork/repo | Always pass --source-uri (slsa-verifier) or --certificate-identity-regexp + --certificate-oidc-issuer (cosign); enforce source in admission policy |
| SBOM satisfies a provenance gate | An image with only an SBOM passes a “must have provenance” check | Older gh attestation verify passed on any attestation; or policy predicateType too loose | Update GitHub CLI (now defaults to provenance predicate); pin exact predicateType: https://slsa.dev/provenance/v1 in policy |
id-token permission error | Action fails: “unable to get ID token” / 403 from Fulcio | Job missing permissions: id-token: write | Add id-token: write to the signing job’s permissions; it is not granted by default |
| externalParameters unverifiable | Cannot write a stable expectation; values change every build | buildType is too low-level (e.g. external params record the literal command list) | Choose a higher-level buildType that puts commands in an in-repo config and records only the source repo in externalParameters |
| Admission blocks everything after enable | All pods denied post-rollout, including system pods | failurePolicy: Fail with controller not yet ready, or no namespace exemptions | Exempt kube-system/controller namespace; roll out in Warn/audit first; ensure controller HA before Enforce |
| Rekor lookup timeouts | Intermittent admission denials under load | Every admission hits public Rekor; rate-limited or slow | Cache inclusion proofs / log checkpoint; bundle attestation with image; consider private Rekor |
| Tag-based attestation drift | Verified image differs from what runs | Attested/deployed by mutable tag; tag repointed | Attest and deploy by @sha256: digest only, never by tag |
| Source-track VSA absent | sourceLevels missing from VSA | Branch lacks required reviews / protection, so the platform cannot attest the control | Configure branch protection + required reviews; for platform-agnostic L3 use gittuf’s RSL |
| Reusable-workflow builder mismatch | slsa-verifier rejects: builder id not recognized | --builder-id tag does not match the generator version actually used | Pin --builder-id to the exact generator_generic_slsa3.yml@refs/tags/vX.Y.Z used in the workflow |
| Provenance verifies, digest doesn’t match | ”subject digest mismatch” | Verifying a rebuilt/re-tagged artifact whose bytes differ from the attested subject | Verify the exact bytes that were attested; do not rebuild between attest and verify |
| Cross-fork review bypass | Unauthorized change reaches main despite review rules | Change reviewed in a fork by a colluder, then merged upstream | Require review on the upstream merge itself (context-specific approvals), not just on the fork |
Common Mistakes
- Verifying the signature but not the identity. The single most dangerous error. A valid Sigstore signature only means someone signed via public Fulcio. Without pinning the OIDC issuer and the workflow/repo subject, you admit any artifact anyone has ever signed. Always pin both.
- Not pinning the source. Closely related: a genuine provenance from
evilfork/my-appwill verify perfectly. The expectations check onexternalParameters(source repo/ref) is what makes provenance meaningful. Skipping it reduces SLSA to expensive theater. - Trusting tags instead of digests. Attesting or deploying
:latestor:v1.2.3and assuming immutability. Tags are mutable references; attestations bind to digests. Resolve to@sha256:…everywhere — in attestation, policy, and deployment. - Treating SLSA as a scanner replacement. SLSA proves build integrity; it does not tell you whether the code has a CVE. It is orthogonal to SCA/SBOM/VEX, not a substitute. A perfectly-attested artifact can still contain Log4Shell.
- Jumping straight to L3. L3 usually requires real platform changes. Teams that skip the L1→L2 on-ramp stall and ship nothing. Get provenance flowing (L1), get it signed by a hosted platform (L2), then isolate (L3).
- Generating provenance but never verifying it. Unverified provenance is a JSON file nobody reads. The value is entirely on the verification/enforcement side. Wire up the admission gate or you have spent effort for an audit checkbox.
- Forgetting the build-platform-insider gap. Assuming L3 means “fully secure.” It explicitly does not cover a compromised build platform. Your defense there is which builders you admit to the root of trust, not the level number.
- Letting policy/root-of-trust changes bypass review. The verifier’s expectations are a security control; an attacker who can quietly add
evil/repoto the allowlist wins. Require two-party review on those changes — the spec calls this out as a distinct threat. - Over-broad
externalParametersallowances. Accepting arbitrary user inputs (e.g. compiler flags viaworkflow_dispatchinputs) lets an attacker inject behavior through a parameter while the provenance still “matches.” Reject unrecognized parameters; allow only known-safe ranges.
Best Practices Checklist
Run down this list before flipping enforcement on in production:
- Provenance is generated for every release artifact (Build L2 minimum; L3 for high-risk artifacts).
- All signing is keyless via OIDC, or keys are HSM/KMS-backed and isolated from build steps.
- Verification pins both the OIDC issuer and the workflow/repo identity.
- Verification pins the expected source (
--source-uri/externalParametersrepo check). - Everything references images/artifacts by
@sha256:digest, never mutable tags. - Attestations are bundled with artifacts (OCI referrers / index-hosted), not fetched out-of-band at verify time.
- Admission enforcement (policy-controller or Kyverno) is deployed and was rolled out in audit mode first.
-
failurePolicy: Failis set, with HA controller (≥2 replicas, PDB) and system-namespace exemptions. - The root of trust is version-controlled and changes require two-party review.
- Monitoring covers verification latency, failure-by-reason, controller availability, and L0 dependency trend.
- A documented, audited break-glass exists for verification-path outages.
- Source Track controls (branch protection, required reviews) are configured where source integrity is in scope.
- Attestation retention/GC is configured in the registry.
- SLSA evidence is mapped to your compliance obligations (SSDF PS.1–PS.3 / CRA / internal).
Alternative Approaches
SLSA is one framework in a stack of complementary standards. The mistake is treating them as competitors; they answer different questions. This table evaluates SLSA against the things teams actually weigh it against.
| Framework / Tool | Question it answers | Scope | When to choose / how it relates to SLSA |
|---|---|---|---|
| SLSA | Was this artifact built by a trusted process from a trusted source, untampered? | Build + source integrity | The build-integrity scaffold. Choose when you need provenance and tamper-evidence, not vulnerability data. Foundation the others sit on. |
| SBOM (SPDX / CycloneDX) | What components are in this artifact? | Composition inventory | Complementary, not alternative. SBOM is the ingredient list; SLSA is the manufacturing record. You want both — and SLSA can attest the SBOM. |
| in-toto | (Lower layer) How are attestations structured and verified end-to-end? | Attestation format + layout policy | Not an alternative — SLSA provenance is an in-toto predicate. Use in-toto layouts directly when you need multi-step supply-chain policies beyond build. |
| Sigstore (cosign/Fulcio/Rekor) | Who signed this, and is it logged? | Signing + transparency | The signing toolchain SLSA relies on. Not a framework; the mechanism that makes SLSA attestations verifiable without key management. |
| VEX | Is a known CVE in this artifact actually exploitable? | Vulnerability exploitability | Orthogonal. SLSA says nothing about CVEs; VEX says nothing about build integrity. Pair with SCA. |
| NIST SSDF (SP 800-218) | What secure practices does the producer follow? | Process maturity (broad) | SLSA is a concrete, automatable way to evidence several SSDF practices (PS.1–PS.3). SSDF is the practice checklist; SLSA is machine-verifiable proof for a subset. |
| OpenSSF Scorecard | How healthy are this repo’s security practices? | Repo hygiene heuristics | Complementary signal for the source side; partial coverage of maintainer-hygiene threats SLSA’s Source Track formalizes. |
| Reproducible Builds | Does the same source always yield byte-identical output? | Independent verifiability | A recommended SLSA L3+ practice and the strongest integrity signal; enables independent rebuild-and-compare. Higher setup cost. |
The decision rule: if your question is “can I trust the path from source to binary,” that is SLSA. If it is “what’s inside” (SBOM), “is the known bug reachable” (VEX/SCA), or “what practices does the vendor follow” (SSDF), you need the corresponding companion — and in a mature program you run all of them, with SLSA provenance as the trust anchor the others attach to.
Industry Use Cases
- Startups. Turn on
actions/attest-build-provenance(Build L2) on day one — five lines, free, and it makes “where did this come from” answerable before the team is large enough to have forgotten. Cheap insurance that scales. - Enterprises. Centralize a root of trust and admission enforcement across hundreds of clusters; map provenance to SSDF for the CISA attestation and to CRA for EU market access. The org-wide gate (“nothing deploys unless built by our pipeline from our repos”) is the headline control.
- SaaS. Gate continuous deployment on provenance so a compromised CI runner cannot ship a malicious image to production — the runner cannot mint a fresh OIDC identity for the release workflow, so its image fails admission.
- FinTech. L3 isolation plus private Sigstore for confidentiality and availability; provenance becomes audit evidence for regulators and a fast blast-radius tool during incident response, where minutes matter for trading systems.
- Healthcare. Build integrity for medical-device and health-platform software intersecting FDA cyber-device expectations and HIPAA-adjacent integrity requirements; provenance proves the deployed firmware/image matches reviewed source.
- AI / ML platforms. Extend provenance to model artifacts and training pipelines — sign and attest model weights and container images, verify provenance of pre-trained models pulled from public hubs (over 100 backdoored LLMs were found on Hugging Face in 2024), and treat model registries with the same suspicion as npm/PyPI.
Future Trends
Grounded in the spec roadmap and ecosystem signals, the next 3–5 years:
- Source Track operationalization. v1.2 made the Source Track normative in November 2025, but tooling is the gap — the
source-tooland reusable actions are being funded to reach full v1.2 conformance (capturing code reviewers in source provenance, attesting merge commits, pluggable non-Sigstore signing). Expect GitHub/GitLab native Source VSAs and gittuf-based platform-agnostic Source L3 to mature into something you can adopt in a sprint. - Build Environment Track. In draft now. It pushes integrity below the build process to the build VM itself: remote attestation via Secure Boot, vTPM boot measurements, and TEEs so the control plane can prove the environment was bootstrapped from an expected image with remote-access agents disabled. This closes part of the build-platform-insider gap L3 explicitly leaves open.
- Dependency Track. Also drafting. It formalizes secure ingestion of third-party build dependencies — producer-controlled mirrors, secure ingestion policy, verifiable builder provenance for dependencies — to address dependency confusion, typosquatting, and protestware (node-ipc,
colorsincidents) that the Build Track only makes visible, not prevented. - Regulatory pull becomes regulatory push. EU CRA conformity (full by December 2027) and the maturing CISA attestation regime turn SLSA-aligned evidence from “nice to have” into procurement and market-access gates. Expect VSAs in the bundle formats that conformity-assessment bodies and self-attestation reviewers consume.
- Verification ergonomics and graphs. GUAC-style aggregation (provenance + SBOM + VEX into a queryable graph) and policy engines (AMPEL, Kyverno, policy-controller) will make “show me every running image whose source is only Build L0 or whose top-level revision is Source L1” a one-line query rather than an investigation.
Key Takeaways
- SLSA produces verifiable build provenance — tamper-evident proof that an artifact came from a specific source via a specific build. It is not a scanner, not an inventory, not a CVE checker; it is the trust anchor those attach to.
- The current spec is v1.2 (approved November 2025): Build Track L0–L3 (stable since v1.0) plus the now-normative Source Track. The old “SLSA 1–4” model is obsolete — do not cite it.
- The levels buy increasing assurance: L1 provenance exists (forgeable), L2 hosted platform signs it (detects post-build tampering), L3 signing material is isolated from build steps (forging requires breaking the platform). Adopt incrementally; do not jump to L3.
- A SLSA provenance is an in-toto Statement carrying the
slsa.dev/provenance/v1predicate, DSSE-signed, typically via Sigstore keyless (Fulcio + Rekor) so there is no long-lived key to steal. - Verification is the whole point, and it has two non-negotiable checks: pin the signing identity (OIDC issuer + workflow/repo) and pin the expected source (
externalParameters). Skipping either reduces SLSA to theater. Reference everything by digest, not tag. - Enforce at admission, fail-closed with an HA controller. The root of trust is your most security-critical config — version it and gate changes with two-party review.
- Be honest about scope: SLSA does not stop a malicious trusted maintainer or a compromised build platform insider, and it does not find CVEs. It dramatically shrinks where attackers can hide and turns incident response from weeks into minutes — which, against SolarWinds-, Codecov-, and xz-class attacks, is the difference that matters.
FAQ
1. Is SLSA v1.0’s four-level (L1–L4) model still current? No. v0.1 used an unnamed track numbered SLSA 1–4. v1.0 (2023) restructured into tracks and renumbered the Build Track to L0–L3; the old L4 (hermetic + reproducible) became a recommended practice, not a level. The current approved spec is v1.2 (November 2025). Content citing “SLSA 1–4” is pre-2023 and stale.
2. Does SLSA Build L3 mean my software is secure? No. L3 means the build process is isolated and the provenance is hard to forge. It explicitly does not cover compromise of the build platform itself (a platform insider), it does not address malicious-but-trusted maintainers (threat “A”), and it says nothing about vulnerabilities in your code. It is one layer; pair it with source controls, SCA, and SBOMs.
3. What is the practical difference between L2 and L3 on GitHub Actions?
L2 (actions/attest-build-provenance) signs provenance from within your workflow’s own context — a sufficiently privileged malicious step could in principle influence it. L3 (slsa-github-generator reusable workflows) generates provenance in a separate, tenant-inaccessible job, so the signing material is unreachable from your build steps. L2 is five lines; L3 adds an isolated job and a runner spin-up.
4. Why keyless signing instead of a managed signing key? Because there is no long-lived secret to leak. Keyless uses an ephemeral key bound to an OIDC identity via Fulcio; the key exists for seconds and is discarded, with the event logged to Rekor. A leaked long-lived signing key is catastrophic — it forges trust itself. Use HSM/KMS keys only when you need an offline release root or run private Sigstore without workload identity.
5. What is the single most common verification mistake?
Verifying that a Sigstore signature is valid without pinning the identity (OIDC issuer + workflow/repo subject). A valid signature only proves “someone signed via public Fulcio.” Without identity pinning you admit anything anyone has ever signed. Always pin --certificate-oidc-issuer and --certificate-identity-regexp.
6. Would SLSA have prevented the xz-utils backdoor? Not directly — the malice was committed by a trusted maintainer (threat “A”), which even two-party review can’t reliably stop. But the backdoor lived in the released tarball, not in the corresponding git state. A SLSA discipline of building from the canonical tagged commit (not a maintainer-uploaded tarball) plus reproducible builds is exactly the control that surfaces a tarball-vs-repo divergence, and provenance would have made the post-disclosure blast-radius search minutes instead of weeks.
7. How is SLSA different from an SBOM? An SBOM lists what is in an artifact (ingredients). SLSA provenance records how it was built and proves it wasn’t tampered with (manufacturing record). They’re complementary — you want both, and SLSA can attest the SBOM itself. SolarWinds had a valid signature and would have had a clean-looking SBOM; the compromise was in the build, which only provenance catches.
8. What does the DSSE PAE protect against?
DSSE signs over a Pre-Authentication Encoding that includes the payload type alongside the payload (DSSEv1 SP LEN(type) SP type SP LEN(body) SP body). This prevents an attacker from taking bytes signed as one content type and presenting it as another. It’s why you can’t verify provenance by hashing the JSON — tooling must reconstruct the PAE.
9. How do I verify provenance at deploy time in Kubernetes?
Use an admission controller: Sigstore policy-controller (ClusterImagePolicy) or Kyverno (verifyImages). Both intercept pod creation, verify the image has a SLSA provenance attestation (predicateType: https://slsa.dev/provenance/v1) from the expected keyless identity, and check externalParameters against your expectations. Pods without valid provenance are rejected at admission.
10. Should admission verification fail open or fail closed?
Fail closed (failurePolicy: Fail) for real security — but only with an HA controller (≥2 replicas, PodDisruptionBudget, regional/cached Rekor) so a verification outage rarely blocks deploys, plus a documented break-glass. Fail open silently disables your control during an outage, which is exactly when an attacker would want it disabled.
11. What’s in the externalParameters field and why does it matter so much?
It’s the untrusted external input to the build — for a GitHub build, the source repo, ref, and workflow path. Verification compares these to your expectations for an official release. If you can’t form a stable expectation (the values change every build), your buildType is too low-level; redesign it to record the source repo and keep volatile inputs in an in-repo config file.
12. Do I need Sigstore to do SLSA? No, but it’s the path of least resistance. SLSA requires signed provenance (L2+); Sigstore is the dominant way to sign without managing keys. You can use your own keys/HSM and your own transparency log, or a private Sigstore stack for air-gapped/regulated environments. The spec is tool-agnostic about the signing mechanism.
13. How does SLSA map to EO 14028 / SSDF compliance? SSDF (NIST SP 800-218) practices PS.1 (protect code from tampering), PS.2 (verify release integrity), and PS.3 (archive each release) are most cleanly evidenced by L2+ signed provenance, published verification instructions, and retained attestations. The CISA Secure Software Development Attestation Form reads against SSDF, so SLSA artifacts are the concrete evidence behind those self-attestations.
14. What is a VSA and when should I issue one? A Verification Summary Attestation records that a trusted verifier checked an artifact against a policy and reached a verdict (PASSED/FAILED, verified levels, dependency-level histogram). Issue one so downstream consumers can trust the verdict without re-running verification or accessing every underlying attestation — and to keep your pipeline details confidential while still proving verification happened.
15. How do I handle transitive dependencies that are SLSA Build L0?
Realistically, most of your transitive graph is L0 today. SLSA v1.0+ imposes no requirement to verify resolvedDependencies for the Build level, so use a trimming heuristic / exception mechanism, track the L0 share via the VSA’s dependencyLevels histogram, and drive it down over time as the ecosystem (npm provenance, PyPI PEP 740) raises the floor. Don’t let perfect dependency coverage block shipping build provenance.
16. What does the new Source Track actually require? Source L1 retains identifiable history; L2 adds source provenance and change tracking; L3 adds continuous enforcement of technical controls (required reviews, branch protection) that is itself attested, so a consumer can verify enforcement without trusting the forge’s UI at one moment. SLSA approximates producer intent as two-party review, with explicit mitigations for review-bypass tricks.
17. Can I reach Source L3 without GitHub or GitLab native support?
Yes — gittuf (OpenSSF Incubating since June 2025) stores policy and enforcement records in a Reference State Log inside the repository, giving a platform-agnostic path to Source L3 that a verifier can check without trusting the forge. GitHub’s attest-build-provenance and GitLab Build Cloud also emit source-level VSAs when required reviews are configured.
18. Why must I verify by digest instead of tag?
Tags are mutable references; an attacker (or a careless force-push) can repoint :v1.2.3 to different bytes. Attestations bind to immutable @sha256: digests. If you deploy by tag, the bytes that run can differ from the bytes that were attested. Resolve to digests in attestation, policy, and deployment manifests.
19. What’s the performance impact of admission-time verification?
L2 signing is sub-second. Admission verification adds a Fulcio cert check, a Rekor inclusion-proof lookup, and policy evaluation per pod. Keep p99 under control by bundling attestations with images (no extra fetch), caching Rekor proofs/log checkpoints, and scoping policies with tight image globs. Set webhookTimeoutSeconds to 20–30s so a slow lookup doesn’t cause spurious denials under fail-closed.
20. Where does SLSA stop, and what do I pair it with? SLSA stops at build and source integrity. It does not detect CVEs (pair with SCA + VEX), does not inventory components (pair with SBOM), does not stop typosquatting/dependency-confusion (future Dependency Track; today use registry controls and behavioral analysis like Socket/Phylum), and does not defend against a build-platform insider (manage via your root of trust; future Build Environment Track). It is the integrity anchor of a layered program, not the whole program.
SEO Metadata
SEO Title: SLSA v1.2 in Production: Build Provenance Deep Dive
Meta Description: A production guide to SLSA v1.2 — Build & Source Tracks, in-toto provenance, Sigstore keyless signing, and Kubernetes admission-time verification with real configs.
URL Slug: slsa-v1.2-production-build-provenance-guide
Primary Focus Keyword: SLSA framework
Secondary Focus Keywords: SLSA build provenance, SLSA v1.2 Source Track, in-toto attestation, Sigstore keyless signing, cosign verify provenance, SLSA Kubernetes admission, software supply chain security, build integrity attestation
FAQ Schema (JSON-LD)
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Is SLSA v1.0's four-level (L1-L4) model still current?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. v0.1 used an unnamed track numbered SLSA 1-4. v1.0 (2023) restructured into tracks and renumbered the Build Track to L0-L3, and the old L4 became a recommended practice rather than a level. The current approved spec is v1.2 (November 2025), which added the Source Track. Content citing 'SLSA 1-4' is pre-2023 and stale."
}
},
{
"@type": "Question",
"name": "Does SLSA Build L3 mean my software is secure?",
"acceptedAnswer": {
"@type": "Answer",
"text": "No. L3 means the build process is isolated and the provenance is hard to forge. It does not cover compromise of the build platform itself, does not address malicious trusted maintainers, and says nothing about vulnerabilities in your code. It is one layer to pair with source controls, SCA, and SBOMs."
}
},
{
"@type": "Question",
"name": "What is the most common SLSA verification mistake?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Verifying that a Sigstore signature is valid without pinning the identity (OIDC issuer plus workflow/repo subject). A valid signature only proves someone signed via public Fulcio; without identity pinning you admit anything anyone has ever signed. Always pin the OIDC issuer and the identity, and also pin the expected source repository."
}
},
{
"@type": "Question",
"name": "Would SLSA have prevented the xz-utils backdoor?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Not directly, because the malice was committed by a trusted maintainer. But the backdoor lived in the released tarball, not the git state, so building from the canonical tagged commit plus reproducible builds is exactly the control that surfaces a tarball-vs-repo divergence. Provenance would also have made the blast-radius investigation minutes instead of weeks."
}
},
{
"@type": "Question",
"name": "How is SLSA different from an SBOM?",
"acceptedAnswer": {
"@type": "Answer",
"text": "An SBOM lists what is inside an artifact (ingredients); SLSA provenance records how it was built and proves it was not tampered with (manufacturing record). They are complementary, and SLSA can attest the SBOM itself. A valid signature and a clean SBOM would not have caught SolarWinds, because that compromise was in the build, which only provenance catches."
}
},
{
"@type": "Question",
"name": "How do I verify SLSA provenance at deploy time in Kubernetes?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Use an admission controller such as Sigstore policy-controller (ClusterImagePolicy) or Kyverno (verifyImages). Both intercept pod creation, verify the image has a SLSA provenance attestation from the expected keyless identity, and check externalParameters against your expectations. Pods without valid provenance are rejected at admission, ideally fail-closed with an HA controller."
}
}
]
}
Strategic Internal & External Linking
External (authoritative): link to the SLSA specification (slsa.dev/spec/v1.2), the SLSA threats page, the in-toto Attestation Framework, Sigstore docs (Fulcio/Rekor/Cosign), GitHub Artifact Attestations docs, PyPI PEP 740 docs, the Kyverno and Sigstore policy-controller docs, and NIST SP 800-218 (SSDF) for the compliance mapping.
Internal cross-links: pair this with companion pieces on SBOM generation (Syft/CycloneDX), Sigstore keyless signing in CI/CD, Kubernetes admission control with Kyverno/OPA Gatekeeper, container image hardening and distroless builds, GitHub Actions OIDC and least-privilege workflow permissions, and EU CRA / EO 14028 supply-chain compliance.