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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- 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
Pourquoi le pipeline est-il une cible ?
Section intitulée « Pourquoi le pipeline est-il une cible ? »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.
| Pilier | Ce qu'il empêche | Leçon dédiée |
|---|---|---|
| Security gates | Livrer une faille, un secret ou une dépendance vulnérable | cette leçon |
| Secrets | Exposer un identifiant à un job qui n'en a pas besoin | Les secrets dans les pipelines |
| Isolation | Qu'un job compromis atteigne le reste du système | Permissions minimales |
| Signature | Dé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.
Où placer les security gates ?
Section intitulée « Où placer les security gates ? »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.
| Gate | Transition | Ce qu'il vérifie | Ce qu'il bloque |
|---|---|---|---|
| Pre-commit | Avant le commit | Secrets | Le commit, sur le poste |
| Pull request | Avant le merge | Secrets, SAST, SCA | Le merge dans main |
| Build | Avant la publication | Scan d'image, signature | La publication de l'artefact |
| Déploiement | Avant la production | Signature vérifiée, DAST en staging | Le 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.
Décider des seuils
Section intitulée « Décider des seuils »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.
| Constat | Pull request | Déploiement |
|---|---|---|
| Secret détecté | Bloque | Bloque |
| SAST de sévérité ERROR | Bloque | Bloque |
| CVE présente dans le KEV ou CRITICAL | Bloque | Bloque |
| CVE HIGH | Avertit dans la PR | Bloque, sauf exception datée |
| Injection confirmée par le DAST | sans objet | Bloque |
| Image non signée | sans objet | Bloque |
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.
Un gate, c'est un code de sortie
Section intitulée « Un gate, c'est un code de sortie »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 constats | Pour bloquer |
|---|---|---|
| Gitleaks 8.30.1 | 1 | rien à ajouter |
| Semgrep 1.178.0 | 0 | --error, avec --severity ERROR pour le seuil |
| Trivy 0.74.0 | 0 | --exit-code 1 et --severity |
| Grype 0.119.0 | 0 | --fail-on <sévérité>, qui sort en 2 |
| ZAP 2.17.0 | 2 sur un simple WARN | fichier de règles et -I |
| Nuclei 3.11.1 | 0, toujours | tester le fichier écrit par -o |
| cosign verify 3.1.1 | 10 sans signature valide | rien à 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é.
Un workflow de gates complet
Section intitulée « Un workflow de gates complet »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.
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.
Protéger les secrets du pipeline
Section intitulée « Protéger les secrets du pipeline »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
productionexige 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.
Limiter ce qu'un job compromis peut atteindre
Section intitulée « Limiter ce qu'un job compromis peut atteindre »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.
| Axe | Mesure | Effet |
|---|---|---|
| Permissions | permissions: {} puis droits par job | Un jeton volé ne permet que ce que le job faisait |
| Runner | Runner éphémère, recréé à chaque job | Une compromission ne survit pas au job |
| Réseau | Runner sans route vers le réseau interne | Le job n'atteint pas la production |
| Environnement | Secrets et comptes séparés par environnement | Une 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.
Refuser une image substituée au déploiement
Section intitulée « Refuser une image substituée au déploiement »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.
IMG=localhost:5055/api-facturation:1.0.0cosign verify --key cosign.pub --insecure-ignore-tlog "$IMG"Mesuré avec cosign 3.1.1, en trois temps :
| Situation | Code de cosign verify | Message |
|---|---|---|
| Image signée par le pipeline | 0 | The signatures were verified against the specified public key |
| Autre image poussée sous le même tag | 10 | Error: no signatures found |
| Vérification par le digest d'origine, après la substitution | 0 | l'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.
Signature et attestation de provenance
Section intitulée « Signature et attestation de provenance »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.
Checklist d'un pipeline sécurisé
Section intitulée « Checklist d'un pipeline sécurisé »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
À retenir
Section intitulée « À retenir »- 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
0par 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 verifysort 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.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »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
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
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Durcir l'environnement de build et les runners
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
FAQ : questions fréquentes
Section intitulée « FAQ : questions fréquentes »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.