Lab: Generating and Verifying SLSA Provenance for Container Images

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-generator reusable workflow.
  • Generate provenance using GitHub’s native artifact attestations (actions/attest-build-provenance).
  • Verify provenance with slsa-verifier, cosign, and gh 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.io by 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 (for gh attestation commands).
  • 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-generator repository 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-generator reusable 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 the slsa-github-generator reusable 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 isolationslsa-github-generator achieves 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-verifier in CI gates, cosign verify-attestation in 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: