Supply Chain Security
Provenance
Attesting how an artifact was built to defend against tampered build pipelines.
Signing proves who published an image. It says nothing about how that image came to be. If the build system itself is compromised, it will happily sign a malicious artifact with the real key. This is the SolarWinds pattern, and no signature catches it.
Build provenance closes that gap. It is signed metadata describing how an artifact was produced: which source commit, which build system, which parameters, and when. Verifying provenance lets you assert “this image was built by my GitHub Actions workflow from this repository”, not merely that someone signed it.
SLSA Levels
SLSA grades build integrity, and provenance is the mechanism behind the middle levels:
| Level | Requirement |
|---|---|
| 0 | No guarantees. |
| 1 | Provenance exists and describes how the artifact was built. |
| 2 | Provenance is signed and generated by a hosted build service. |
| 3 | The build runs in a hardened, isolated environment that the build itself cannot tamper with. |
Most teams reach SLSA 2 or 3 simply by using a hosted CI system’s official provenance action; the platform generates trustworthy provenance for free.
The In-Toto Attestation Format
Provenance is expressed as an in-toto attestation: a signed statement binding a subject (the artifact’s digest) to a predicate (the claim). For provenance the predicate is a SLSA provenance document:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{ "name": "ghcr.io/adi/app", "digest": { "sha256": "9f2c..." } }
],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://actions.github.io/buildtypes/workflow/v1",
"externalParameters": {
"workflow": ".github/workflows/release.yml",
"repository": "https://github.com/adi/app",
"ref": "refs/tags/v1.4.0"
}
},
"runDetails": {
"builder": { "id": "https://github.com/actions/runner" }
}
}
}
Everything needed to answer “where did this come from” is in the predicate, and the whole statement is signed.
Generate Provenance In GitHub Actions
The official attest-build-provenance action generates and signs SLSA provenance for an image, keylessly, in one step:
permissions:
id-token: write # keyless signing via OIDC
packages: write
attestations: write # store the attestation
steps:
- uses: actions/checkout@v4
- name: Build and push
id: push
uses: docker/build-push-action@v6
with:
push: true
tags: ghcr.io/adi/app:${{ github.ref_name }}
- name: Attest provenance
uses: actions/attest-build-provenance@v1
with:
subject-name: ghcr.io/adi/app
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
The digest is signed, not the tag, the same discipline as image signing. For the strongest SLSA 3 posture, the slsa-github-generator runs the build in an isolated reusable workflow the calling job cannot influence.
Verify Provenance
With the GitHub CLI, verification checks the signature and the identity that produced it:
gh attestation verify oci://ghcr.io/adi/app:v1.4.0 \
--repo adi/app
Expected output:
✓ Verification succeeded!
sha256:9f2c... was attested by:
repo: adi/app
predicate type: https://slsa.dev/provenance/v1
workflow: .github/workflows/release.yml@refs/tags/v1.4.0
Cosign verifies the same attestation and lets you assert the exact identity:
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp "https://github.com/adi/app/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/adi/app:v1.4.0
The certificate identity is the point: it fails if anything other than that specific workflow in that repository produced the image.
Enforce At Admission
Provenance becomes a control when the cluster demands it. Kyverno’s verifyImages rule accepts an attestations block that checks not just that provenance exists but that its contents match, for example that the source repository is one you own:
verifyImages:
- imageReferences: ["ghcr.io/adi/*"]
attestations:
- type: https://slsa.dev/provenance/v1
conditions:
- all:
- key: "{{ predicate.buildDefinition.externalParameters.repository }}"
operator: Equals
value: "https://github.com/adi/app"
A pod whose image lacks matching provenance, or was built from an unexpected repository, is rejected at admission.
Clean Up
Nothing runs locally in this exercise; the workflow snippets live in CI. Remove any Kyverno policy you applied while testing:
kubectl delete clusterpolicy verify-provenance --ignore-not-found
Practical Guidance
- Use your CI platform’s official provenance action; it gets you to SLSA 2+ with almost no effort and no key to manage.
- Verify the identity in the provenance (repo and workflow), not merely that provenance exists, because an attacker’s fork can produce valid-but-wrong provenance.
- For SLSA 3, run releases through an isolated, hardened builder the build job cannot tamper with.
- Enforce provenance at admission with policy that inspects the predicate, so only images built from your repositories by your workflows can run.
- Layer the controls: scanning finds known CVEs, SBOMs inventory contents, signing proves origin, provenance proves the build path. Each defends against attacks the others miss.