SLSA, Provenance, and Actually Signing Things: A Practical Supply Chain Setup

Paul Wechuli
May 6, 2026
supply-chain
slsa
sigstore
cosign
github-actions
kubernetes
security

Build provenance, image signing with Sigstore, and policy enforcement in Kubernetes — the parts that are now table stakes, and the parts that are still over-engineered for most teams.

SLSA, Provenance, and Actually Signing Things: A Practical Supply Chain Setup

Software supply chain security stopped being a buzzword somewhere around 2024 and is now a checkbox on most enterprise procurement questionnaires. SLSA, in-toto, Sigstore, SBOMs, attestations — there is a lot of vocabulary, and most of it sounds harder than it is. Here's the setup I now consider the minimum viable supply chain story for a team shipping containers to production.

The three things you actually need

Strip away the jargon and there are three concrete artifacts you want for every production container image:

  1. A signature. Cryptographic proof that "this image was built by us." Produced with Sigstore's cosign, using keyless signing tied to your GitHub Actions OIDC identity.
  2. A build provenance attestation. A signed statement describing how the image was built — which workflow, which commit, which builder. This is the SLSA piece.
  3. An SBOM attestation. A signed statement listing the components inside the image. SPDX or CycloneDX format.

All three are attached to the image itself in the registry, retrieved via cosign verify and cosign verify-attestation. You don't need a separate metadata service. The registry is the source of truth.

The build job

Using the official SLSA generator workflow plus cosign attest:

name: Release
on:
  push:
    tags: ["v*"]

permissions:
  id-token: write
  contents: read
  packages: write
  attestations: write

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image: ${{ steps.push.outputs.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:
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
          provenance: mode=max
          sbom: true

  sign:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      packages: write
    steps:
      - uses: sigstore/cosign-installer@v3
      - run: |
          cosign sign --yes \
            ghcr.io/${{ github.repository }}@${{ needs.build.outputs.digest }}

  attest:
    needs: build
    uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
    with:
      image: ghcr.io/${{ github.repository }}
      digest: ${{ needs.build.outputs.digest }}
    secrets:
      registry-username: ${{ github.actor }}
      registry-password: ${{ secrets.GITHUB_TOKEN }}

There is no private key. There is no key management. cosign sign opens an OIDC flow with the Sigstore Fulcio CA, gets a short-lived certificate bound to your workflow identity, signs the image, logs the signature to the Rekor transparency log, and exits. The certificate expires in ten minutes. The record is permanent.

Verifying on the way into the cluster

A signature nobody checks is decoration. The enforcement happens at admission time in Kubernetes, with a policy controller. I use Kyverno because the policy syntax is readable; Sigstore Policy Controller works equivalently.

apiVersion: kyverno.io/v2
kind: ClusterPolicy
metadata:
  name: require-signed-images
spec:
  validationFailureAction: Enforce
  rules:
    - name: verify-cosign-signatures
      match:
        any:
          - resources:
              kinds: [Pod]
              namespaces: [prod, prod-*]
      verifyImages:
        - imageReferences:
            - "ghcr.io/my-org/*"
          attestors:
            - entries:
                - keyless:
                    subject: "https://github.com/my-org/*/.github/workflows/release.yml@refs/tags/v*"
                    issuer: "https://token.actions.githubusercontent.com"
                    rekor:
                      url: https://rekor.sigstore.dev

This says: any pod in a prod namespace using a ghcr.io/my-org/* image must be signed by a Sigstore certificate whose subject matches our release workflow. An unsigned image, or an image signed by someone else's workflow, is rejected at admission.

The subject pattern is the important part. It's the same security principle as the OIDC trust policy from my last post — pin to the specific workflow that's allowed to produce production artifacts. A developer can't bypass the release pipeline by manually pushing an image, because their personal OIDC identity won't match the subject pattern.

What I think is over-engineered

A few things I keep getting asked about and keep saying "no, not yet":

  • SLSA Level 4 with hermetic, reproducible builds. Wonderful goal. The engineering cost is enormous, the actual security improvement over SLSA Level 3 is modest, and most teams don't have Level 3 nailed down yet. Get to L3 (provenance generated by a trusted builder, signed). Stop there.
  • In-toto layouts with full step-by-step attestation chains. Solves a real problem for very high-assurance environments. For most teams, the build provenance attestation generated by the SLSA generator is sufficient and self-contained.
  • Running your own Fulcio and Rekor. People want this for "no external dependency in the supply chain." The public Sigstore infrastructure is now run by a Linux Foundation project with serious uptime guarantees. The operational cost of running your own is high and the security benefit is small.

What is not over-engineered, and worth doing:

  • Enforce signature verification on prod admission, not just dev.
  • Pin the subject to your specific release workflow, not just "any workflow in your org."
  • Verify on every pull, not just on first deploy. Mutating tags is a real attack vector.
  • Audit the Rekor log periodically for unexpected signing events under your org's identity. If you ever see a signature from a workflow that shouldn't exist, you've caught a problem.

What you actually get out of this

A few months after rolling this out, you start to get value beyond "we pass the audit":

  • Incident response gets easier. "Which workflow built this image and from what commit?" is a one-command lookup against the attestations.
  • Internal trust boundaries become enforceable. You can say "the data platform team's images may run in the data namespace, but not in the payments namespace" with a policy, and it sticks.
  • You can finally answer the SBOM question. When a CVE drops, querying "which of our deployed images contain log4j" stops being a multi-day project.

This used to be a months-long effort. With the current tooling, a competent platform engineer can set this up across an org in about a week, mostly spent writing policies and dealing with the inevitable "oh no, our nightly batch job uses an unsigned image" discoveries. The result is the kind of supply chain story that, five years ago, only the largest tech companies could plausibly claim.

It's no longer hard. It's mostly just unfamiliar.