← All posts
· 4 min read ·
CI/CDSecuritySupply ChainGitHub ActionsSLSADevSecOps

SLSA Level 3 in GitHub Actions: Build Provenance Without the Complexity

SLSA (Supply-chain Levels for Software Artifacts) Level 3 ensures your build outputs are traceable to a specific source commit and build environment. GitHub Actions makes it achievable without custom infrastructure.

Provenance chain from source commit to deployed artifact

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.

LevelKey requirement
SLSA 1Build process is scripted; provenance exists
SLSA 2Build service is hosted; provenance is signed
SLSA 3Build environment is hardened; provenance is non-falsifiable
SLSA 4Two-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.

← All posts