GitHub Actions Workflow Patterns for SLSA¶
Turn SLSA provenance generation into repeatable, organization-wide patterns.
Pattern-Based Approach
These patterns are designed as reusable workflows that can be called from multiple repositories, ensuring consistent SLSA implementation across your organization.
Overview¶
This guide covers reusable GitHub Actions workflow patterns for:
- Generic artifact provenance - Binaries, tarballs, release archives
- Container image provenance - OCI images with SLSA attestation
- Verification gates - Enforce provenance validation before release
Prerequisites: Understanding of SLSA Levels and basic SLSA Implementation.
Advanced patterns: See Advanced GitHub Actions Patterns for multi-platform builds, monorepo patterns, and organization-wide adoption.
Architecture: Workflow Separation¶
graph TD
A[Build Job] -->|Artifact| B[Upload Artifact]
A -->|Hashes| C[Provenance Job]
C -->|.intoto.jsonl| D[Upload to Release]
B --> E[Verification Job]
D --> E
E -->|Pass| F[Deploy/Release]
E-->|Fail| G[Block]
%% Ghostty Hardcore Theme
style A fill:#65d9ef,color:#1b1d1e
style C fill:#a7e22e,color:#1b1d1e
style E fill:#ffd866,color:#1b1d1e
style F fill:#a9dc76,color:#1b1d1e
style G fill:#f92572,color:#1b1d1e
Critical pattern: Build, provenance generation, and verification are separate jobs with minimal permissions each.
Pattern 1: Generic Artifact Provenance (Reusable)¶
Create .github/workflows/slsa-generic-provenance.yml as a reusable workflow:
name: SLSA Generic Provenance
on:
workflow_call:
inputs:
artifact-name:
description: 'Name of artifact to download'
required: true
type: string
artifact-path:
description: 'Path pattern to hash (e.g., dist/*)'
required: true
type: string
upload-to-release:
description: 'Upload provenance to GitHub release'
required: false
type: boolean
default: true
permissions: {}
jobs:
provenance:
permissions:
actions: read # Download artifact
id-token: write # OIDC token for signing
contents: write # Upload to release (if enabled)
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact-name }}
path: artifacts/
- name: Generate artifact hashes
id: hash
run: |
cd artifacts
sha256sum ${{ inputs.artifact-path }} | base64 -w0 > ../hashes.txt
echo "hashes=$(cat ../hashes.txt)" >> "$GITHUB_OUTPUT"
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0
with:
base64-subjects: "${{ steps.hash.outputs.hashes }}"
upload-assets: ${{ inputs.upload-to-release }}
Using the Reusable Workflow¶
name: Release
on:
push:
tags: ['v*']
permissions: {}
jobs:
build:
permissions:
contents: read
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build artifacts
run: make build
- uses: actions/upload-artifact@v4
with:
name: release-artifacts
path: dist/
provenance:
needs: [build]
uses: ./.github/workflows/slsa-generic-provenance.yml
permissions:
actions: read
id-token: write
contents: write
with:
artifact-name: release-artifacts
artifact-path: '*'
upload-to-release: true
Benefits: Single reusable workflow for all repositories, consistent SLSA implementation, job-level permissions isolation, easy to update organization-wide.
Pattern 2: Container Image Provenance (Reusable)¶
Create .github/workflows/slsa-container-provenance.yml:
name: SLSA Container Provenance
on:
workflow_call:
inputs:
image:
description: 'Container image reference (registry/repo:tag)'
required: true
type: string
secrets:
registry-username:
required: true
registry-password:
required: true
permissions: {}
jobs:
provenance:
permissions:
packages: write
id-token: write
runs-on: ubuntu-latest
steps:
- name: Login to registry
run: |
echo "${{ secrets.registry-password }}" | \
podman login ghcr.io \
--username "${{ secrets.registry-username }}" \
--password-stdin
- name: Generate image hash
id: hash
run: |
IMAGE_DIGEST=$(skopeo inspect container-image:${{ inputs.image }} \
--format '{{.Digest}}')
echo "${{ inputs.image }}@${IMAGE_DIGEST}" | base64 -w0 > hash.txt
echo "hashes=$(cat hash.txt)" >> "$GITHUB_OUTPUT"
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ inputs.image }}
registry-username: ${{ secrets.registry-username }}
registry-password: ${{ secrets.registry-password }}
Using Container Provenance¶
jobs:
build:
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
outputs:
image: ${{ steps.build.outputs.image }}
steps:
- uses: actions/checkout@v4
- name: Build and push with buildah
id: build
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ github.ref_name }}"
buildah build -t "$IMAGE" .
buildah push "$IMAGE"
echo "image=$IMAGE" >> "$GITHUB_OUTPUT"
provenance:
needs: [build]
uses: ./.github/workflows/slsa-container-provenance.yml
permissions:
packages: write
id-token: write
with:
image: ${{ needs.build.outputs.image }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
Pattern 3: Verification Gate (Composite Action)¶
Create .github/actions/verify-provenance/action.yml:
name: 'Verify SLSA Provenance'
description: 'Verify artifact provenance with slsa-verifier'
inputs:
artifact-path:
description: 'Path to artifact to verify'
required: true
provenance-path:
description: 'Path to provenance file (.intoto.jsonl)'
required: true
source-uri:
description: 'Expected source repository (github.com/org/repo)'
required: true
runs:
using: 'composite'
steps:
- name: Install slsa-verifier
shell: bash
run: |
curl -sL "https://github.com/slsa-framework/slsa-verifier/releases/download/v2.6.0/slsa-verifier-linux-amd64" \
-o slsa-verifier
chmod +x slsa-verifier
sudo mv slsa-verifier /usr/local/bin/
- name: Verify provenance
shell: bash
run: |
for artifact in ${{ inputs.artifact-path }}; do
slsa-verifier verify-artifact "$artifact" \
--provenance-path "${{ inputs.provenance-path }}" \
--source-uri "${{ inputs.source-uri }}"
done
Using the Verification Action¶
jobs:
verify:
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: release-artifacts
- uses: actions/download-artifact@v4
with:
name: provenance
- uses: ./.github/actions/verify-provenance
with:
artifact-path: 'dist/*'
provenance-path: 'provenance/*.intoto.jsonl'
source-uri: 'github.com/${{ github.repository }}'
Best Practices¶
1. Empty Workflow-Level Permissions¶
Why: Prevents accidental permission escalation.
2. Version Tag Pinning¶
Why: slsa-verifier validates against known version tags. SHA pins fail verification.
3. Separate Build and Provenance Jobs¶
jobs:
build:
permissions: { contents: read }
provenance:
needs: [build]
permissions: { id-token: write, contents: write }
Why: Limits blast radius.
4. Verification in Required Status Checks¶
Add verification as required status check in branch protection to block merge without valid provenance.
Related Content¶
- Advanced GitHub Actions Patterns - Multi-platform builds, monorepo patterns, organization-wide adoption
- SLSA Implementation - Technical reference for slsa-github-generator
- Verification Workflows - Verification patterns and troubleshooting
- Go Integration - Language-specific patterns
Reusable workflows turn SLSA provenance from per-repository implementation to organization-wide enforcement.