Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Pipeline CI/CD sécurisé : gates, secrets, signature

Par mis à jour le

Améliorer cette page

Un pipeline CI/CD sécurisé n'est pas un pipeline qui lance des scanners : c'est un pipeline qui s'arrête quand un scanner trouve ce que l'équipe a décidé de bloquer. Cette leçon assemble les quatre piliers de cette défense, les security gates, les secrets, l'isolation et la signature, pour un DevOps qui a déjà un pipeline fonctionnel. Elle s'appuie sur des mesures : les codes de sortie des scanners vus au module précédent, qui montrent que la moitié d'entre eux ne bloquent rien par défaut, un workflow de gates passé à trois scanners de workflows, et une image remplacée sous le même tag que cosign verify refuse.

  • Expliquer pourquoi le pipeline concentre ce qu'un attaquant cherche
  • Placer les security gates aux transitions du pipeline, et décider de leurs seuils
  • Faire bloquer chaque scanner, à partir de son code de sortie mesuré
  • Rédiger un workflow de gates validé par actionlint, zizmor et poutine
  • Limiter ce qu'un job compromis peut atteindre : secrets, permissions, runners
  • Refuser au déploiement une image que personne n'a signée

Un pipeline transforme du code source en artefact déployé, et il réunit pour cela trois choses qu'un attaquant cherche ailleurs séparément : des secrets (jetons de déploiement, identifiants cloud), des privilèges (publier un paquet, pousser une image, déployer) et une exécution automatique à chaque commit. Le compromettre, c'est faire signer et livrer son code par la chaîne de la victime.

L'attaque de tj-actions/changed-files, en mars 2025, l'a montré sans détour : en déplaçant les tags de version d'une action, l'attaquant a fait imprimer les secrets de plus de 23 000 dépôts dans leurs journaux. Aucun test n'a échoué, aucun scanner applicatif n'a sonné : la menace ne venait pas du code, mais de ce que le pipeline exécute et de ce qu'il expose. La leçon précédente l'a démontré sur un tag déplacé ; celle-ci construit la défense.

PilierCe qu'il empêcheLeçon dédiée
Security gatesLivrer une faille, un secret ou une dépendance vulnérablecette leçon
SecretsExposer un identifiant à un job qui n'en a pas besoinLes secrets dans les pipelines
IsolationQu'un job compromis atteigne le reste du systèmePermissions minimales
SignatureDéployer un artefact substituéSigner et vérifier avec Cosign

Les quatre piliers sont indissociables. Des gates parfaits n'empêchent pas un secret de fuir dans les journaux, et une image signée par un pipeline sans gates est une faille certifiée. Le maillon le plus faible fixe la solidité de l'ensemble.

Un security gate est un point de décision automatisé : si la condition n'est pas remplie, le pipeline s'arrête et rien ne passe à l'étape suivante. On en place un à chaque transition, comme des sas successifs, pour qu'un contrôle contourné ou mal configuré soit rattrapé par le suivant.

GateTransitionCe qu'il vérifieCe qu'il bloque
Pre-commitAvant le commitSecretsLe commit, sur le poste
Pull requestAvant le mergeSecrets, SAST, SCALe merge dans main
BuildAvant la publicationScan d'image, signatureLa publication de l'artefact
DéploiementAvant la productionSignature vérifiée, DAST en stagingLe déploiement

Chaque gate a sa raison d'être. Le pre-commit donne un retour en quelques secondes, mais se contourne avec --no-verify : il sert le confort du développeur. La pull request est le premier contrôle que personne ne contourne. Le build voit l'artefact réel, couches de l'image de base comprises. Le déploiement vérifie que ce qui part en production est bien ce que le pipeline a produit.

Bloquer sur chaque alerte, faux positifs et sévérités mineures compris, fait perdre confiance aux équipes, qui finissent par contourner ou désactiver le gate. Ne rien bloquer rend le gate décoratif. Le seuil se décide donc par type de constat, et s'écrit noir sur blanc : un secret bloque toujours, car il est binaire ; une CVE dépend de son exploitation et de l'exposition du composant, comme l'a montré la leçon sur les CVE.

