Vous signez vos images, vous scannez vos dépendances, vous générez des
attestations. Mais qui peut modifier votre code source ? Si n'importe quel
compte peut pousser sur main ou déplacer un tag de version, toute la
sécurité en aval se contourne par l'amont. Cette leçon s'adresse aux
développeurs et aux responsables de dépôt qui veulent qu'aucune modification
n'atteigne la branche principale sans validation explicite. Elle couvre
les rulesets GitHub et les branches protégées GitLab, la protection des
tags, le fichier CODEOWNERS et la documentation des règles de contribution.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Comprendre pourquoi un dépôt sans règles est le premier risque d'une chaîne CI/CD, à partir de l'attaque tj-actions
- Protéger la branche principale avec un ruleset GitHub ou une branche protégée GitLab, puis le vérifier
- Verrouiller les tags de version, que la protection de branche ne couvre pas
- Désigner les relecteurs obligatoires des zones sensibles avec
CODEOWNERS - Situer votre configuration dans SLSA Source et dans le score Scorecard
- Ancrage socle : cette leçon produit la preuve de SOCLE-SRC-GEN-1 et des exigences de protection des branches et de la revue
Pourquoi gouverner le code source ?
Section intitulée « Pourquoi gouverner le code source ? »Gouverner le code source, c'est décider qui peut changer quoi, et sous quelle validation. Sans cette décision, le reste de la chaîne protège un code dont personne ne garantit l'origine. L'OWASP Top 10 CI/CD place ce risque en tête, sous le nom CICD-SEC-1, « mécanismes de contrôle de flux insuffisants » : un attaquant qui obtient un droit d'écriture pousse son code jusqu'en production sans croiser aucun relecteur.
Ce que permet un dépôt sans règles
Section intitulée « Ce que permet un dépôt sans règles »Sur GitHub comme sur GitLab, un dépôt fraîchement créé n'impose presque rien.
Tout collaborateur disposant du droit d'écriture peut pousser directement
sur main, fusionner une pull request sans approbation, modifier un
workflow pour lire les secrets de la CI, ou déplacer un tag de version vers
un autre commit. Et en cas d'incident, rien ne permet de reconstituer qui a
validé quoi.
Un compte compromis ou un contributeur malveillant peut alors injecter une porte dérobée dans le code, modifier un workflow pour voler des jetons, altérer un Dockerfile pour piéger l'image, ou remplacer une dépendance par un paquet malveillant. Chacune de ces modifications part en production par le chemin normal, puisque rien ne l'arrête.
Ce que montre l'attaque tj-actions
Section intitulée « Ce que montre l'attaque tj-actions »En mars 2025, l'action GitHub tj-actions/changed-files, utilisée par plus de
23 000 dépôts, a été compromise (CVE-2025-30066). L'attaquant disposait
d'un jeton d'accès personnel volé à un bot qui avait les droits d'écriture
sur le dépôt. Il n'a touché à aucune branche : il a réécrit les tags de
version pour les faire pointer vers un commit malveillant, qui imprimait les
secrets des pipelines dans leurs journaux de build.
La leçon est double. Côté éditeur, une protection de branche ne protège que les branches : les tags demandent leur propre règle, et un jeton de bot à large portée est une clé de toute la maison. Côté consommateur, seul l'épinglage par SHA de commit protégeait les projets, ce que traite la leçon Épingler les actions par SHA. Le risque de jetons trop larges est développé dans OWASP Top 10 CI/CD.
Quels mécanismes combiner ?
Section intitulée « Quels mécanismes combiner ? »La gouvernance du code source repose sur quatre mécanismes complémentaires. Le tableau dit ce que chacun apporte et ce qui se passe quand il manque. Le premier est le fondement : sans lui, les autres restent des suggestions que n'importe qui peut ignorer.
| Mécanisme | Rôle | Sans lui |
|---|---|---|
| Protection de branche (ruleset) | Interdit le push direct, exige une pull request validée et une CI verte | N'importe quel compte modifie main |
| Protection des tags | Interdit de déplacer ou de supprimer un tag de version | Une version publiée change de contenu sans bruit |
| CODEOWNERS | Désigne qui doit approuver chaque zone du code | N'importe quel collaborateur approuve |
| CONTRIBUTING.md | Documente les règles pour les contributeurs | Règles floues, erreurs de bonne foi |
Étape 1 : protéger la branche principale
Section intitulée « Étape 1 : protéger la branche principale »Une protection de branche est le gardien automatique du dépôt : elle refuse
toute modification directe de main et impose le passage par une pull
request validée. Sur GitHub, la forme actuelle est le ruleset, qui
remplace les règles classiques et se lit sans droit d'administration, ce qui
compte pour les outils d'audit comme Scorecard.
Ce que contrôle un ruleset de branche
Section intitulée « Ce que contrôle un ruleset de branche »Un ruleset n'est pas une case unique mais un ensemble de verrous activés séparément. Le tableau associe chaque règle à ce qu'elle empêche. Trois méritent votre attention avant les autres : la pull request obligatoire, l'approbation du dernier push, qui empêche d'ajouter un commit après la revue, et une liste de contournement vide, sans laquelle un administrateur passe outre toutes les règles.
| Règle (interface GitHub) | Ce qu'elle empêche |
|---|---|
| Require a pull request before merging | Push direct sur la branche protégée |
| Required approvals | Fusion sans validation humaine |
| Dismiss stale pull request approvals when new commits are pushed | Approbation conservée après un changement du code |
| Require approval of the most recent reviewable push | Commit ajouté après la revue, sans nouvel accord |
| Require review from Code Owners | Fusion sans l'accord des responsables désignés |
| Require status checks to pass | Fusion avec une CI en échec |
| Block force pushes | Réécriture de l'historique de la branche |
| Restrict deletions | Suppression de la branche protégée |
Configurer un ruleset sur GitHub
Section intitulée « Configurer un ruleset sur GitHub »La configuration se fait dans les paramètres du dépôt, ou de l'organisation pour l'appliquer à plusieurs dépôts d'un coup. Les intitulés ci-dessous sont ceux de l'interface anglaise de GitHub, la seule disponible.
-
Créer le ruleset
Dans le dépôt, ouvrez Settings, puis Rules, Rulesets, et choisissez New ruleset, puis New branch ruleset.
-
Nommer et cibler la règle
- Ruleset name :
Protéger main - Enforcement status : Active
- Target branches : Add target, puis Include default branch
Ajoutez si besoin d'autres cibles par motif, comme
release/*. - Ruleset name :
-
Activer les règles essentielles
- Restrict deletions et Block force pushes
- Require a pull request before merging, avec au moins 1 approbation (2 pour un dépôt sensible), Dismiss stale pull request approvals, Require review from Code Owners et Require approval of the most recent reviewable push
- Require status checks to pass, avec vos jobs de CI (tests, lint, build)
-
Vider la liste de contournement
En haut de la page, la Bypass list doit rester vide. Un rôle qui y figure, administrateurs compris, fusionne sans respecter les règles, ce qui annule l'intérêt du ruleset.
-
Enregistrer
Cliquez Create. Le ruleset s'applique immédiatement.
Configurer une branche protégée sur GitLab
Section intitulée « Configurer une branche protégée sur GitLab »GitLab n'a pas de ruleset : la protection se règle par branche, et les approbations se règlent à part. Les règles d'approbation et l'approbation par les code owners demandent l'édition Premium ou Ultimate ; sur l'édition gratuite, seule la restriction de push et de fusion est disponible.
-
Protéger
mainDans Settings, Repository, Protected branches : choisissez
main, réglez Allowed to merge sur Maintainers, Allowed to push and merge sur No one, et laissez Allowed to force push désactivé. -
Exiger l'approbation des code owners
Sur la même ligne, activez Code owner approval, si vous avez un fichier
CODEOWNERS. -
Bloquer les fusions risquées
Dans Settings, Merge requests, cochez Pipelines must succeed et All threads must be resolved.
-
Fixer le nombre d'approbations
Dans la section Merge request approvals, créez une règle avec au moins 1 approbation requise, et retirez à l'auteur le droit d'approuver sa propre merge request.
Vérifier que la protection fonctionne
Section intitulée « Vérifier que la protection fonctionne »Une protection non testée est une hypothèse. Le test le plus simple consiste à
tenter un push direct sur main depuis un clone à jour : il doit être
refusé par le serveur, et non accepté puis signalé.
git switch mainecho "# Test de protection" >> README.mdgit commit -am "test: un push direct doit échouer"git push origin mainAvec un ruleset, GitHub répond par une erreur GH013 qui liste la règle violée ; la protection classique répond par l'erreur GH006.
remote: error: GH013: Repository rule violations found for refs/heads/main.remote: - Changes must be made through a pull request.Si le push passe, relisez la cible du ruleset et sa liste de
contournement. Annulez ensuite le commit local de test avec
git reset --hard origin/main.
Étape 2 : protéger aussi les tags
Section intitulée « Étape 2 : protéger aussi les tags »La protection de branche ne couvre pas les tags, et c'est exactement par
là qu'est passée l'attaque tj-actions. Un tag de version comme v1.4.0 est un
nom que les consommateurs croient figé : s'il peut être déplacé, une
version publiée peut changer de contenu sans que personne ne le voie.
Sur GitHub, un ruleset de tags se crée au même endroit que celui des
branches, avec New tag ruleset. Ciblez tous les tags de version, par
exemple le motif v*, et activez Restrict updates et Restrict
deletions : un tag publié ne peut alors plus être ni déplacé ni supprimé,
sauf par un rôle inscrit dans la liste de contournement, qui doit rester vide.
GitHub propose en complément des releases immuables : une fois l'option
activée dans les paramètres du dépôt, une release publiée, son tag et ses
fichiers ne peuvent plus être modifiés. C'est ce réglage qui a protégé la
version v0.35.0 de trivy-action lors de la compromission de mars 2026,
détaillée dans la leçon Tests de sécurité.
Sur GitLab, l'équivalent est le menu Settings, Repository, Protected
tags, où Allowed to create se règle sur Maintainers.
Étape 3 : désigner les responsables avec CODEOWNERS
Section intitulée « Étape 3 : désigner les responsables avec CODEOWNERS »Une protection qui exige « 1 approbation » ne dit pas par qui. Par défaut,
n'importe quel collaborateur doté du droit d'écriture approuve, et un attaquant
qui contrôle deux comptes, ou un complice, s'auto-valide. Le fichier
CODEOWNERS règle ce point : il désigne les relecteurs obligatoires de
chaque zone du code, et la forge les ajoute automatiquement à chaque pull
request qui la touche.
Comment fonctionne CODEOWNERS
Section intitulée « Comment fonctionne CODEOWNERS »À l'ouverture d'une pull request, la forge regarde les fichiers modifiés,
cherche dans CODEOWNERS la dernière règle qui correspond à chacun, et ajoute
les responsables trouvés comme relecteurs. Leur approbation ne devient
obligatoire que si la règle « Require review from Code Owners » est active
dans la protection de branche : sans elle, le fichier n'est qu'une suggestion.
Créer le fichier CODEOWNERS
Section intitulée « Créer le fichier CODEOWNERS »Sur GitHub, le fichier se place dans .github/CODEOWNERS, à la racine ou
dans docs/ ; sur GitLab, à la racine, dans docs/ ou dans .gitlab/.
L'exemple ci-dessous commence par un propriétaire par défaut, puis protège
les zones qui donnent accès aux secrets, à l'image ou aux dépendances.
# La DERNIÈRE règle qui correspond à un fichier l'emporte :# règle générale d'abord, règles spécifiques ensuite.
# Propriétaire par défaut de tout ce qui suit* @acme/maintainers
# Workflows CI/CD : ils lisent les secrets du dépôt.github/workflows/ @acme/securite.github/actions/ @acme/securite.gitlab-ci.yml @acme/securite
# Construction et déploiement : un Dockerfile piégé piège l'imageDockerfile @acme/opsdocker-compose*.yml @acme/opshelm/ @acme/opsterraform/ @acme/ops
# Dépendances : premier vecteur d'attaque de la chaînepackage.json @acme/securitepackage-lock.json @acme/securiterequirements*.txt @acme/securitego.mod @acme/securitego.sum @acme/securite
# Le fichier CODEOWNERS lui-même.github/CODEOWNERS @acme/securiteLa dernière ligne compte plus qu'elle n'en a l'air : sans elle, un
contributeur pourrait retirer les responsables d'une zone en modifiant
CODEOWNERS dans la même pull request que son changement, et ne demander
l'avis de personne.
Lire la syntaxe CODEOWNERS
Section intitulée « Lire la syntaxe CODEOWNERS »Les motifs suivent la syntaxe de .gitignore, avec une différence de
taille : l'ordre compte, puisque la dernière règle qui correspond
l'emporte. Le tableau résume les formes utiles et leur portée.
| Motif | Signification | Exemple |
|---|---|---|
* | Tous les fichiers (règle par défaut) | * @acme/maintainers |
*.py | Tous les fichiers de cette extension | *.py @acme/python |
/src/ | Le dossier src/ à la racine uniquement | /src/ @acme/core |
tests/ | Tout dossier tests/ où qu'il soit | tests/ @acme/qa |
@alice | Un compte utilisateur | docs/ @alice |
@acme/securite | Une équipe de l'organisation | go.mod @acme/securite |
Vérifier que CODEOWNERS fonctionne
Section intitulée « Vérifier que CODEOWNERS fonctionne »GitHub signale les erreurs de syntaxe directement dans l'affichage du fichier
CODEOWNERS, mais le seul vrai test est une pull request qui touche une
zone protégée. Les relecteurs désignés doivent y apparaître automatiquement,
et la fusion doit rester bloquée tant qu'ils n'ont pas approuvé.
git switch -c test/codeownersecho "# test" >> .github/workflows/ci.ymlgit commit -am "test: vérifier CODEOWNERS"git push origin test/codeownersOuvrez la pull request vers main : l'équipe @acme/securite doit figurer
parmi les relecteurs requis, et le bouton de fusion afficher « Review
required from code owners » tant que l'approbation manque.
Étape 4 : documenter les règles avec CONTRIBUTING.md
Section intitulée « Étape 4 : documenter les règles avec CONTRIBUTING.md »CODEOWNERS dit qui approuve, CONTRIBUTING.md explique comment
contribuer. Ce document évite les erreurs de bonne foi (branche mal nommée,
pull request sans description), épargne aux relecteurs de répéter les mêmes
règles, et sert de preuve d'audit que le processus était écrit avant
l'incident. GitHub l'affiche automatiquement s'il se trouve à la racine, dans
.github/ ou dans docs/.
Le modèle ci-dessous tient en une page, ce qui est voulu : un document de contribution trop long n'est pas lu. Il nomme les zones sensibles et leurs responsables, liste ce qui est interdit, et dit où signaler une faille.
# Contribuer
Toute modification passe par une pull request vers `main`.
## Processus
1. Créez une branche depuis `main` : `feature/…`, `fix/…`, `security/…`.2. Ouvrez une pull request qui explique le pourquoi, pas seulement le quoi, et lie l'issue concernée (`Fixes #123`).3. Attendez une CI verte et au moins une approbation. Les zones sensibles demandent l'approbation de leurs responsables (fichier CODEOWNERS).
## Zones sensibles
| Zone | Risque | Responsables ||------|--------|--------------|| `.github/workflows/` | Accès aux secrets CI/CD | @acme/securite || `Dockerfile`, `helm/` | Image ou déploiement piégé | @acme/ops || fichiers de dépendances | Attaque de la chaîne d'approvisionnement | @acme/securite |
## Interdit
- Pousser directement sur `main` ou forcer un push sur une branche protégée.- Fusionner sans l'approbation des responsables requis ou avec une CI rouge.- Désactiver un contrôle de CI pour faire passer une fusion.
## Signaler une vulnérabilité
N'ouvrez pas d'issue publique : utilisez l'onglet Security du dépôt(« Report a vulnerability ») ou écrivez à securite@acme.example.Réception accusée sous 48 heures.Quel niveau de protection viser ?
Section intitulée « Quel niveau de protection viser ? »Trois paliers se distinguent, du minimum que tout dépôt devrait appliquer à la configuration des dépôts sensibles. Chacun ferme une attaque de plus, et chacun correspond à des exigences du référentiel SOCLE, citées entre parenthèses pour servir de preuve.
Le minimum, pour tout dépôt
Section intitulée « Le minimum, pour tout dépôt »Ce premier palier ferme le trou le plus large, le push direct sur main,
et impose une revue et une CI verte avant toute fusion. Il ne demande
aucune organisation d'équipe : quelques réglages du dépôt suffisent, et un
projet personnel peut l'appliquer dès le premier jour.
- Pull request obligatoire et au moins une approbation (SOCLE-SRC-GEN-1).
- Checks de statut obligatoires (SOCLE-SRC-BRA-1).
- Force push et suppression interdits sur
main(SOCLE-SRC-SIG-1).
Le palier standard, pour une équipe
Section intitulée « Le palier standard, pour une équipe »C'est le palier visé par la plupart des équipes. Il ajoute la brique qui empêche l'auto-approbation et verrouille le contournement par les administrateurs : à ce niveau, un compte compromis seul ne suffit plus à faire passer du code malveillant.
CODEOWNERSactif avec « Require review from Code Owners » (SOCLE-SRC-BRA-2).- Approbations rejetées à chaque nouveau push (SOCLE-SRC-BRA-3), approbation du dernier push exigée, liste de contournement vide.
- Tags de version protégés contre la mise à jour et la suppression.
Le palier renforcé, pour un dépôt sensible
Section intitulée « Le palier renforcé, pour un dépôt sensible »Ce dernier palier vise les projets dont la compromission coûterait cher. Il exige deux approbations de personnes différentes pour contrer la collusion, et des commits signés pour authentifier chaque auteur, avec Gitsign par exemple.
- Revue à deux personnes (SOCLE-SRC-BRA-4).
- Commits signés exigés par la protection de branche.
- Journal d'audit des changements de protection, consulté régulièrement.
Ces paliers se mesurent. Le check Branch-Protection de OpenSSF Scorecard note la protection par paliers séquentiels : l'approbation du dernier push manquante bloque à elle seule le palier 6/10, quels que soient les réglages plus stricts déjà actifs.
Quelles erreurs rendent la protection inutile ?
Section intitulée « Quelles erreurs rendent la protection inutile ? »La plupart de ces erreurs ont un point commun : une protection activée à moitié, qui donne un faux sentiment de sécurité. Le cas le plus fréquent est un fichier CODEOWNERS sans règle qui le rende obligatoire, donc ignorable. Lisez la colonne Conséquence avant la solution : elle dit ce qu'un attaquant peut faire tant que le réglage n'est pas corrigé.
| Erreur | Conséquence | Solution |
|---|---|---|
| CODEOWNERS sans « Require review from Code Owners » | Les responsables sont ajoutés mais pas obligatoires | Activer la règle dans le ruleset |
| Administrateurs dans la liste de contournement | Toutes les protections se contournent | Vider la Bypass list |
| Tags non protégés | Une version publiée change de contenu | Ruleset de tags avec Restrict updates et Restrict deletions |
Règle * placée en dernier | Elle écrase toutes les règles spécifiques | Règle par défaut en premier |
CODEOWNERS sans propriétaire pour lui-même | Un contributeur retire les responsables dans sa propre PR | Ajouter .github/CODEOWNERS à une équipe de sécurité |
| Équipe citée inexistante ou sans droit d'écriture | Ligne ignorée, revue non requise | Vérifier l'équipe et ses droits |
| Jeton de bot à droits d'écriture larges | Un vol de jeton suffit à tout modifier | Jetons à portée minimale, expirants, audités |
Où se situe cette configuration dans SLSA Source ?
Section intitulée « Où se situe cette configuration dans SLSA Source ? »SLSA définit, depuis sa version 1.2, une piste Source en quatre niveaux. Le tableau les résume avec leurs exigences ; la colonne de droite dit ce que la configuration de cette leçon apporte à chacun.
| Niveau | Exigence SLSA | Ce que cette leçon apporte |
|---|---|---|
| L1 Version contrôlée | Un système de gestion de versions | Acquis avec Git |
| L2 Historique et provenance | Historique continu, immuable et conservé ; attestations de provenance émises par la forge | Historique protégé (force push et suppression interdits), mais pas d'attestation de provenance |
| L3 Contrôles techniques continus | La forge applique les contrôles de l'organisation sur les références nommées | Rulesets de branches et de tags, checks obligatoires |
| L4 Revue à deux personnes | Deux personnes de confiance relisent tout changement des branches protégées | Palier renforcé, avec deux approbations |
La nuance compte : la configuration décrite ici est alignée sur les contrôles des niveaux 3 et 4, mais elle ne produit pas les attestations de provenance de source qu'exige déjà le niveau 2. Elle ne suffit donc pas, à elle seule, à revendiquer un niveau SLSA Source.
À retenir
Section intitulée « À retenir »- La protection de branche est le fondement : sans elle,
CODEOWNERSetCONTRIBUTING.mdne sont que des suggestions. - Sur GitHub, préférez le ruleset à la protection classique, et gardez sa liste de contournement vide.
- L'approbation du dernier push empêche d'ajouter un commit après la revue ; sans elle, une revue ne vaut que pour ce qui a été relu.
- Les tags de version demandent leur propre ruleset : l'attaque tj-actions les a réécrits sans toucher à une seule branche.
CODEOWNERSdésigne les relecteurs obligatoires des zones sensibles, et doit se protéger lui-même.- Cette configuration s'aligne sur les contrôles de SLSA Source L3 et L4, sans produire les attestations de provenance exigées dès L2.
- Preuve attendue par SOCLE-SRC-GEN-1 : la configuration de protection de branche, revue obligatoire et push direct interdit. Une capture ou un export du ruleset la constitue.
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 : Living Off The Pipeline : quand les outils deviennent des armes
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications