Aller au contenu
English
CI/CD & Automatisation medium

Lab : promouvoir un déploiement approuvé

Read this page in English

55 min de lecture

logo github

Le lab sécurité construit un dépôt qui publie une image attestée sur GHCR. Il s'arrête là : rien ne dit qui autorise la mise en production, ni comment revenir en arrière. Ce lab ajoute la pièce manquante, le workflow deploy.yml, et met en évidence la propriété qui fait toute la valeur des environnements : un job en attente d'approbation n'a jamais vu les secrets de production.

  • Deux environnements, staging et production, avec des secrets distincts
  • Une approbation obligatoire et une restriction de branche sur la production
  • Un workflow deploy.yml qui promeut un digest vérifié, sans rebuild
  • Un rollback qui rejoue la version précédente par le même chemin

Ce lab suppose acquis le build vérifiable : vous avez un dépôt qui publie une image sur GHCR avec une attestation de provenance. Le dépôt de référence github.com/stephrobert/secure-python-pipeline en fournit la base ; forkez-le dans votre compte, ce lab écrit dans les réglages du dépôt et dans ses workflows.

Côté outillage, la CLI gh est authentifiée et Docker est disponible en local. Côté GitHub, un point n'est pas négociable : les règles de protection d'environnement exigent un dépôt public sur un plan Free, ou au minimum GitHub Pro sur un dépôt privé. Laissez votre fork public, c'est d'ailleurs ce qui rendra vos attestations vérifiables par un tiers.

  1. Ouvrir les réglages du fork, section Settings > Environments.

  2. Créer staging avec New environment. Aucune règle pour l'instant.

  3. Créer production de la même façon.

  4. Ajouter un secret à chacun : Environment secrets > Add secret, nommé DEPLOY_TOKEN dans les deux cas, avec des valeurs différentes et reconnaissables, par exemple staging-token-abc et prod-token-xyz.

    Ces valeurs factices sont volontaires : le lab va prouver quel secret chaque job reçoit, et à quel moment.

Vérification immédiate, les deux environnements apparaissent dans la liste, et chacun affiche 1 secret.

C'est ici que l'environnement cesse d'être une étiquette.

  1. Ouvrir production, puis cocher Required reviewers.

  2. S'ajouter soi-même comme relecteur. Sur un dépôt personnel, vous êtes le seul compte disponible.

  3. Laisser activée l'option qui autorise l'auto-approbation. Sans elle, la personne qui déclenche le déploiement ne peut pas l'approuver, et vous bloqueriez votre propre lab. En équipe, c'est l'inverse qu'il faut faire : désactiver l'auto-approbation est précisément ce qui crée le contrôle à quatre yeux.

  4. Activer Deployment branches and tags, puis choisir Selected branches and tags et ajouter le motif main.

  5. Enregistrer. L'environnement staging, lui, reste sans règle.

Créez .github/workflows/deploy.yml. Le workflow prend un digest en entrée, le vérifie, le déploie sur staging, puis attend l'approbation avant la production.

.github/workflows/deploy.yml
name: Deploy
on:
workflow_dispatch:
inputs:
digest:
description: "Digest de l'image à déployer (sha256:...)"
required: true
type: string
# Aucune permission par défaut.
permissions: {}
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
deploy-staging:
name: Déployer sur staging
runs-on: ubuntu-24.04
timeout-minutes: 10
environment:
name: staging
url: https://staging.example.invalid
permissions:
contents: read
packages: read # pull de l'image depuis GHCR
attestations: read # lecture de l'attestation de provenance
env:
IMAGE: ghcr.io/${{ github.repository }}
DIGEST: ${{ inputs.digest }}
steps:
- name: Harden runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Login GHCR
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Vérifier la provenance
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh attestation verify "oci://${IMAGE}@${DIGEST}" --owner "${GITHUB_REPOSITORY_OWNER}"
- name: Déployer (simulation)
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
if [ -z "${DEPLOY_TOKEN}" ]; then
echo "Aucun jeton d'environnement reçu" >&2
exit 1
fi
echo "Cible : ${IMAGE}@${DIGEST}"
echo "Empreinte : $(printf %s "${DEPLOY_TOKEN}" | sha256sum | cut -c1-12)"
docker run --rm --detach --name app-staging "${IMAGE}@${DIGEST}"
docker ps --filter name=app-staging
deploy-production:
name: Déployer en production
needs: deploy-staging
runs-on: ubuntu-24.04
timeout-minutes: 10
environment:
name: production
url: https://example.invalid
permissions:
contents: read
packages: read
attestations: read
env:
IMAGE: ghcr.io/${{ github.repository }}
DIGEST: ${{ inputs.digest }}
steps:
- name: Harden runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit
- name: Login GHCR
uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c # v4.5.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Vérifier la provenance
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh attestation verify "oci://${IMAGE}@${DIGEST}" --owner "${GITHUB_REPOSITORY_OWNER}"
- name: Déployer (simulation)
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
if [ -z "${DEPLOY_TOKEN}" ]; then
echo "Aucun jeton d'environnement reçu" >&2
exit 1
fi
echo "Cible : ${IMAGE}@${DIGEST}"
echo "Empreinte : $(printf %s "${DEPLOY_TOKEN}" | sha256sum | cut -c1-12)"
docker run --rm --detach --name app-prod "${IMAGE}@${DIGEST}"
docker ps --filter name=app-prod

