← Recipes

阅读中文版本

Attach Build Provenance to Release Artifacts

Generate a signed SLSA provenance attestation in GitHub Actions and verify it before deploying the container image.

~35 minsupply-chaincisecurity

Task

Prove that a deployed image was built by your CI workflow from a specific commit, not on a laptop or by an unknown pipeline.

When to use this

A release image is promoted from staging to production by tag. There is no way to confirm the image came from the protected main branch, so a compromised registry credential could let an attacker push a look-alike image with the same tag.

Prerequisites

  • GitHub Actions with a container registry such as GHCR
  • Workflow permissions id-token: write, attestations: write, packages: write
  • GitHub CLI 2.49 or newer for verification

Steps

1. Build and push the image by digest, then attest that digest in the same workflow.

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

steps:
  - uses: actions/checkout@v4
  - uses: docker/build-push-action@v6
    id: build
    with:
      push: true
      tags: ghcr.io/acme/payments-api:${{ github.sha }}
  - uses: actions/attest-build-provenance@v2
    with:
      subject-name: ghcr.io/acme/payments-api
      subject-digest: ${{ steps.build.outputs.digest }}
      push-to-registry: true

Expected output: The registry stores an attestation linked to the image digest and the workflow identity.

2. Verify the attestation locally before promoting the image.

gh attestation verify oci://ghcr.io/acme/payments-api:latest --owner acme

Expected output: Verification succeeds and reports the workflow, repository, and commit that produced the image.

3. Make the deploy job verify provenance and fail closed on any error.

- name: Verify provenance
  run: gh attestation verify oci://ghcr.io/acme/payments-api:${{ github.sha }} --owner acme
- name: Deploy
  run: kubectl set image deployment/payments-api api=ghcr.io/acme/payments-api:${{ github.sha }}

Expected output: Deployment stops if the attestation is missing, invalid, or bound to a different repository.

4. Test the negative case by verifying an image that was built outside the workflow.

gh attestation verify oci://ghcr.io/acme/debug-tools:dev --owner acme

Expected output: The command exits non-zero with no attestations found, proving the gate rejects unattested images.

Troubleshooting

Problem

Verification reports no attestations found

Solution

Confirm push-to-registry is true, the subject digest matches the image digest, and the attestation was created in the same repository that owns the image.

Problem

The workflow cannot request an OIDC token

Solution

Add id-token: write at the job level; organization policy may also restrict which workflows can request tokens.

Limitations

  • Provenance proves how and where an artifact was built, not that the source code is free of vulnerabilities.
  • Verification needs network access to the GitHub attestation API and trusts GitHub as the identity provider.

Provenance answers a narrow question

Build provenance answers: which workflow, repository, and commit produced this exact digest? It does not scan the image or judge code quality. That narrow guarantee is enough to block a class of supply-chain attacks where someone pushes an image that looks like a release but was built elsewhere.

Attest the digest

Tags are mutable; digests are not. Build the image, read the digest output, and attest that digest. The attestation is stored in the registry alongside the image, so verification travels with the artifact. Grant only the permissions needed: id-token: write for the OIDC identity, attestations: write for the attestation, and packages: write for the push.

- uses: actions/attest-build-provenance@v2
  with:
    subject-name: ghcr.io/acme/payments-api
    subject-digest: ${{ steps.build.outputs.digest }}
    push-to-registry: true

Verify at the gate

Verification belongs in the deploy job, not in a wiki page. Run gh attestation verify against the image reference and owner before any kubectl set image. If the command fails, stop. Treat a missing attestation as a failed verification rather than skipping the check for convenience.

Keep the chain intact

Protect the main branch, require reviews, and pin third-party actions by commit SHA so the workflow itself cannot be altered silently. Record the verified digest in the deployment record. When promoting between environments, promote the digest, not a rebuilt image, so the artifact that was tested is the artifact that runs.