SLSA and Supply Chain Security for Platform Engineers: Implementing Provenance, Attestations, and Sigstore in GitOps Pipelines
Sources
For years, the security boundary in platform engineering was the perimeter: firewalls, VPCs, and IAM roles. But in a world of automated GitOps pipelines and OCI registries, the perimeter has shifted. The real risk is no longer just who can deploy to your cluster, but what exactly is being deployed and how it was built.
The "Supply Chain" is the entire lifecycle from a commit in GitHub to a running pod in Kubernetes. If an attacker compromises your CI runner, they can inject a backdoor into your image after the code review but before the push. This is where SLSA (Supply-chain Levels for Software Artifacts) and Sigstore come in.
The SLSA Framework: Beyond the Marketing
SLSA (pronounced "salsa") is not a tool; it is a security framework. It defines standards for provenance—the verifiable record of how an artifact was produced. While the industry talks about "Levels," the pragmatic goal for a Senior Platform Engineer is SLSA Level 3.
- Level 1: Basic documentation. You have a build script.
- Level 2: Signed provenance. You know who built it, but the build process might be mutable.
- Level 3: Hardened provenance. The build happens on a trusted, isolated platform (like GitHub Actions or Tekton) where the provenance is generated by the platform, not the user script. This prevents "build-time" injection.
- Level 4: Hermetic builds. Zero network access during build; 100% reproducible. (Production reality: Extremely rare and often overkill for most SaaS).
Technical Architecture: Keyless Signing Flow
Traditional signing relied on long-lived GPG keys stored in CI secrets. This is a disaster waiting to happen—keys leak, rotate poorly, and are hard to audit. The modern approach is Keyless Signing using the Sigstore stack (Fulcio, Rekor, Cosign).
+------------------+ +-------------------+ +-----------------------+
| GitHub Action | ----> | Fulcio (CA) | ----> | Short-lived Cert |
| (OIDC Identity) | | (Issues Cert via | | (Bound to Workflow) |
+------------------+ +-------------------+ +-----------------------+
|
v
+------------------+ +-------------------+ +-----------------------+
| OCI Registry | <---- | Cosign (Sign) | <---- | Ephemeral Key |
| (Image + Sig + | | (Signs Digest) | | (Discarded after) |
| Attestation) | +-------------------+ +-----------------------+
|
v
+-------------------+
| Rekor (Log) |
| (Public Ledger of |
| Signing Events) |
+-------------------+
In this flow, there are no permanent private keys. The build runner proves its identity via an OIDC token, gets a 10-minute certificate from Fulcio, signs the image, and the event is etched into the Rekor transparency log.
Implementation: From Build to Admission Control
1. Generating Provenance in GitHub Actions
Modern GitHub Actions have built-in support for SLSA provenance via the actions/attest action. This replaces the need for complex manual Cosign scripts for basic provenance.
name: Secure Release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC / Keyless signing
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build and Push Image
id: build-image
run: |
IMAGE="ghcr.io/${{ github.repository }}-app:${{ github.ref_name }}"
docker build -t $IMAGE .
docker push $IMAGE
echo "digest=$(docker inspect --format='{{.RepoDigests}}' $IMAGE | cut -d ':' -f 2)" >> $GITHUB_OUTPUT
- name: Attest Build Provenance
uses: actions/attest@v4
with:
subject-name: ghcr.io/${{ github.repository }}-app
subject-digest: ${{ steps.build-image.outputs.digest }}
push-to-registry: true
2. Attaching SBOMs with Cosign
Provenance tells you how it was built; an SBOM (Software Bill of Materials) tells you what is inside. We use syft for generation and cosign for attestation.
# Generate SBOM in CycloneDX format
syft ghcr.io/my-org/my-app@sha256:abc... -o cyclonedx-json > sbom.json
# Attest the SBOM keylessly
# This requires the environment to be authenticated via OIDC (e.g. in GHA)
cosign attest --yes --type cyclonedx --predicate sbom.json ghcr.io/my-org/my-app@sha256:abc...
3. Enforcing Verification in Kubernetes
Signing is useless if the cluster doesn't check the signature. We use Kyverno's ImageValidatingPolicy. This ensures that the cluster rejects any pod whose image lacks a valid SLSA attestation from our specific GitHub workflow.
apiVersion: policies.kyverno.io/v1
kind: ImageValidatingPolicy
metadata:
name: require-slsa-provenance
spec:
validationActions: [Deny] # Start with [Audit] in production!
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
matchImageReferences:
- glob: "ghcr.io/my-org/*"
attestors:
- name: github-oidc
cosign:
keyless:
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: '^https://github\\.com/my-org/[^/]+/\\.github/workflows/release\\.yml@refs/tags/v.*$'
ctlog:
url: https://rekor.sigstore.dev
attestations:
- name: slsa
intoto:
type: https://slsa.dev/provenance/v1
validations:
- expression: >-
images.containers.map(image, verifyAttestationSignatures(image, attestations.slsa, [attestors.github-oidc])).all(e, e > 0)
message: "Image rejected: Missing SLSA provenance from the official release workflow."
Production Realities: Where it Breaks
Implementing SLSA L3 sounds clean in a blog post, but at scale, you will hit these three walls:
- The 3rd Party Image Gap: Your policy blocks
ghcr.io/my-org/*, but what aboutredis:alpineornginx:latest? You cannot force Redis to provide SLSA provenance.- Solution: Use a "Verified Base Image" pattern. Pull public images into your own registry, scan them, sign them with your key, and then allow them.
- The "Emergency Fix" Bypass: When a P0 incident hits and the CI is down, engineers will try to push a hotfix image manually from their laptop. The cluster will reject it.
- Solution: Create a highly audited "Break-Glass" identity in Kyverno, but ensure it triggers a high-severity alert in your SOC.
- Performance Latency: Every pod creation now triggers a call to Rekor and Fulcio. If the Sigstore public API is slow or down, your cluster cannot scale.
- Solution: Deploy a local Rekor and Fulcio mirror (private Sigstore) for mission-critical infrastructure.
Who Should Skip This?
Do NOT implement a full SLSA L3/Sigstore pipeline if:
- You are a 3-person startup: The operational toil of managing a private Rekor mirror or fighting Kyverno regexes outweighs the risk. Stick to simple image signing.
- Your apps are non-critical internal tools: If the "worst case" is a internal dashboard being down for an hour, the complexity of attestations is security theater.
- You don't have an OIDC-compatible CI: If you are running legacy Jenkins on a VM without workload identity, keyless signing is not an option. Use Cosign with KMS-backed keys instead.