After the SolarWinds breach, the XZ Utils backdoor, and multiple GitHub Actions compromises, the question “where did this artifact come from?” has moved from theoretical to urgent. SLSA (Supply-chain Levels for Software Artifacts) is a framework for answering it - and GitHub Actions now makes Level 3 achievable for most projects without custom build infrastructure.
What SLSA Actually Means
SLSA defines four levels of supply chain integrity, focused on build provenance - a signed, verifiable statement that a specific artifact was produced by a specific build process from a specific source.
| Level | Key requirement |
|---|---|
| SLSA 1 | Build process is scripted; provenance exists |
| SLSA 2 | Build service is hosted; provenance is signed |
| SLSA 3 | Build environment is hardened; provenance is non-falsifiable |
| SLSA 4 | Two-party review; hermetic builds |
Level 3 is the practical target for most teams. It means: the provenance was generated by a build service you don’t control (GitHub Actions), is signed using Sigstore’s keyless infrastructure, and contains a verifiable link back to the source commit. An attacker who compromises your developer machine cannot produce a valid Level 3 provenance - only the build service can.
Generating Provenance for Container Images
The slsa-framework/slsa-github-generator provides reusable workflows for generating SLSA provenance.
name: Build and Push
on:
push:
branches: [main]
release:
types: [published]
permissions: {}
jobs:
build:
name: Build Image
outputs:
image: ${{ steps.build.outputs.image }}
digest: ${{ steps.build.outputs.digest }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Log in to GHCR
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
id: build
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
provenance:
name: Generate SLSA Provenance
needs: build
permissions:
actions: read
id-token: write
packages: write
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 }}
This produces a provenance attestation pushed to the same registry as your image, signed via Sigstore’s keyless infrastructure (GitHub OIDC → Fulcio CA → Rekor transparency log).
Generating Provenance for Generic Artifacts
For binaries, packages, or deployment zips:
jobs:
build:
outputs:
artifacts: ${{ steps.build.outputs.artifacts }}
hashes: ${{ steps.hash.outputs.hashes }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build
id: build
run: |
make release
echo "artifacts=dist/*.tar.gz" >> "$GITHUB_OUTPUT"
- name: Hash artifacts
id: hash
run: |
set -euo pipefail
hashes=$(sha256sum dist/*.tar.gz | base64 -w0)
echo "hashes=$hashes" >> "$GITHUB_OUTPUT"
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: true
Verifying Provenance Before Deployment
Generating provenance is only half the story. You need to verify it before consuming the artifact.
# Install slsa-verifier
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest
# Verify a container image
slsa-verifier verify-image \
ghcr.io/org/repo:sha-abc123 \
--source-uri github.com/org/repo \
--source-branch main
# Verify a binary artifact
slsa-verifier verify-artifact \
my-app-v1.0.0.tar.gz \
--provenance-path my-app-v1.0.0.tar.gz.intoto.jsonl \
--source-uri github.com/org/repo
Add this verification step to your deployment pipeline before the artifact reaches production:
- name: Verify provenance
run: |
slsa-verifier verify-image \
${{ env.IMAGE }}@${{ env.DIGEST }} \
--source-uri github.com/${{ github.repository }} \
--source-branch main
What Provenance Actually Contains
The provenance attestation is a signed DSSE envelope containing an in-toto statement. Key fields:
{
"subject": [{
"name": "ghcr.io/org/repo",
"digest": {"sha256": "abc123..."}
}],
"predicate": {
"buildType": "https://github.com/slsa-framework/slsa-github-generator/...",
"builder": {
"id": "https://github.com/slsa-framework/slsa-github-generator/..."
},
"invocation": {
"configSource": {
"uri": "git+https://github.com/org/repo@refs/heads/main",
"digest": {"sha1": "commit-sha"},
"entryPoint": ".github/workflows/build.yml"
}
}
}
}
The builder.id is a GitHub Actions workflow URI. Because it’s signed by Sigstore using GitHub’s OIDC token, only a job running in GitHub Actions can produce a signature that chains back to this identity. Your developer machine cannot forge it.
The Practical Value
SLSA provenance doesn’t prevent all supply chain attacks. It answers a specific question: “Was this exact artifact built by this exact CI pipeline from this exact commit?” That’s valuable for:
- Incident response: When a suspicious artifact surfaces, you can immediately determine if it came from your pipeline or was injected elsewhere
- Compliance: Regulators increasingly ask for software build evidence; a signed provenance is concrete, auditable proof
- Deployment gates: Block any artifact without valid provenance from reaching production environments
Level 3 is achievable today with GitHub Actions. The slsa-github-generator workflows handle the complexity. The implementation is a day’s work. The audit trail it creates is permanent.