ConstatPull requestDéploiement
Secret détectéBloqueBloque
SAST de sévérité ERRORBloqueBloque
CVE présente dans le KEV ou CRITICALBloqueBloque
CVE HIGHAvertit dans la PRBloque, sauf exception datée
Injection confirmée par le DASTsans objetBloque
Image non signéesans objetBloque

Commencez strict sur ce tableau réduit, et élargissez avec la maturité de l'équipe. Une exception se documente avec une date d'expiration, sinon elle devient une dette invisible.

Le runner ne lit pas le rapport d'un scanner : il lit son code de sortie. Un scanner qui trouve des failles et sort en 0 laisse passer le job, quel que soit le contenu de son rapport. Or, mesuré sur les outils du module précédent, la moitié d'entre eux sortent en 0 par défaut même quand ils trouvent quelque chose.

Outil (version mesurée)Par défaut, avec des constatsPour bloquer
Gitleaks 8.30.11rien à ajouter
Semgrep 1.178.00--error, avec --severity ERROR pour le seuil
Trivy 0.74.00--exit-code 1 et --severity
Grype 0.119.00--fail-on <sévérité>, qui sort en 2
ZAP 2.17.02 sur un simple WARNfichier de règles et -I
Nuclei 3.11.10, toujourstester le fichier écrit par -o
cosign verify 3.1.110 sans signature validerien à ajouter

Le seuil fait partie du gate. Mesuré avec Trivy sur un requirements.txt qui ne contient que requests==2.25.1 : 4 vulnérabilités MEDIUM. Sans option, le code vaut 0. Avec --severity HIGH,CRITICAL --exit-code 1, il vaut encore 0, ce qui est voulu : rien ne dépasse le seuil. Avec --severity MEDIUM --exit-code 1, il passe à 1 et le job échoue. Testez toujours un gate dans les deux sens : sur un dépôt qui doit passer, et sur un dépôt qui doit être refusé.

Le workflow ci-dessous assemble les gates de la pull request, puis signe l'image au push sur main. Chaque scanner tourne dans son image officielle épinglée par digest, avec l'option qui le fait bloquer, et les actions sont épinglées par SHA de commit. Le job de build ne démarre que si les trois gates ont réussi.

