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:
- 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. - A build provenance attestation. A signed statement describing how the image was built — which workflow, which commit, which builder. This is the SLSA piece.
- 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.