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:

LevelRequirement
0No guarantees.
1Provenance exists and describes how the artifact was built.
2Provenance is signed and generated by a hosted build service.
3The 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

  1. Use your CI platform’s official provenance action; it gets you to SLSA 2+ with almost no effort and no key to manage.
  2. 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.
  3. For SLSA 3, run releases through an isolated, hardened builder the build job cannot tamper with.
  4. Enforce provenance at admission with policy that inspects the predicate, so only images built from your repositories by your workflows can run.
  5. 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.