.github/workflows/security-gates.yml
name: Security gates
on:
pull_request:
branches: [main]
push:
branches: [main]
permissions: {}
jobs:
secrets:
runs-on: ubuntu-24.04
permissions:
contents: read
container:
image: ghcr.io/gitleaks/gitleaks:v8.30.1@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Secrets dans l'historique
run: gitleaks git --redact --exit-code 1 .
sast:
runs-on: ubuntu-24.04
permissions:
contents: read
container:
image: semgrep/semgrep:1.178.0@sha256:32e459968daabe7ab86968184a29109b9564aa00392401156f9788452b42786b
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Analyse statique, bloquante sur ERROR
run: semgrep scan --config p/security-audit --severity ERROR --error --metrics=off .
sca:
runs-on: ubuntu-24.04
permissions:
contents: read
container:
image: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Dépendances, bloquant sur CRITICAL
run: trivy fs --scanners vuln --severity CRITICAL --exit-code 1 .
build-sign:
if: github.event_name == 'push'
needs: [secrets, sast, sca]
runs-on: ubuntu-24.04
permissions:
contents: read
packages: write
id-token: write
outputs:
digest: ${{ steps.push.outputs.digest }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Build et push
id: push
env:
IMAGE: ghcr.io/${{ github.repository }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ACTOR: ${{ github.actor }}
run: |
echo "$GH_TOKEN" | docker login ghcr.io -u "$ACTOR" --password-stdin
docker build -t "$IMAGE:$GITHUB_SHA" .
docker push "$IMAGE:$GITHUB_SHA"
digest=$(docker inspect --format '{{index .RepoDigests 0}}' "$IMAGE:$GITHUB_SHA" | cut -d@ -f2)
echo "digest=$digest" >> "$GITHUB_OUTPUT"
- name: Signature keyless, par digest
env:
IMAGE: ghcr.io/${{ github.repository }}
DIGEST: ${{ steps.push.outputs.digest }}
run: cosign sign --yes "$IMAGE@$DIGEST"

Trois choix méritent d'être lus de près. permissions: {} au niveau du workflow retire tous les droits par défaut, et chaque job ne récupère que les siens : seul build-sign peut écrire un paquet et obtenir un jeton OIDC (id-token: write), dont cosign a besoin pour la signature sans clé. Les valeurs issues du contexte GitHub passent par env:, jamais directement dans run:, ce qui ferme la porte à l'injection de template. Enfin, la signature vise le digest de l'image, pas son tag, pour la raison mesurée plus bas.

Mesuré sur ce fichier : actionlint 1.7.12 sort en 0, zizmor 1.30.0 répond No findings to report (ses 6 remarques pédantes, absence de nom et de commentaire sur les permissions, sont masquées par défaut), et poutine 1.1.6 passe toutes ses règles. Ce n'est pas une garantie de sécurité, c'est l'assurance que le workflow ne reproduit aucun des défauts connus de ces outils. Faites-les tourner sur vos propres workflows, à commencer par zizmor.

Les gates arrêtent une livraison dangereuse, ils ne protègent pas les secrets que le pipeline détient pendant qu'il tourne. Trois règles limitent ce qu'un job compromis peut voler, et la leçon suivante les détaille :

  • un secret n'est jamais écrit dans le code ni dans le workflow, seulement référencé (${{ secrets.NOM }}), et il n'est exposé qu'à l'étape qui l'utilise, pas à tout le workflow ;
  • chaque environnement a ses propres secrets, et l'environnement production exige une approbation avant de les libérer ;
  • un jeton éphémère obtenu par OIDC remplace la clé cloud longue durée : il n'y a plus rien à voler une fois le job terminé.

Le ver Shai-Hulud, en 2025, a illustré l'enjeu : son script d'installation cherchait des jetons GitHub, npm et cloud dans l'environnement des machines où il s'exécutait, puis se republiait avec les comptes volés. Un job qui ne détient aucun secret inutile ne lui offrait rien.

On part du principe qu'un job sera un jour compromis, par une action, une dépendance ou une pull request piégée. L'isolation décide alors de l'étendue des dégâts, le « rayon d'explosion ». Elle joue sur quatre axes, qui ne se remplacent pas.

AxeMesureEffet
Permissionspermissions: {} puis droits par jobUn jeton volé ne permet que ce que le job faisait
RunnerRunner éphémère, recréé à chaque jobUne compromission ne survit pas au job
RéseauRunner sans route vers le réseau interneLe job n'atteint pas la production
EnvironnementSecrets et comptes séparés par environnementUne fuite en test ne touche pas la production

Les runners hébergés par GitHub sont éphémères : une machine neuve par job, détruite ensuite. Un runner auto-hébergé permanent, au contraire, garde d'un job à l'autre fichiers, caches et processus : un job piégé peut y laisser de quoi attaquer le suivant. Réservez-les aux besoins réels, en mode éphémère, et jamais aux pull requests venues de forks.

La signature répond à une question que les gates ne posent pas : l'image qui part en production est-elle celle que le pipeline a construite ? Un attaquant qui obtient un accès en écriture au registre peut pousser une autre image sous le même tag. La mesure se rejoue avec un registre local et une paire de clés cosign ; en CI, la signature sans clé (keyless) remplace la paire de clés par l'identité OIDC du workflow, détaillée dans la leçon Cosign.

Fenêtre de terminal
IMG=localhost:5055/api-facturation:1.0.0
cosign verify --key cosign.pub --insecure-ignore-tlog "$IMG"

Mesuré avec cosign 3.1.1, en trois temps :

SituationCode de cosign verifyMessage
Image signée par le pipeline0The signatures were verified against the specified public key
Autre image poussée sous le même tag10Error: no signatures found
Vérification par le digest d'origine, après la substitution0l'image d'origine reste vérifiable

Le gate de déploiement tient donc en une commande, dont le code 10 arrête le pipeline. La troisième ligne dit comment déployer : par digest, pas par tag. Le tag a changé de contenu sous vos yeux ; le digest désigne une image et une seule. L'option --insecure-ignore-tlog n'est là que parce que le banc signe sans journal de transparence public ; en CI, la signature keyless s'inscrit dans Rekor et la vérification le consulte.

Le schéma résume la substitution : le tag 1.0.0 désignait l'image signée, il désigne désormais l'image piégée, et seule la vérification par digest retrouve l'original.

Substitution d'image sous le même tag : le pipeline construit puis signe l'image d'origine, de digest A ; un attaquant qui a un accès en écriture au registre pousse une image piégée, de digest B, sans signature ; le tag 1.0.0, qui désignait l'image d'origine, désigne après substitution l'image piégée. cosign verify sur le tag sort en code 10 et refuse le déploiement, cosign verify sur le digest A sort en code 0.avantaprès substitutionPipeline : buildpuis cosign signAttaquant avec accèsen écriture au registreImage d'originedigest A, signéeImage piégéedigest B, sans signatureTag 1.0.0cosign verify sur le tagcode 10 : refuséecosign verify sur le digest Acode 0 : déployable
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Une signature prouve que l'image vient du pipeline et n'a pas été modifiée. Elle ne dit rien de comment elle a été construite : un Dockerfile piégé avant le build produit une image piégée, correctement signée. L'attestation de provenance ajoute ces informations, dépôt, commit, workflow et runner, signées avec l'artefact. Le cadre SLSA en définit les niveaux, de L0 à L3 sur la piste Build ; viser L2 est atteignable avec les outils natifs de GitHub Actions. Le module sur la supply chain y consacre la leçon de la provenance SLSA à la décision.

La checklist se lit par paliers : le premier se met en place en quelques jours sur un pipeline existant, le dernier demande un outillage et une organisation plus lourds. Chaque case correspond à un contrôle vu dans cette leçon ou dans les leçons du module.

Socle, immédiatement :

  • Chaque scanner bloque sur un seuil écrit, vérifié par son code de sortie
  • Les gates sont déclarés requis dans la protection de main
  • permissions: {} au niveau du workflow, droits minimaux par job
  • Actions et images épinglées par SHA ou digest

Ensuite :

  • Secrets exposés à la seule étape qui les utilise, OIDC à la place des clés cloud
  • Workflows passés à zizmor, actionlint et poutine en CI
  • Images signées, et signature vérifiée au déploiement, par digest

Maturité avancée :

  • Attestations de provenance SLSA L2 ou plus
  • SBOM publié avec chaque artefact
  • Runners auto-hébergés éphémères et isolés du réseau interne
  • Le pipeline concentre secrets, privilèges et exécution automatique : c'est ce qui en fait une cible.
  • Un gate se place à chaque transition et bloque sur un seuil décidé et écrit.
  • Le runner ne lit que le code de sortie : Semgrep, Trivy, Grype et Nuclei sortent en 0 par défaut même avec des constats.
  • Un gate se teste dans les deux sens : un dépôt qui doit passer, un dépôt qui doit être refusé.
  • Le workflow de gates mesuré passe actionlint, zizmor et poutine sans constat, grâce à permissions: {}, au SHA et à env:.
  • cosign verify sort en 10 sur une image substituée sous le même tag : on déploie par digest.
  • Un gate non requis dans la protection de branche n'empêche aucun merge.

Vérifiez que l'essentiel de cette leçon 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

Les questions ci-dessous reprennent les doutes les plus fréquents sur la construction d'un pipeline sécurisé : par où commencer, quoi bloquer, et ce qui distingue une signature d'une attestation.

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn