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.

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:

  1. The 3rd Party Image Gap: Your policy blocks ghcr.io/my-org/*, but what about redis:alpine or nginx: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.
  2. 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.
  3. 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:

References & Further Reading