Quatre choix méritent un mot. Le digest arrive par env:, jamais interpolé dans la commande, parce que c'est une entrée fournie par un humain. La vérification de provenance est répétée dans les deux jobs, parce qu'une approbation peut durer des heures et que seul le contrôle le plus tardif dit quelque chose de l'instant du déploiement. cancel-in-progress vaut false : sur un déploiement, interrompre vaut pire que retarder.

Enfin, le jeton n'est jamais affiché. Le masquage automatique de GitHub ne couvre que la valeur exacte : en publier les premiers caractères le contourne. On imprime donc une empreinte SHA-256 tronquée, qui suffit à prouver que les deux environnements délivrent des secrets différents sans en révéler aucun.

Le digest de la dernière image publiée se lit directement depuis le registre, sans passer par l'API :

Fenêtre de terminal
docker buildx imagetools inspect ghcr.io/VOTRE-COMPTE/secure-python-pipeline:latest \
--format '{{.Manifest.Digest}}'

Conservez la valeur retournée, de la forme sha256:.... Notez-la quelque part : au moment du rollback, ce sera le digest précédent qu'il faudra retrouver.

Étape 5 : lancer la promotion et observer l'attente

Section intitulée « Étape 5 : lancer la promotion et observer l'attente »
Fenêtre de terminal
gh workflow run deploy.yml -f digest=sha256:VOTRE_DIGEST
gh run watch

Le comportement attendu, dans l'ordre :

  1. deploy-staging démarre immédiatement. L'environnement staging n'a aucune règle. Les logs affichent l'empreinte du jeton de staging. Notez-la.

  2. deploy-production passe en attente. GitHub affiche Review pending deployments, et le job ne démarre pas.

  3. Les logs de production sont vides. C'est le point à observer : le runner n'a pas démarré, donc il n'a pas reçu DEPLOY_TOKEN. Le secret de production n'a jamais quitté GitHub.

  4. Approuver depuis l'interface de l'exécution, bouton Review deployments, cocher production, puis Approve and deploy.

  5. Le job démarre et affiche une empreinte différente de celle de staging : c'est la preuve que chaque environnement a délivré son propre secret.

Fenêtre de terminal
# Les déploiements enregistrés pour la production
gh api "repos/VOTRE-COMPTE/secure-python-pipeline/deployments?environment=production" \
--jq '.[] | {id, sha, created_at}'

Dans l'interface, l'onglet Deployments du dépôt affiche les deux environnements avec leur dernier déploiement et l'URL renseignée dans le workflow. Cette page est ce que vous consulterez en incident pour savoir quelle version tourne où, et depuis quand.

La règle posée à l'étape 2 limite la production à main. Vérifions qu'elle mord.

  1. Créer une branche et y pousser le workflow tel quel :

    Fenêtre de terminal
    git switch -c test-protection
    git push -u origin test-protection
  2. Lancer le workflow depuis cette branche :

    Fenêtre de terminal
    gh workflow run deploy.yml --ref test-protection -f digest=sha256:VOTRE_DIGEST
  3. Observer : deploy-staging s'exécute, deploy-production échoue immédiatement, avec un message indiquant que la branche n'est pas autorisée à déployer sur cet environnement.

Le job échoue avant de démarrer, donc avant toute délivrance de secret. La restriction n'est pas un contrôle applicatif, c'est une porte fermée en amont.

Le rollback n'a pas de workflow dédié : c'est le même, avec l'autre digest. Encore faut-il qu'il existe un autre digest. Un fork qui ne compte qu'une seule release n'a rien où revenir : publiez-en une seconde avant de continuer.

Fenêtre de terminal
# Modifier une ligne de l'application, puis publier une nouvelle version
gh release create v1.0.1 --generate-notes

Le workflow release.yml construit et atteste alors une seconde image. C'est vers la première que vous reviendrez.

  1. Retrouver le digest précédent, dans vos notes de l'étape 4 ou dans les versions publiées du paquet :

    Fenêtre de terminal
    gh api "user/packages/container/secure-python-pipeline/versions" \
    --jq '.[] | {digest: .name, tags: .metadata.container.tags, created_at}'
  2. Relancer le déploiement sur cette valeur :

    Fenêtre de terminal
    gh workflow run deploy.yml -f digest=sha256:DIGEST_PRECEDENT
  3. Approuver de nouveau la production.

Le retour arrière emprunte exactement le même chemin contrôlé que la mise en production : provenance vérifiée, approbation tracée, déploiement enregistré. Il n'y a aucun rebuild, donc aucun risque nouveau introduit au pire moment.

Le lab est réussi quand ces six affirmations sont vraies sur votre fork :

ContrôleRésultat attendu
Environnementsstaging et production existent, chacun avec son DEPLOY_TOKEN
ApprobationLa production passe par Review pending deployments
Étanchéité des secretsAucun log de production avant l'approbation
Jetons distinctsLes deux jobs affichent des empreintes de jeton différentes
Restriction de brancheLe déploiement production depuis une autre branche échoue
RollbackLe digest précédent se redéploie sans rebuild

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Concurrency : Le groupement d'exécutions utilisé dans le workflow du lab, détaillé et généralisé.
  • Runners : introduction : Choisir et durcir les machines qui exécuteront ces déploiements pour de vrai.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn