SLSA Framework: Achieving Supply Chain Security Levels 1–4

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: true

Understanding 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 covers
  • externalParameters.workflow — the source repo and exact workflow file
  • resolvedDependencies[0].digest.gitCommit — the exact source commit
  • builder.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-verifier

Verifying 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 provenance

Verifying a Go Binary

slsa-verifier verify-artifact myapp \
  --provenance-path myapp.intoto.jsonl \
  --source-uri github.com/myorg/myapp \
  --source-tag v1.2.3

Practical 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 latest in 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-verifier and interpret output

Level 3 (Month 2)

  • Using slsa-github-generator reusable 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.2

Use 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.

Read more

Start now free