Automation

GitHub Actions : prouver la provenance d'un artefact avant le déploiement en production

Un runbook de production pour relier commit, workflow, digest, attestation, approbation et identité Azure avant de promouvoir un artefact GitHub Actions.

27 août 2026 github-actionsartifactprovenanceattestationsupply-chaindevopsazureoidcsecurityautomationrunbookrollbackproduction

Un workflow GitHub Actions a produit l’image orders-api:2026.08.27. Les tests sont verts et le job de déploiement attend une approbation pour l’environnement production. Pourtant, l’équipe ne sait pas répondre à une question simple : l’image qui va être déployée est-elle exactement celle construite depuis le commit validé ?

Relancer le build depuis la branche principale ne résout pas ce doute. Cela crée un nouvel artefact, potentiellement avec d’autres dépendances, un autre runner ou des actions mises à jour. Le runbook doit relier un artefact immuable à son dépôt, son commit, son workflow, son digest et son attestation, puis vérifier que l’identité de déploiement ne peut pas substituer une autre image au dernier moment.

Figer le candidat de déploiement

Commencez par une fiche de promotion indépendante du tag. Un tag de conteneur est un pointeur pratique, pas une identité suffisante pour une décision de production. Le candidat doit être décrit par son digest et par l’exécution qui l’a produit.

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

Conservez aussi l’heure UTC, l’acteur qui demande la promotion et le digest actuellement déployé. Sans cet état avant changement, un rollback risque de revenir vers un tag qui a lui-même bougé.

Séparer commit, run, artefact et déploiement

Une chaîne de promotion contient quatre objets différents :

  • le commit revu, qui décrit les sources attendues ;
  • le run de build, qui exécute un workflow à un instant donné ;
  • l’artefact, identifié par son digest ;
  • le déploiement, qui applique ce digest à une cible Azure.

Un statut vert sur le commit ne prouve pas à lui seul que le digest candidat vient de ce run. À l’inverse, un digest connu ne prouve pas que le workflow autorisé l’a produit. Le contrôle utile consiste à joindre ces objets sans reconstruire l’artefact.

Listez les informations du run et récupérez l’artefact dans un répertoire propre. Ne mélangez pas les sorties d’une ancienne exécution avec le candidat en cours.

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/*

Le SHA-256 d’une archive téléchargée peut différer du digest OCI de l’image : ce sont deux objets distincts. Pour une image, figez le digest retourné par le registre lors du push et utilisez ensuite la référence <registry>/<repository>@sha256:<digest>. N’inférez pas le digest d’image depuis le nom du fichier ou le tag.

Vérifier l’attestation, pas seulement sa présence

Une attestation de provenance relie le sujet vérifié à une identité de build. Sa simple existence n’est pas suffisante : il faut vérifier la signature, le digest du sujet et l’identité attendue du producteur.

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

Conservez la sortie JSON brute comme preuve. La commande vérifie le contenu, borne le dépôt, impose le workflow signataire et contrôle le commit source ; elle doit échouer si l’une de ces identités diverge. Une attestation valide issue d’un autre workflow n’autorise pas automatiquement la production. Avec un workflow réutilisable, --signer-workflow doit désigner ce workflow signataire, pas seulement le workflow appelant.

Pour une image OCI, passez directement oci://<registry>/<repository>@sha256:<digest> à gh attestation verify avec les mêmes contraintes de dépôt, workflow et commit. Le point décisif reste le même : le sujet vérifié doit être l’objet effectivement transmis au déploiement.

Empêcher la substitution après validation

Le job de déploiement ne doit pas recalculer un tag comme latest ou ${{ github.ref_name }} après l’approbation. Il doit recevoir le digest validé comme sortie explicite du job de qualification ou comme entrée d’un workflow de promotion contrôlé.

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 }}'

Ce fragment illustre le contrat, pas un workflow universel. Épinglez les actions selon la politique du dépôt et bornez les permissions par job. Le job deploy doit être incapable de pousser une nouvelle image : son rôle est de promouvoir un digest déjà qualifié, pas de reconstruire ou de republier.

Lier l’approbation à l’identité Azure

L’environnement GitHub production fournit le point de contrôle humain ou organisationnel. L’identité fédérée Azure fournit le périmètre d’exécution. Les deux doivent porter sur le même contrat.

Vérifiez au minimum :

  • que le job de déploiement référence bien l’environnement production ;
  • que les règles de protection attendues sont actives et que l’initiateur ne peut pas s’auto-approuver lorsque la séparation des rôles l’exige ;
  • que le credential fédéré Azure limite le sujet au dépôt, au workflow ou à l’environnement prévu ;
  • que l’identité n’a que les droits nécessaires sur la cible de déploiement ;
  • qu’elle ne peut pas modifier le registre ou la source de l’artefact si ce pouvoir n’est pas requis.

OIDC évite un secret Azure durable, mais ne corrige pas un sujet trop large ni un rôle surdimensionné. Une authentification réussie prouve seulement qu’un jeton a été accepté. Elle ne prouve pas que le bon artefact a été sélectionné.

Valider par canari sans perdre le digest

La validation runtime doit rester attachée au même digest. Déployez-le d’abord sur un slot, une révision ou un périmètre canari si la plateforme le permet. Relevez le digest réellement exécuté, puis comparez-le au candidat avant d’observer les métriques.

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

Une application saine exécutant un digest inattendu reste un échec de promotion. De même, un digest correctement attesté mais défaillant en runtime doit être rollbacké : la provenance répond à « d’où vient cet objet ? », pas à « fonctionne-t-il correctement en production ? ».

Décider promotion, blocage, rebuild ou 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.

Ne « corrigez » jamais une attestation manquante en attestant après coup un binaire dont l’origine n’est plus démontrable. Reproduisez le build dans la chaîne autorisée. Le nouvel artefact reçoit un nouveau digest et recommence la qualification depuis le début.

Conclusion

Un pipeline vert n’est pas encore une preuve de provenance. Une promotion fiable relie le commit revu, le run autorisé, le digest immuable, l’attestation vérifiée, l’approbation de l’environnement et l’identité Azure qui applique le changement.

La décision devient alors exploitable : promouvoir le digest prouvé, conserver le candidat tant que la preuve est incomplète, reconstruire si l’origine ne peut plus être établie, ou rollbacker vers le digest précédent si le canari échoue. L’objectif n’est pas d’ajouter une case de sécurité au pipeline, mais d’empêcher qu’un artefact différent entre en production entre le build et le déploiement.