SLSA Framework: Achieving Supply Chain Security Levels 1–4
After Log4Shell, the security community spent considerable energy debating how attackers got a vulnerable library into so many systems. The answer is uncomfortable: developers added it as a dependency, trusting that upstream had done the right thing. That trust had no cryptographic backing, no audit trail, and no way to verify it had not been violated.
SLSA (Supply-chain Levels for Software Artifacts, pronounced "salsa") is a framework from Google that makes that trust verifiable. It defines four levels of supply chain integrity, each requiring progressively stronger guarantees about how software was built. By the time you reach SLSA Level 3, any artifact you ship carries a tamper-evident provenance record that anyone can verify.
What SLSA Levels Mean
SLSA is built around three categories of requirements: Source, Build, and Provenance. Each level adds requirements on top of the previous one.
SLSA Level 1: Provenance Exists
The minimum: your build process generates a provenance document. This document records what source was used, what build system ran, and what artifact was produced. The provenance at Level 1 doesn't need to be signed or hosted anywhere tamper-evident—it just needs to exist.
What it prevents: Accidental loss of build metadata. Nothing stops an attacker. Effort to achieve: Low. Add provenance generation to your existing CI pipeline.
SLSA Level 2: Hosted Build Service, Signed Provenance
The build must run on a hosted, managed build service (GitHub Actions, Google Cloud Build, etc.)—not on a developer's laptop. The provenance must be signed by the build service.
What it prevents: Builds from uncontrolled environments. Provenance tampering after the fact. Effort to achieve: Medium. Most teams already use hosted CI; you need to add provenance signing.
SLSA Level 3: Hardened Build Platform, Non-falsifiable Provenance
The build platform must have specific security properties:
- The build is isolated (each build runs in a fresh, ephemeral environment)
- The provenance is generated and signed by the build platform itself, not by user-controlled code
- The build parameters (source repo, trigger, inputs) cannot be forged by the build script
What it prevents: Compromised build scripts falsifying their own provenance. Attackers injecting into the build environment. Effort to achieve: Moderate. Requires using a supported build system and the SLSA GitHub Actions generator.
SLSA Level 4: Two-Person Review, Hermetic Builds
All source changes require two-person review. Builds are hermetic—they fetch no dependencies at build time, only from a controlled snapshot. This is the level required for the most sensitive software.
What it prevents: Insider threats. Dependency confusion attacks. Network-based injection during builds. Effort to achieve: High. Requires significant process changes and tooling investment.
Most teams should target Level 2 as a baseline and Level 3 for production artifacts. Level 4 is realistic only for critical infrastructure or highly regulated environments.
Achieving SLSA Level 3 with GitHub Actions
The slsa-github-generator project provides reusable workflows that generate SLSA Level 3 provenance for common artifact types: Go binaries, container images, generic files, and Maven/Gradle JARs.
The key security property: the provenance is generated by a separate, isolated workflow job that cannot be influenced by user-controlled build scripts. This is what makes Level 3 non-falsifiable.
Container Image with SLSA Level 3 Provenance
# .github/workflows/build-slsa.yml
name: Build with SLSA Level 3 Provenance
on:
push:
branches: [main]
tags: ["v*"]
workflow_dispatch:
permissions: {} # Explicitly deny all permissions at top level
jobs:
# ---- Step 1: Build the container image ----
build:
permissions:
contents: read
packages: write
id-token: write
outputs:
image: ${{ steps.build.outputs.image }}
digest: ${{ steps.build.outputs.digest }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Export outputs
id: outputs
run: |
echo "image=ghcr.io/${{ github.repository }}" >> $GITHUB_OUTPUT
echo "digest=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
# ---- Step 2: Generate SLSA Level 3 provenance ----
# This job runs in isolation and cannot be influenced by the build job
provenance:
needs: [build]
permissions:
actions: read # Required to read workflow run info
id-token: write # Required for keyless signing
packages: write # Required to upload provenance to GHCR
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with:
image: ${{ needs.build.outputs.image }}
digest: ${{ needs.build.outputs.digest }}
registry-username: ${{ github.actor }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }}Go Binary with SLSA Level 3 Provenance
# .github/workflows/build-go-slsa.yml
name: Build Go Binary with SLSA Level 3
on:
push:
tags: ["v*"]
permissions: {}
jobs:
build:
permissions:
contents: read
outputs:
binary-name: ${{ steps.build.outputs.binary-name }}
hashes: ${{ steps.hash.outputs.hashes }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Build
id: build
run: |
go build -trimpath -o myapp ./cmd/myapp
echo "binary-name=myapp" >> $GITHUB_OUTPUT
- name: Generate hashes
id: hash
run: |
HASHES=$(sha256sum myapp | base64 -w0)
echo "hashes=$HASHES" >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: myapp
path: myapp
provenance:
needs: [build]
permissions:
actions: read
id-token: write
contents: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
with:
base64-subjects: "${{ needs.build.outputs.hashes }}"
upload-assets: trueUnderstanding the Provenance Document
SLSA provenance follows the in-toto attestation framework. The generated provenance document looks like this (simplified):
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "https://slsa.dev/provenance/v1",
"subject": [
{
"name": "ghcr.io/myorg/myapp",
"digest": { "sha256": "abc123..." }
}
],
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"repository": "https://github.com/myorg/myapp",
"ref": "refs/heads/main",
"path": ".github/workflows/build-slsa.yml"
}
},
"resolvedDependencies": [
{
"uri": "git+https://github.com/myorg/myapp@refs/heads/main",
"digest": { "gitCommit": "def456..." }
}
]
},
"runDetails": {
"builder": {
"id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0"
},
"metadata": {
"invocationId": "https://github.com/myorg/myapp/actions/runs/12345",
"startedOn": "2026-01-15T10:30:00Z",
"finishedOn": "2026-01-15T10:35:00Z"
}
}
}
}The critical fields:
subject.digest— the exact artifact this provenance coversexternalParameters.workflow— the source repo and exact workflow fileresolvedDependencies[0].digest.gitCommit— the exact source commitbuilder.id— the SLSA generator workflow, at a pinned tag
The provenance is signed with cosign using keyless signing, so the signature is verifiable without managing any keys.
Verifying with slsa-verifier
slsa-verifier is the reference verification tool. It checks the provenance signature, verifies the provenance against Rekor, and validates the builder identity.
Installation
# macOS
brew install slsa-verifier
# Linux
SLSA_VERIFIER_VERSION=$(curl -s https://api.github.com/repos/slsa-framework/slsa-verifier/releases/latest \
| grep '"tag_name"' | cut -d'"' -f4)
curl -sL "https://github.com/slsa-framework/slsa-verifier/releases/download/${SLSA_VERIFIER_VERSION}/slsa-verifier-linux-amd64" \
-o /usr/local/bin/slsa-verifier
chmod +x /usr/local/bin/slsa-verifierVerifying a Container Image
slsa-verifier verify-image \
ghcr.io/myorg/myapp@sha256:abc123 \
--source-uri github.com/myorg/myapp \
--source-branch main \
--builder-id "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml"Successful output:
Verified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml" at SLSA Level 3
PASSED: Verified SLSA provenanceVerifying a Go Binary
slsa-verifier verify-artifact myapp \
--provenance-path myapp.intoto.jsonl \
--source-uri github.com/myorg/myapp \
--source-tag v1.2.3Practical SLSA Implementation Checklist
Use this checklist when rolling out SLSA across your organization:
Level 1 (Week 1)
- All builds run in CI, not on developer machines
- CI generates a build log (most systems do this automatically)
- Build configuration is version-controlled
- Artifacts are named and versioned (no overwriting
latestin place)
Level 2 (Week 2–3)
- Using a hosted, managed CI system (GitHub Actions, Cloud Build)
- Added provenance generation to build workflows
- Provenance is signed (cosign keyless or key-based)
- Provenance is stored alongside the artifact (OCI attestation or release asset)
- At least one team member can run
slsa-verifierand interpret output
Level 3 (Month 2)
- Using
slsa-github-generatorreusable workflows - Build scripts cannot modify provenance generation (isolated job)
- Source repo requires branch protection (direct pushes to main disabled)
- All build dependencies pinned (actions pinned to commit SHA, not mutable tag)
- Deployment pipeline verifies SLSA provenance before running images
Level 3+ Hardening (Ongoing)
- Kyverno or OPA policies enforce provenance verification in Kubernetes
- Automated alerts for provenance verification failures in production
- Regular audit of all signing certificates in Rekor for unexpected identities
- SBOM attestation generated alongside SLSA provenance (defense in depth)
Pinning Actions to Commit SHAs
Level 3 requires that build inputs be fixed and verifiable. A major footgun is using mutable action tags:
# INSECURE: v4 can be moved to point to malicious code
- uses: actions/checkout@v4
# SECURE: commit SHA is immutable
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2Use pin-github-action or Dependabot's security updates to maintain pinned SHAs with human-readable version comments.
Integrating SLSA Verification into Deployment
A pre-deployment verification script:
#!/bin/bash
set -euo pipefail
IMAGE="${1:?Usage: verify-slsa.sh IMAGE@DIGEST}"
SOURCE_URI="${2:?Usage: verify-slsa.sh IMAGE@DIGEST SOURCE_URI}"
echo "Verifying SLSA provenance for ${IMAGE}..."
slsa-verifier verify-image \
"${IMAGE}" \
--source-uri "${SOURCE_URI}" \
--builder-id "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml"
echo "SLSA Level 3 provenance verified. Proceeding with deployment."Call this in your Helm chart pre-install hook or ArgoCD pre-sync job to make provenance verification a hard gate before any workload starts.
Why This Matters More Than You Think
SolarWinds happened because customers installed a signed update—signed with Solarwinds' own key, which the attackers had compromised. SLSA doesn't prevent key compromise, but it raises the bar: an attacker compromising your registry credentials can't forge SLSA Level 3 provenance, because the provenance is generated by an isolated build system job with a separate OIDC identity that the attacker doesn't control.
More importantly, SLSA creates an audit trail. When you can state "every container running in production has verifiable provenance tied to a specific git commit built by our authorized CI workflow," you have a foundation for incident response that most organizations currently lack.
Start at Level 1 this week. It requires almost no effort and immediately gives you build metadata you didn't have. Level 2 follows naturally. Level 3 is a month of work that pays dividends every time a supply chain incident hits the news and you can honestly say "we checked, we're not affected" within an hour.