Generating provenance for a tagged release Jump to heading
Provenance is produced by the build or not at all โ it is a statement about a relationship that only exists during the build, between the source checked out and the artefact emitted. Adding it afterwards means asserting something you did not observe. This recipe wires it into a release pipeline, including the constraint that most setups omit: making sure only a signed tag on a protected branch can produce a release build in the first place. Part of build provenance and attestations.
When to use this approach Jump to heading
- Releases produce artefacts that are deployed or distributed.
- You need to answer which commit produced a given artefact.
- A verification gate exists or is planned at deploy time.
- Compliance or a customer requires build provenance.
- If nothing verifies the attestation, generating it is bookkeeping โ set up the gate first or alongside.
Step 1 โ Constrain what may produce a release build Jump to heading
An attestation is truthful about whatever the build consumed, so the constraint belongs before the build.
# .github/workflows/release.yml
name: release
on:
push:
tags: ['v[0-9]+.[0-9]+.[0-9]+'] # only version tags, never branches
jobs:
guard:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: The tag must be signed and on the default branch
run: |
git verify-tag "${GITHUB_REF_NAME}"
git merge-base --is-ancestor "${GITHUB_REF_NAME}^{commit}" origin/main # Verification: a tag on a side branch fails the guard
git tag -s v9.9.9 -m probe side-branch && git push origin v9.9.9
gh run list --workflow release.yml --limit 1 --json conclusion
git push origin --delete v9.9.9 && git tag -d v9.9.9 Step 2 โ Capture the digest the build produced Jump to heading
The attestationโs subject must be the exact artefact, by digest rather than by tag.
build:
needs: guard
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write # keyless signing
attestations: write
outputs:
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/acme/app:${{ github.ref_name }} # Verification: the digest is a content hash, not a tag
echo "$DIGEST" | grep -E '^sha256:[0-9a-f]{64}$' && echo "digest captured" Step 3 โ Attest, keylessly Jump to heading
- uses: actions/attest-build-provenance@v1
with:
subject-name: ghcr.io/acme/app
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true # Or with cosign, for registries or builders without native support
cosign attest --yes \
--predicate /tmp/provenance.json --type slsaprovenance \
"ghcr.io/acme/app@${DIGEST}" # Verification: the attestation is retrievable and names the right commit
gh attestation verify "oci://ghcr.io/acme/app@${DIGEST}" --owner acme --format json \
| jq -r '.[0].verificationResult.statement.predicate.buildDefinition
.resolvedDependencies[0].digest.gitCommit'
git rev-parse "${GITHUB_REF_NAME}^{commit}" Those two values must match. If they differ, the build checked out something other than the tag โ usually because the workflow ran on a merge commit created by the forge rather than on the tag itself.
Step 4 โ Push the attestation where verification will look Jump to heading
An attestation stored somewhere the deployment cannot read is not a gate.
# Stored alongside the image in the registry โ the usual choice
cosign download attestation "ghcr.io/acme/app@${DIGEST}" | jq -r '.payload' | base64 -d | jq '.predicateType' # Or in the forge's attestation store, queryable by digest
gh attestation verify "oci://ghcr.io/acme/app@${DIGEST}" --owner acme # Verification: a deployment-time lookup succeeds using only the digest
gh attestation verify "oci://ghcr.io/acme/app@${DIGEST}" --owner acme --repo acme/app >/dev/null \
&& echo "retrievable by digest alone" SAFETY WARNING โ
permissions: id-token: writegrants the job the ability to obtain an identity token that proves it is your workflow. A job holding that permission and also checking out untrusted code can be induced to sign an attestation for an artefact an attacker influenced. Grant it only on the job that builds and signs, keep untrusted code out of that job entirely, and never combine it with a trigger that runs on unreviewed contributions.
Step 5 โ Record the release facts together Jump to heading
# One place that ties tag, commit, digest and run together
cat >> releases.tsv <<EOF
$(date -u +%FT%TZ) ${GITHUB_REF_NAME} $(git rev-parse HEAD) ${DIGEST} ${GITHUB_RUN_ID}
EOF # And as an annotated tag message, which travels with the repository
git tag -s -f "${GITHUB_REF_NAME}" -m "Release ${GITHUB_REF_NAME}
image: ghcr.io/acme/app@${DIGEST}
commit: $(git rev-parse HEAD)
run: ${GITHUB_RUN_ID}" # Verification: the tag message names the digest that was attested
git tag -l "${GITHUB_REF_NAME}" --format='%(contents)' | grep '^image:' Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
What if our registry does not support attestations? Jump to heading
Store them in the forgeโs attestation store, which is queryable by digest and independent of the registry. The verification command changes and the gate works identically โ what matters is that the deployment can look up an attestation given only the digest it is about to run.
Should every build be attested, or only releases? Jump to heading
Attest anything that can be deployed. Restricting it to tagged releases is common and leaves a gap wherever a branch build can reach an environment โ and those paths exist in most organisations, usually for staging. Attesting everything costs nothing extra per build.
Does this work for language packages rather than images? Jump to heading
Yes: the subject is a digest, and packages have digests. The same attestation types and verification commands apply, and several package registries now accept and serve attestations directly. The awkward part is usually the deployment gate rather than the generation.
Related Jump to heading
- Build Provenance & Attestations โ the parent topic and where the chain breaks.
- Verifying Attestations Before Deploy โ the gate this generation exists to feed.
- Signing and Verifying Release Tags โ anchoring the source end of the chain.