Attest Release Archives and Verify Before Deploy
Sign a build artifact with GitHub attestations, then verify its digest and source ref in the deploy job so a swapped archive never ships.
Task
Prove that the tarball promoted to production was produced by the trusted workflow from the expected branch.
When to use this
A deploy job downloads a release archive by URL. If the storage bucket is writable by another team, a swapped archive with the same name deploys untrusted code.
Prerequisites
- GitHub Actions with id-token: write and attestations: write
- GitHub CLI 2.49 or newer
- A release artifact built inside the workflow
Steps
1. Build the archive and attest it by subject path in the same job.
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- run: tar -czf dist/payments-1.5.0.tar.gz -C build .
- uses: actions/attest-build-provenance@v2
with:
subject-path: dist/payments-1.5.0.tar.gz
Expected output: An attestation is stored against the archive digest in the repository attestation store.
2. Verify the downloaded archive against the repository and the release branch.
gh attestation verify dist/payments-1.5.0.tar.gz --repo acme/payments --source-ref refs/heads/mainExpected output: Verification prints the signer workflow, commit, and source ref, and exits with code 0.
3. Bind the deploy to the verified digest instead of the file name.
sha256sum dist/payments-1.5.0.tar.gzExpected output: The printed digest matches the attestation subject and is recorded in the deployment manifest.
4. Test that a modified archive is rejected.
printf 'x' >> dist/payments-1.5.0.tar.gz && gh attestation verify dist/payments-1.5.0.tar.gz --repo acme/paymentsExpected output: Verification exits non-zero with no matching attestation, proving the gate blocks a modified archive.
Troubleshooting
Problem
Verification reports no attestations found
Solution
Attestations are bound to the repository and the exact digest; verify the bytes that were built against the repository that built them.
Problem
Verification fails on the source ref
Solution
Pass the branch that produced the artifact, or omit --source-ref only when any branch is acceptable by policy.
Limitations
- Attestations prove origin and integrity, not that the artifact is free of vulnerabilities.
- Private repositories require an authenticated gh session or token to read attestations.
Release files need the same proof as images
Container images usually get signing and provenance because the registry supports it; release tarballs are downloaded from a bucket and trusted by file name. That trust is misplaced. Anyone who can write to the bucket can replace the archive, and a deploy script that downloads payments-1.5.0.tar.gz will happily unpack whatever it finds. Attestations bind a digest to the workflow identity and the commit that produced it, so the deploy job can check both before unpacking.
Attest in the build job
Attestation must happen in the job that builds the artifact, with id-token: write so GitHub can issue the OIDC token that signs the statement. Pass subject-path rather than a digest you computed by hand; the action hashes the file and records that hash. Save the archive as a workflow artifact so the deploy job consumes the same bytes, then verify the download before use.
- uses: actions/attest-build-provenance@v2
with:
subject-path: dist/payments-1.5.0.tar.gz
- run: gh attestation verify dist/payments-1.5.0.tar.gz --repo acme/payments --source-ref refs/heads/main
Verify, then publish the digest
gh attestation verify checks the signature, the repository, the workflow, and the source ref. Make the deploy fail closed: no attestation, a different repository, or a different branch should all stop the release. Record the artifact digest alongside the deployment, because the digest is the only identifier that cannot be reused for a different file. Run the tamper test once in a staging pipeline; a gate that has never rejected anything is not known to work.