Automation

GitHub Actions: prove artifact provenance before production deployment

A production runbook for tying commit, workflow, digest, attestation, approval and Azure identity together before promoting a GitHub Actions artifact.

27 Aug 2026 github-actionsartifactprovenanceattestationsupply-chaindevopsazureoidcsecurityautomationrunbookrollbackproduction

A GitHub Actions workflow has produced the image orders-api:2026.08.27. Tests are green and the deployment job is waiting for approval on the production environment. The team still cannot answer one basic question: is the image about to be deployed exactly the one built from the reviewed commit?

Rebuilding from the default branch does not remove that uncertainty. It creates a different artifact, possibly with different dependencies, a different runner or updated actions. This runbook links one immutable artifact to its repository, commit, workflow, digest and attestation, then checks that the deployment identity cannot substitute another image at the last moment.

Freeze the deployment candidate

Start with a promotion record that does not depend on a tag. A container tag is a useful pointer, not a sufficient identity for a production decision. Describe the candidate by digest and by the workflow run that produced it.

yaml artifact-promotion-card.yml
candidate:
repository: <owner>/<repository>
commit_sha: <full-commit-sha>
workflow: .github/workflows/build.yml
workflow_run_id: <run-id>
artifact_name: orders-api
registry: <registry>.azurecr.io
image_repository: platform/orders-api
image_digest: sha256:<digest>
target_environment: production

required_evidence:
- successful build and test jobs
- immutable artifact digest
- valid provenance attestation
- expected workflow identity
- production environment approval
- bounded Azure deployment identity
- rollback digest already available

Record the UTC timestamp, the actor requesting promotion and the digest currently running in production. Without the pre-change state, rollback may resolve a tag that has moved since the deployment.

Separate commit, run, artifact and deployment

A promotion chain contains four different objects:

  • the reviewed commit, which defines the expected source;
  • the build run, which executes a workflow at a point in time;
  • the artifact, identified by its digest;
  • the deployment, which applies that digest to an Azure target.

A green commit status does not prove that the candidate digest came from the corresponding run. A known digest does not prove that the authorized workflow produced it either. The useful control joins these objects without rebuilding the artifact.

Inspect the run and download the artifact into a clean directory. Do not mix outputs from an older execution with the current candidate.

bash collect-build-evidence.sh
set -euo pipefail

repo="<owner>/<repository>"
run_id="<run-id>"
artifact="orders-api"

gh run view "$run_id" --repo "$repo" \
--json databaseId,headSha,event,status,conclusion,workflowName,url

rm -rf ./promotion-candidate
mkdir ./promotion-candidate
gh run download "$run_id" --repo "$repo" \
--name "$artifact" --dir ./promotion-candidate

