Best Practices

How to Implement SLSA Level 3 Practically

SLSA Level 3 requires hardened builds, verifiable provenance, and isolated build environments. Here is the practical path, not the theoretical one.

Shadab Khan
Security Engineer
7 min read

SLSA (Supply-chain Levels for Software Artifacts) is the framework the rest of the industry has coalesced around for describing how trustworthy a build is. Level 1 is "we have some build scripts." Level 2 is "builds happen in a known system with hosted provenance." Level 3 is where the framework starts to have real teeth: hardened builds, non-falsifiable provenance, and isolation between build steps. Level 4 adds hermetic and reproducible builds; most shops cannot get there without changing their architecture, so Level 3 is where the compliance and procurement pressure actually lands in 2026.

This post is the working checklist I walk engineering teams through when the board has asked for SLSA 3 attestations by the next quarterly review.

What Does SLSA 3 Actually Require?

SLSA 3 requires that builds run on a hosted build platform, produce signed provenance describing how the artifact was built, run with isolation between build runs, and that provenance is non-falsifiable — meaning even a malicious build-step author cannot forge the provenance document. In practice this maps to: a CI platform you do not operate, a signing identity you cannot tamper with from inside the build, and a build environment that resets between runs.

What Changes Between SLSA 2 and SLSA 3?

The jump is almost entirely about non-falsifiable provenance and isolation. At SLSA 2 the build produces provenance; at SLSA 3 the provenance must be generated by a trusted component that the build itself cannot influence. This is why SLSA 3 on GitHub Actions is usually implemented using the slsa-framework/slsa-github-generator — it runs in a separate reusable workflow whose identity a compromised build step cannot impersonate.

Step-by-Step Implementation

Step 1: Move Builds Off Self-Hosted Runners (or Harden Them)

Self-hosted runners are the single biggest barrier to SLSA 3 for most shops. A persistent runner that serves multiple build jobs violates the isolation requirement because a compromised build can leave behind tooling for the next one. Either move to hosted runners (GitHub-hosted, GitLab SaaS, Buildkite managed agents) or switch self-hosted runners to ephemeral mode via Actions Runner Controller with a fresh VM or pod per job.

Step 2: Use a Trusted Reusable Workflow for Provenance

The trusted workflow pattern is what makes provenance non-falsifiable in GitHub Actions. Your build calls a reusable workflow that runs in a separate job with its own OIDC identity, and only the trusted workflow has permission to write the attestation. Here is a minimum example for a Go binary:

name: release
on:
  push:
    tags: ['v*']

permissions: read-all

jobs:
  build:
    permissions:
      id-token: write
      contents: read
      actions: read
    uses: slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@v2.1.0
    with:
      go-version: '1.23'
      config-file: .slsa-goreleaser.yml
      upload-assets: true

The reusable workflow runs on its own GitHub runner, pulls the build outputs as artifacts, and signs provenance using a Fulcio certificate whose identity includes the workflow path. A downstream verifier can check the builderID field of the provenance against an allowlist of known-good workflow paths and versions.

Step 3: Lock Down Write Permissions

Every build job must declare a minimal permissions: block. SLSA 3 guidance treats an overly broad default GITHUB_TOKEN as a potential falsification vector. Set permissions: read-all at the workflow level and grant write permissions only to the specific jobs that need them.

Step 4: Pin Every Action by SHA, Not Tag

Tags are mutable; SHAs are not. Every uses: line in your workflow must reference a specific commit SHA:

- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1
- uses: actions/setup-go@0c52d547c9bc32b1aa3301fd7a9cb496313a4491   # v5.0.0

This alone closes a very common supply-chain vector — the "tag reassignment" attack where a compromised maintainer moves @v4 to point at a malicious commit. Use pin-github-action or step-security/secure-repo to automate pinning.

Step 5: Require Branch Protection for Tag-Triggered Releases

Production tags must be signed and the branch they point to must be protected. Require signed commits, require reviewers, and disable force pushes. Attach a custom ruleset that refuses tag pushes from anyone outside a release-managers team. This is how you prevent an attacker with a compromised developer token from creating a v1.2.3 tag on a hand-crafted malicious commit.

