Overview
SLSA (Supply-chain Levels for Software Artifacts) provenance is a verifiable record that describes how an artifact was built: the source repository, the build platform, the entry point, and the input materials. When attached to a container image, provenance lets consumers answer a critical question before deploying: “Was this image actually built from the source I expect, on a platform I trust?”
In this hands-on lab you will:
- Build and push a container image to GitHub Container Registry (GHCR).
- Generate SLSA Level 3 provenance using the official
slsa-github-generatorreusable workflow. - Generate provenance using GitHub’s native artifact attestations (
actions/attest-build-provenance). - Verify provenance with
slsa-verifier,cosign, andgh attestation verify. - Enforce provenance at deployment time with a Kubernetes admission policy.
By the end of this lab you will have a complete, reproducible pipeline that proves the integrity of every container image you ship.
Prerequisites
Before you begin, make sure you have the following in place:
- GitHub account with a test repository (public or private with GitHub Pro/Team/Enterprise).
- GHCR access — your GitHub account can push to
ghcr.ioby default; confirm by navigating to Settings → Packages. - Cosign CLI installed locally:
# macOS brew install cosign # Linux / other go install github.com/sigstore/cosign/v2/cmd/cosign@latest - slsa-verifier CLI installed:
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest - GitHub CLI (
gh) version 2.49 or later (forgh attestationcommands). - Docker installed and running.
- kubectl with access to a test Kubernetes cluster (for the enforcement exercise).
Environment Setup
Step 1 — Create the Test Repository
Create a new GitHub repository called slsa-provenance-lab. Clone it locally and add the following files.
main.go
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from SLSA provenance lab!")
})
fmt.Println("Server starting on :8080")
http.ListenAndServe(":8080", nil)
}
go.mod
module github.com/YOUR_USER/slsa-provenance-lab
go 1.22
Dockerfile
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod ./
COPY main.go ./
RUN go build -o server .
FROM alpine:3.19
RUN apk --no-cache add ca-certificates
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
Step 2 — Confirm GHCR Access
Authenticate Docker with GHCR so you can push images:
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_USER --password-stdin
Replace YOUR_USER with your GitHub username and GITHUB_TOKEN with a personal access token that has write:packages scope.
Step 3 — Baseline Build-and-Push Workflow (No Provenance)
Create .github/workflows/build.yml to verify the image builds and pushes correctly before adding provenance:
name: Build and Push (baseline)
on:
push:
tags:
- "v*"
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/slsa-provenance-lab
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.IMAGE }}:${{ github.ref_name }}
${{ env.IMAGE }}:latest
Push a test tag to verify:
git add -A
git commit -m "baseline build workflow"
git tag v0.1.0
git push origin main --tags
Confirm the image appears at ghcr.io/YOUR_USER/slsa-provenance-lab:v0.1.0 before continuing.
Exercise 1: Generate SLSA Provenance with slsa-github-generator
Why slsa-github-generator Achieves SLSA Level 3
The key property of SLSA Build Level 3 is that the provenance is generated by a build platform the developer cannot influence. The slsa-github-generator achieves this by running as a reusable workflow hosted in a separate repository. Because GitHub Actions isolates reusable workflow runs from the calling workflow, the provenance generation step is hardened against tampering — even a compromised build job cannot alter the provenance output.
The Workflow
Create .github/workflows/slsa-provenance.yml:
name: Build + SLSA Provenance (slsa-github-generator)
on:
push:
tags:
- "v*"
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/slsa-provenance-lab
jobs:
# --- Job 1: Build and push the container image ---
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image: ${{ env.IMAGE }}
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.IMAGE }}:${{ github.ref_name }}
${{ env.IMAGE }}:latest
# --- Job 2: Generate SLSA Level 3 provenance ---
provenance:
needs: build
permissions:
actions: read
id-token: write
packages: write
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.1.0
with:
image: ${{ needs.build.outputs.image }}
digest: ${{ needs.build.outputs.digest }}
secrets:
registry-username: ${{ github.actor }}
registry-password: ${{ secrets.GITHUB_TOKEN }}
Understanding the Workflow
- The build job builds the image, pushes it to GHCR, and outputs the image reference and digest.
- The provenance job calls the reusable workflow from the
slsa-framework/slsa-github-generatorrepository at a pinned tag (@v2.1.0). Because this workflow runs in an isolated environment controlled by the SLSA framework maintainers, it meets the SLSA Level 3 requirement for a hardened, non-falsifiable build platform. - The provenance is signed using Sigstore’s keyless signing (Fulcio certificate + Rekor transparency log) and attached to the image in GHCR as a cosign attestation.
Trigger the Workflow
git add .github/workflows/slsa-provenance.yml
git commit -m "add SLSA provenance workflow"
git tag v1.0.0
git push origin main --tags
In the Actions tab you will see two jobs: build and provenance. The provenance job generates an in-toto attestation, signs it via Sigstore, and pushes the attestation to GHCR alongside the image. When both jobs succeed, your image at ghcr.io/YOUR_USER/slsa-provenance-lab@sha256:<digest> now carries a signed SLSA Level 3 provenance attestation.
Exercise 2: Generate Provenance with GitHub Artifact Attestations
GitHub’s Native Approach
GitHub provides a built-in mechanism for generating build provenance through the actions/attest-build-provenance action. This approach is simpler to configure and stores attestations in GitHub’s own attestation storage, making them verifiable with the gh CLI. The trade-off is that these attestations follow GitHub’s own verification path rather than the SLSA framework’s tooling.
The Workflow
Create .github/workflows/github-attestation.yml:
name: Build + GitHub Artifact Attestation
on:
push:
tags:
- "v*"
env:
IMAGE: ghcr.io/${{ github.repository_owner }}/slsa-provenance-lab
jobs:
build-and-attest:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
attestations: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ env.IMAGE }}:${{ github.ref_name }}
${{ env.IMAGE }}:latest
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v2
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
Comparing the Two Approaches
| Aspect | slsa-github-generator | GitHub Artifact Attestations |
|---|---|---|
| SLSA Level | Level 3 (isolated reusable workflow) | Level 2–3 (GitHub-managed, single workflow) |
| Verification tool | slsa-verifier, cosign |
gh attestation verify, cosign |
| Attestation storage | OCI registry (alongside image) | GitHub attestation API + optional OCI push |
| Setup complexity | Two-job workflow with reusable workflow call | Single-job workflow with one additional step |
| Signing | Sigstore (Fulcio + Rekor) | Sigstore (Fulcio + Rekor via GitHub) |
Both approaches are valid. Use slsa-github-generator when you need strict SLSA Level 3 compliance with cross-platform verification. Use GitHub artifact attestations when you want a simpler setup and your consumers already use the GitHub ecosystem.
Exercise 3: Verify Provenance with slsa-verifier
The slsa-verifier CLI is the official tool for verifying SLSA provenance generated by slsa-github-generator. It checks the cryptographic signature, the builder identity, the source repository, and the artifact digest in a single command.
Step 1 — Get the Image Digest
Retrieve the digest of the image you pushed:
IMAGE=ghcr.io/YOUR_USER/slsa-provenance-lab
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE:v1.0.0" | cut -d@ -f2)
echo "$DIGEST"
You can also find the digest in the Actions workflow output or on the GHCR package page.
Step 2 — Verify Successfully
slsa-verifier verify-image "ghcr.io/YOUR_USER/slsa-provenance-lab@$DIGEST" \
--source-uri github.com/YOUR_USER/slsa-provenance-lab \
--source-tag v1.0.0
Expected output:
Verified build using builder "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.1.0" at commit abc123def456
VERIFIED: SLSA verification passed
Step 3 — Verification Failure: Wrong Source URI
Try verifying against an incorrect source repository:
slsa-verifier verify-image "ghcr.io/YOUR_USER/slsa-provenance-lab@$DIGEST" \
--source-uri github.com/YOUR_USER/wrong-repo \
--source-tag v1.0.0
Expected output:
FAILED: SLSA verification failed: source used to generate the binary does not match provenance
Step 4 — Verification Failure: Wrong Tag
slsa-verifier verify-image "ghcr.io/YOUR_USER/slsa-provenance-lab@$DIGEST" \
--source-uri github.com/YOUR_USER/slsa-provenance-lab \
--source-tag v9.9.9
Expected output:
FAILED: SLSA verification failed: tag "v9.9.9" does not match provenance
What slsa-verifier Checks
- Builder identity — confirms the provenance was created by the official
slsa-github-generatorreusable workflow at the expected ref. - Source repository — the provenance must reference the source URI you specify.
- Source tag/branch — optionally checks the Git ref that triggered the build.
- Artifact digest — the SHA-256 digest recorded in the provenance must match the image you are verifying.
- Signature and transparency log — the Sigstore signature is verified against the Rekor transparency log.
Exercise 4: Verify with cosign verify-attestation
Cosign provides a lower-level but more flexible way to verify attestations attached to container images. This is useful when you need to inspect the raw provenance payload or when integrating into custom verification pipelines.
Verify the Attestation
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp "https://github.com/slsa-framework/slsa-github-generator/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/YOUR_USER/slsa-provenance-lab@$DIGEST
On success, cosign prints the attestation payload as JSON. A truncated example:
Verification for ghcr.io/YOUR_USER/slsa-provenance-lab@sha256:abc123... --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The code-signing certificate was verified using trusted certificate authority
{
"payloadType": "application/vnd.in-toto+json",
"payload": "eyJfdHlwZSI6Imh0dHBz...",
"signatures": [{ "sig": "MEUCIQD..." }]
}
Inspect the Provenance Payload
Decode the base64 payload to inspect the provenance fields:
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp "https://github.com/slsa-framework/slsa-github-generator/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/YOUR_USER/slsa-provenance-lab@$DIGEST \
| jq -r '.payload' | base64 -d | jq .
Key fields in the output:
buildDefinition.buildType— identifies the build system (e.g.,https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1).buildDefinition.externalParameters.workflow— the workflow file and ref that ran the build.buildDefinition.resolvedDependencies— the Git commit, repository, and other inputs.runDetails.builder.id— the URI of the trusted builder that generated the provenance.
Exercise 5: Verify with gh attestation verify
For images attested using GitHub’s native artifact attestations (Exercise 2), the gh CLI provides the simplest verification path.
Verify the Attestation
gh attestation verify oci://ghcr.io/YOUR_USER/slsa-provenance-lab@$DIGEST \
--owner YOUR_USER
Expected output:
Loaded digest sha256:abc123def456... for oci://ghcr.io/YOUR_USER/slsa-provenance-lab@sha256:abc123...
Loaded 1 attestation from GitHub API
✓ Verification succeeded!
PredicateType: https://slsa.dev/provenance/v1
SubjectName: ghcr.io/YOUR_USER/slsa-provenance-lab
SubjectDigest: sha256:abc123def456...
SignerRepo: YOUR_USER/slsa-provenance-lab
SignerWorkflow: .github/workflows/github-attestation.yml
RunnerEnv: github-hosted
Download and Inspect the Attestation
To download the raw attestation bundle for offline inspection:
gh attestation download oci://ghcr.io/YOUR_USER/slsa-provenance-lab@$DIGEST \
--owner YOUR_USER \
--output attestation-bundle.json
# Inspect the provenance predicate
cat attestation-bundle.json | jq '.dsseEnvelope.payload' -r | base64 -d | jq .
This gives you the full SLSA provenance predicate, which you can store alongside your deployment records for audit purposes.
Exercise 6: Enforce Provenance at Deployment
Generating and verifying provenance manually is valuable, but the real security benefit comes from automated enforcement at deployment time. In this exercise, you will configure a Kubernetes admission policy that rejects any container image that lacks valid SLSA provenance.
Option A: Sigstore Policy Controller
The Sigstore policy-controller is a Kubernetes admission webhook that verifies image signatures and attestations before pods are admitted.
Install the Policy Controller
helm repo add sigstore https://sigstore.github.io/helm-charts
helm repo update
helm install policy-controller sigstore/policy-controller \
--namespace cosign-system \
--create-namespace
Create a ClusterImagePolicy
Create slsa-policy.yml:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-slsa-provenance
spec:
images:
- glob: "ghcr.io/YOUR_USER/**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "https://github.com/slsa-framework/slsa-github-generator/.*"
attestations:
- name: must-have-slsa-provenance
predicateType: https://slsa.dev/provenance/v1
policy:
type: cue
data: |
predicateType: "https://slsa.dev/provenance/v1"
Apply it:
kubectl apply -f slsa-policy.yml
Label the Namespace for Enforcement
kubectl label namespace default policy.sigstore.dev/include=true
Option B: Kyverno Policy
If you use Kyverno as your policy engine, create kyverno-slsa-policy.yml:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-slsa-provenance
spec:
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: check-slsa-provenance
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "ghcr.io/YOUR_USER/*"
attestations:
- type: https://slsa.dev/provenance/v1
attestors:
- entries:
- keyless:
issuer: https://token.actions.githubusercontent.com
subjectRegExp: "https://github.com/slsa-framework/slsa-github-generator/.*"
rekor:
url: https://rekor.sigstore.dev
Apply it:
kubectl apply -f kyverno-slsa-policy.yml
Test: Image With Provenance (Admitted)
kubectl run test-allowed \
--image=ghcr.io/YOUR_USER/slsa-provenance-lab@$DIGEST \
--restart=Never
Expected result: the pod is created successfully.
Test: Image Without Provenance (Rejected)
Push a quick image without provenance:
docker build -t ghcr.io/YOUR_USER/slsa-provenance-lab:no-provenance .
docker push ghcr.io/YOUR_USER/slsa-provenance-lab:no-provenance
NO_PROV_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' \
ghcr.io/YOUR_USER/slsa-provenance-lab:no-provenance | cut -d@ -f2)
kubectl run test-rejected \
--image=ghcr.io/YOUR_USER/slsa-provenance-lab@$NO_PROV_DIGEST \
--restart=Never
Expected result:
Error from server: admission webhook denied the request:
image ghcr.io/YOUR_USER/slsa-provenance-lab@sha256:...
failed to verify: no matching attestations found
This confirms that the admission policy is correctly blocking images that lack SLSA provenance.
Inspecting the Provenance Document
Understanding the provenance document is essential for auditing and building automation on top of it. Below is a representative provenance document generated by slsa-github-generator, followed by a field-by-field explanation.
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "ghcr.io/YOUR_USER/slsa-provenance-lab",
"digest": {
"sha256": "abc123def456789..."
}
}
],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"ref": "refs/tags/v1.0.0",
"repository": "https://github.com/YOUR_USER/slsa-provenance-lab",
"path": ".github/workflows/slsa-provenance.yml"
}
},
"resolvedDependencies": [
{
"uri": "git+https://github.com/YOUR_USER/slsa-provenance-lab@refs/tags/v1.0.0",
"digest": {
"gitCommit": "a1b2c3d4e5f6..."
}
}
]
},
"runDetails": {
"builder": {
"id": "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.1.0"
},
"metadata": {
"invocationId": "https://github.com/YOUR_USER/slsa-provenance-lab/actions/runs/1234567890/attempts/1",
"startedOn": "2026-03-23T10:15:30Z",
"finishedOn": "2026-03-23T10:17:45Z"
}
}
}
}
Field-by-Field Breakdown
_type— identifies this as an in-toto Statement v1, the envelope format used by SLSA.subject— the artifact this provenance describes. Contains the image name and its SHA-256 digest. This is what you match against the image you are verifying.predicateType— declares that this attestation is SLSA provenance v1. Verification tools use this to determine how to interpret the predicate.buildDefinition.buildType— specifies the build system. For GitHub Actions, this tells verifiers to expect GitHub-specific fields.buildDefinition.externalParameters.workflow— the workflow file, repository, and Git ref that triggered the build. You should verify that this matches the expected source.buildDefinition.resolvedDependencies— lists the resolved inputs including the exact Git commit. This is the “materials” list — it provides a complete record of what went into the build.runDetails.builder.id— the URI of the builder that generated the provenance. For SLSA Level 3, this must be a trusted, isolated builder like theslsa-github-generatorreusable workflow at a pinned tag.runDetails.metadata— timestamps and a link to the specific GitHub Actions run, enabling full traceability from artifact back to build.
When building automated verification, always check: (1) the subject digest matches, (2) the builder ID is on your allow-list, (3) the source repository and ref match your expectations, and (4) the signature is valid.
Cleanup
Remove the resources created during this lab:
# Delete Kubernetes test pods
kubectl delete pod test-allowed test-rejected --ignore-not-found
# Remove the admission policy (Sigstore)
kubectl delete clusterimagepolicy require-slsa-provenance --ignore-not-found
# Or remove the Kyverno policy
kubectl delete clusterpolicy require-slsa-provenance --ignore-not-found
# Remove the namespace label
kubectl label namespace default policy.sigstore.dev/include-
# Delete GHCR images (via GitHub UI or CLI)
gh api -X DELETE /user/packages/container/slsa-provenance-lab/versions/PACKAGE_VERSION_ID
# Delete the test repository if desired
# gh repo delete YOUR_USER/slsa-provenance-lab --yes
Key Takeaways
- SLSA provenance is a signed, tamper-evident record of how a container image was built. It captures the source, the builder, and the build parameters.
- SLSA Level 3 requires build isolation —
slsa-github-generatorachieves this by running provenance generation in a separate reusable workflow that the developer cannot modify at runtime. - GitHub artifact attestations provide a simpler alternative that integrates tightly with the GitHub ecosystem, with trade-offs in cross-platform portability.
- Verification must be automated — use
slsa-verifierin CI gates,cosign verify-attestationin scripts, or Kubernetes admission controllers to enforce provenance before deployment. - Provenance verification checks multiple claims: builder identity, source repository, source ref, artifact digest, and cryptographic signature validity.
- Inspect provenance documents to understand exactly what was built, from which commit, by which builder. This is your audit trail for supply chain incidents.
Next Steps
Continue strengthening your software supply chain security:
- Artifact Provenance and Attestations: From SLSA to in-toto — dive deeper into the SLSA framework, in-toto attestation formats, and how to build a comprehensive provenance strategy across your entire build pipeline.
- Signing and Verifying Container Images with Sigstore and Cosign — learn how to sign container images with keyless Sigstore signing, verify signatures in CI/CD, and enforce signature policies in Kubernetes.