SLSA Provenance for Container Images: From Build to Admission Control
Problem
Without provenance, you cannot prove where a container image came from, what source code it was built from, or whether the build process was tampered with. An attacker who compromises your CI pipeline can inject malicious code into images that pass all vulnerability scans, because the vulnerability is not in a known package but in your own modified source.
SLSA (Supply-chain Levels for Software Artifacts) provides a framework for provenance. This article implements it end-to-end: generate provenance attestations in CI, store them alongside images, and verify at Kubernetes admission time.
Threat Model
- Adversary: Attacker who has compromised the CI pipeline (stolen credentials, modified workflow, or backdoored build environment) and is injecting malicious code into container images.
- Blast radius: Every environment that deploys the compromised image. Without provenance verification at admission: the malicious image runs in production.
Configuration
Generating Provenance in GitHub Actions
# .github/workflows/build-with-provenance.yml
name: Build with SLSA Provenance
on:
push:
branches: [main]
permissions:
contents: read
packages: write
id-token: write # Required for keyless signing
jobs:
build:
runs-on: ubuntu-latest
outputs:
image-digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Build and push image
id: build
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
# Digest is the immutable content-addressable identifier
# (not the tag, which can be overwritten)
- name: Install cosign
uses: sigstore/cosign-installer@v3
- name: Sign the image (keyless. Sigstore)
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
env:
COSIGN_EXPERIMENTAL: "true"
- name: Generate SLSA provenance attestation
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with:
image: ghcr.io/${{ github.repository }}
digest: ${{ steps.build.outputs.digest }}
Verifying Provenance Locally
# Verify the signature
cosign verify \
--certificate-identity-regexp="https://github.com/your-org/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/your-org/your-app@sha256:abc123...
# Verify provenance attestation
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp="https://github.com/your-org/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/your-org/your-app@sha256:abc123...
# View the provenance (JSON)
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp="https://github.com/your-org/.*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/your-org/your-app@sha256:abc123... | jq '.payload' | base64 -d | jq .
Admission Control with Kyverno
# kyverno-verify-provenance.yaml
# Block images without valid SLSA provenance from running in production.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-slsa-provenance
spec:
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: verify-provenance
match:
any:
- resources:
kinds: [Pod]
namespaces: [production, staging]
verifyImages:
- imageReferences:
- "ghcr.io/your-org/*"
attestors:
- count: 1
entries:
- keyless:
issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/your-org/*"
rekor:
url: "https://rekor.sigstore.dev"
attestations:
- type: https://slsa.dev/provenance/v1
conditions:
- all:
- key: "{{ builder.id }}"
operator: Equals
value: "https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v2.0.0"
Break-Glass Procedure
When signing infrastructure is unavailable (Sigstore outage, Rekor down):
# Emergency namespace with relaxed provenance requirements.
# Time-limited: label expires after 4 hours.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-slsa-provenance
spec:
rules:
- name: verify-provenance
match:
any:
- resources:
kinds: [Pod]
namespaces: [production, staging]
exclude:
any:
- resources:
namespaces: [emergency-deploy]
# Namespace with label: break-glass=active
# Activate break-glass:
kubectl create namespace emergency-deploy
kubectl label namespace emergency-deploy break-glass=active
# Deploy the image to emergency-deploy namespace
kubectl apply -f deployment.yaml -n emergency-deploy
# MANDATORY: post-hoc verification within 24 hours
# Once signing infrastructure is restored, re-verify the image
# and move the deployment to the production namespace with full provenance.
# Deactivate break-glass:
kubectl delete namespace emergency-deploy
Expected Behaviour
- Every container image pushed to the registry has a cosign signature and SLSA provenance attestation
cosign verifysucceeds for all production images- Kyverno blocks images without valid provenance in production and staging namespaces
- Break-glass namespace available for emergency deployments (time-limited, post-hoc verified)
- Provenance includes: source repository, commit SHA, builder identity, build parameters
Trade-offs
| Control | Impact | Risk | Mitigation |
|---|---|---|---|
| Keyless signing (Sigstore) | No key management; certificates from OIDC | Depends on Sigstore/Rekor availability | Break-glass procedure for Sigstore outages. Or: use keyed signing with cosign as backup. |
| Admission verification | Adds 100-500ms to pod admission (signature verification) | Kyverno webhook unavailability blocks all pod creation | failurePolicy: Ignore for availability. Or: break-glass namespace. |
| SLSA Level 3 (hardened builder) | Strongest provenance guarantees | Requires GitHub Actions hosted runners (not self-hosted) | Use SLSA Level 2 if self-hosted runners are required. |
| Break-glass namespace | Allows emergency deploys without provenance | Potential for abuse (deploying unsigned images permanently) | Time-limited namespace. Audit log monitoring for break-glass usage. Post-hoc verification mandatory. |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Sigstore/Rekor outage | cosign sign fails in CI; images built but not signed | CI workflow fails at signing step; Sigstore status page | Use break-glass procedure. Retry signing when Sigstore recovers. |
| Kyverno verification timeout | Pod admission times out (>30 seconds); pods stuck in Pending | kubectl describe pod shows webhook timeout |
Increase webhookTimeoutSeconds. Check Kyverno pod health. Check network connectivity to Rekor. |
| Provenance mismatch | Image was built by unexpected builder; Kyverno rejects | Admission error shows builder ID mismatch | Verify the CI workflow is using the correct SLSA generator version. Update Kyverno policy if builder version was intentionally updated. |
When to Consider a Managed Alternative
Provenance infrastructure requires key/certificate management and Rekor integration.
- Snyk (#48): Integrated supply chain security with image scanning + provenance verification.
- Anchore (#98): Enterprise SBOM + provenance management with policy engine.
- Scribe Security (#100): Managed SLSA attestation and provenance platform.
Premium content pack: SLSA pipeline templates. GitHub Actions workflows with provenance generation, cosign signing, Kyverno admission policies, and break-glass procedures.