Step 6: Emit Source Provenance Alongside Build Provenance

SLSA 3 is about build provenance but real supply chain security needs source provenance too. Produce a git-source attestation linking the released commit SHA to the release tag and the review audit trail. The gitsign tool from Sigstore produces commit signatures verifiable through Rekor, which is the simplest path to source provenance that survives an audit.

Step 7: Publish and Verify the Provenance on Consumers

A provenance document nobody verifies is just JSON. Consumers of your artifacts should run:

slsa-verifier verify-artifact widget-linux-amd64 \
  --provenance-path widget-linux-amd64.intoto.jsonl \
  --source-uri github.com/acme/widget \
  --source-tag v1.4.2 \
  --builder-id "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@refs/tags/v2.1.0"

Document this in your customer-facing verification guide and wire it into every internal deploy pipeline. Provenance that is not verified provides no security guarantee.

Step 8: Enforce Provenance at Artifact Pull Time

For container images, run admission-time verification that refuses images without a valid SLSA 3 provenance attestation:

apiVersion: kyverno.io/v2beta1
kind: ClusterPolicy
metadata:
  name: require-slsa-3-provenance
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-slsa-provenance
      match:
        any:
        - resources:
            kinds: [Pod]
      verifyImages:
      - imageReferences:
        - "ghcr.io/acme/*"
        attestations:
        - type: https://slsa.dev/provenance/v1
          attestors:
          - entries:
            - keyless:
                subjectRegExp: "^https://github.com/slsa-framework/slsa-github-generator/.+"
                issuer: "https://token.actions.githubusercontent.com"
          conditions:
          - all:
            - key: "{{ predicate.buildDefinition.buildType }}"
              operator: Equals
              value: "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1"
            - key: "{{ predicate.runDetails.builder.id }}"
              operator: In
              value: ["https://github.com/slsa-framework/slsa-github-generator/.github/workflows/builder_go_slsa3.yml@refs/tags/v2.1.0"]

What Is Hermeticity and Do You Need It for Level 3?

Hermeticity means the build consumes only inputs that are explicitly declared and fetched at the start of the build — no network access during the build itself. SLSA 3 does not strictly require hermeticity, but it is the single best way to satisfy the "all dependencies declared" portion of the provenance schema. Bazel and Nix are the obvious ways in. For non-Bazel teams, "hermetic-ish" is achievable by vendoring dependencies and cutting network access after the initial fetch step.

How Do You Handle Multi-Artifact Builds?

Emit one provenance per artifact, not a single umbrella provenance. If your build produces five binaries and three containers, that is eight provenance documents. This matches the SLSA spec's subject-per-artifact model and makes verification unambiguous: a consumer pulling one binary should not have to parse provenance for seven others they did not want.

What About Monorepo Builds?

Monorepos are fine for SLSA 3 as long as the trusted workflow boundary is enforced at the per-artifact level. Each shipped artifact from the monorepo gets its own call to the trusted builder workflow. Avoid writing a single "build everything" workflow that emits a single attestation — that attestation will be over-broad and downstream verifiers will not be able to assert anything precise.

How Do You Handle Third-Party Dependencies?

Third-party dependencies are outside the SLSA level of your own build but inside the provenance your build emits. The provenance document lists them; the build platform does not assert their SLSA level. For high-assurance postures, track the SLSA level of critical upstream artifacts and pin to those with attestations where possible (e.g., Go standard library binaries, Python wheels from pypi with provenance).

How Safeguard.sh Helps

Safeguard.sh reads SLSA provenance as a first-class trust signal alongside SBOMs. Our 100-level depth reachability engine reconciles the dependency list declared in the provenance against the dependencies that are actually reachable at runtime, catching both over- and under-declared builds. Griffin AI drafts policy-as-code rules from observed provenance patterns so your admission controllers evolve as your builder versions do. TPRM tracks the SLSA level of every vendor artifact that lands in your environment, and our container self-healing runtime rebuilds, re-attests, and redeploys images through the same trusted workflow path whenever a reachable CVE forces a release — without dropping your SLSA 3 posture in the process.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.