sha256sum ./promotion-candidate/*

The SHA-256 of a downloaded archive may differ from the OCI image digest because they identify different objects. For an image, freeze the digest returned by the registry when the image is pushed and deploy <registry>/<repository>@sha256:<digest>. Do not infer an image digest from a filename or tag.

Verify the attestation, not its presence

A provenance attestation binds the verified subject to a build identity. Its presence alone is not enough: verify the signature, the subject digest and the expected producer identity.

bash verify-artifact-attestation.sh
set -euo pipefail

repo="<owner>/<repository>"
artifact_path="./promotion-candidate/orders-api.tar.gz"
commit_sha="<full-commit-sha>"
signer_workflow="<owner>/<repository>/.github/workflows/build.yml"

gh attestation verify "$artifact_path" \
--repo "$repo" \
--signer-workflow "$signer_workflow" \
--source-digest "$commit_sha" \
--format json > attestation-verification.json

jq -e '
length > 0 and
all(.[]; .verificationResult.statement.subject | length > 0)
' attestation-verification.json

Retain the raw JSON output as evidence. The command verifies the content, scopes the repository, enforces the signer workflow and checks the source commit; it must fail when any of these identities diverges. A valid attestation produced by another workflow does not automatically authorize production. When a reusable workflow signs the artifact, --signer-workflow must name that signer rather than only the caller workflow.

For an OCI image, pass oci://<registry>/<repository>@sha256:<digest> directly to gh attestation verify with the same repository, workflow and commit constraints. The invariant is the same: the verified subject must be the object handed to deployment.

Prevent substitution after qualification

The deployment job must not resolve a tag such as latest or ${{ github.ref_name }} after approval. It should receive the qualified digest as an explicit output of the verification job or as an input to a controlled promotion workflow.

yaml deploy-by-digest.yml
jobs:
qualify:
  permissions:
    contents: read
    attestations: read
  outputs:
    image_ref: ${{ steps.candidate.outputs.image_ref }}
  steps:
    - name: Freeze candidate
      id: candidate
      run: |
        image_ref='<registry>.azurecr.io/platform/orders-api@sha256:<digest>'
        echo "image_ref=$image_ref" >> "$GITHUB_OUTPUT"
    - name: Verify provenance
      run: ./scripts/verify-provenance.sh '${{ steps.candidate.outputs.image_ref }}'

deploy:
  needs: qualify
  environment: production
  permissions:
    contents: read
    id-token: write
  steps:
    - name: Login to Azure with OIDC
      uses: azure/login@<pinned-version-or-sha>
      with:
        client-id: ${{ vars.AZURE_CLIENT_ID }}
        tenant-id: ${{ vars.AZURE_TENANT_ID }}
        subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
    - name: Deploy the qualified digest
      run: ./scripts/deploy.sh '${{ needs.qualify.outputs.image_ref }}'

This fragment illustrates the contract, not a universal workflow. Pin actions according to repository policy and keep permissions scoped per job. The deploy job should not be able to push a new image: its role is to promote a qualified digest, not rebuild or republish it.

Bind approval to the Azure identity

The GitHub production environment supplies the human or organizational control point. The Azure federated identity supplies the execution boundary. Both need to describe the same contract.

Verify at least that:

  • the deployment job references the production environment;
  • expected protection rules are active and the initiator cannot self-approve when separation of duties requires it;
  • the Azure federated credential restricts its subject to the intended repository, workflow or environment;
  • the identity has only the permissions required on the deployment target;
  • it cannot modify the registry or artifact source when that capability is unnecessary.

OIDC removes a long-lived Azure secret, but it does not fix an overbroad subject or an oversized role. Successful authentication only proves that a token was accepted. It does not prove that the correct artifact was selected.

Canary without losing the digest

Runtime validation must stay attached to the same digest. Deploy it first to a slot, revision or canary scope when the platform supports one. Read the digest actually running and compare it with the candidate before looking at service metrics.

text promotion-validation.txt
Identity checks
Runtime image digest equals the qualified digest
Deployment run and Azure activity are correlated
No tag resolution occurred after approval

Service checks
Health probe succeeds
One representative read path succeeds
One bounded write path is verified if required
Error rate and latency stay within the agreed window

Stop conditions
Runtime digest differs
Attestation cannot be reconstructed
Azure identity exceeds the expected scope
Canary creates duplicate or partial effects
Rollback digest is unavailable

A healthy application running an unexpected digest is still a failed promotion. Conversely, an attested digest that fails at runtime must be rolled back: provenance answers “where did this object come from?”, not “does it behave correctly in production?”.

Decide promote, hold, rebuild or rollback

text artifact-promotion-decision.txt
PROMOTE
Commit, workflow, run, subject digest and attestation match.
Environment approval and Azure identity are compliant.
Canary runs the exact qualified digest and passes validation.

HOLD
Evidence is incomplete but no production change has started.
Preserve the artifact and run metadata; repair the verification path.

REBUILD
The artifact cannot be tied to an authorized build identity.
Build once from the approved commit in the controlled workflow,
issue a new attestation and qualify the new digest.

ROLLBACK
The qualified digest is deployed but runtime validation fails.
Redeploy the previously recorded digest, verify runtime identity,
then keep the failed digest and evidence for diagnosis.

STOP AND INVESTIGATE
Digest changed after approval, provenance points to another workflow,
or the deploy identity can replace the candidate outside the contract.

Never repair a missing attestation by attesting a binary after the fact when its origin is no longer demonstrable. Reproduce the build in the authorized workflow. The new artifact receives a new digest and starts qualification again.

Conclusion

A green pipeline is not yet proof of provenance. A reliable promotion ties together the reviewed commit, authorized run, immutable digest, verified attestation, environment approval and Azure identity applying the change.

The resulting decision is operational: promote the proven digest, hold the candidate while evidence is incomplete, rebuild when origin cannot be established, or roll back to the recorded previous digest when the canary fails. The purpose is not to add another security checkbox to the pipeline. It is to prevent a different artifact from entering production between build and deployment.