Ce que vous avez le droit d'attaquer — et vos deux cibles
Jour 1 · Poser le décor, le droit et le dépôt · 25 min
Ceci n'est pas une précaution de style, c'est une règle de droit. En France, accéder ou se maintenir dans un système de traitement automatisé de données sans autorisation est un délit (articles 323-1 et suivants du Code pénal), puni même en cas de simple intrusion sans dommage. Les outils que vous allez apprendre — sqlmap, ffuf, nuclei, ZAP — sont des outils d'attaque réels. Employés sur un système qui ne vous appartient pas et pour lequel vous n'avez pas d'autorisation écrite, ils constituent une infraction.
Pendant tout ce parcours, vous n'attaquez QUE des applications délibérément vulnérables que vous déployez vous-même, sur votre propre poste : devsecops-lab, que vous écrivez aujourd'hui, et Juice Shop, que vous lancez en conteneur local. Elles tournent sur localhost, isolées, et sur elles seules.
Trois interdits absolus, sans exception :
- jamais sur Internet — pas de scan d'un site public, même « pour voir », même s'il « a l'air vulnérable » ;
- jamais sur une ressource de Caplogy — pas de serveur, pas d'application, pas de réseau de l'entreprise, même en tant que salarié ;
- jamais sur le système d'un tiers sans mandat écrit — un test d'intrusion réel repose toujours sur une autorisation formelle et un périmètre signé.
Un pentester professionnel travaille avec un contrat, un périmètre et une fenêtre de temps. Ici, votre « contrat » est ce paragraphe : votre périmètre est votre poste, votre fenêtre est la formation. Toute manipulation du parcours respecte ce cadre — si une page semble vous demander de viser autre chose que vos deux cibles locales, c'est la page qui a tort.
Pourquoi c'est la première leçon et pas une annexe
Un ingénieur qui apprend sqlmap apprend en même temps une capacité de nuisance. Ce qui sépare le professionnel du délinquant n'est ni la compétence ni l'intention : c'est l'autorisation écrite. C'est la seule différence, et elle est intégralement documentaire.
Deux conséquences pratiques, que vous retrouverez dans votre métier :
- Un test d'intrusion commence toujours par un document — périmètre, adresses IP, fenêtre horaire, contacts d'escalade, ce qui est explicitement hors champ. Sans lui, on ne tape pas la première commande.
- Un accès légitime n'est pas un mandat de test. Vous êtes salarié de Caplogy : vous avez le droit d'utiliser ses applications, pas de les attaquer. Ce sont deux autorisations distinctes, et la seconde ne se déduit jamais de la première.
Vos deux cibles
Vous travaillerez sur deux applications, et il ne faut jamais les confondre. devsecops-lab est la vôtre : vous l'écrivez aujourd'hui, elle contient dix failles délibérées, et tout le fil rouge consiste à les détecter automatiquement puis à les corriger — c'est votre dépôt, votre pipeline, votre responsabilité. Juice Shop est une cible d'entraînement que vous attaquez sans en posséder le code : vous la lancez sur votre poste, vous la cartographiez, vous l'exploitez, mais vous ne corrigez jamais son code source. La première vous apprend à construire sûr ; la seconde à casser. Les deux tournent sur votre machine, et sur elle seule.
| <code>devsecops-lab</code> | Juice Shop | |
|---|---|---|
| Nature | Application Flask d'environ 130 lignes, que vous écrivez | Application Node.js complète, image officielle |
| Vous en avez le code | Oui — c'est votre dépôt Git | Non — boîte noire, vous ne le lisez pas |
| Version | devsecops-lab:dev puis :hardened | bkimminich/juice-shop:v17.3.0 |
| Adresse | http://localhost:5000 | http://localhost:3000 |
| Ce que vous y faites | casser, détecter, corriger, durcir, déployer | reconnaître, exploiter, rapporter — jamais corriger |
| Modules | M1 → M4 et le projet fil rouge | M2 (DAST), M5 (pentest), projet fil rouge |
La trace que vous laissez dès ce soir
Votre dépôt contiendra un fichier journal-incidents.md. Sa toute première ligne
n'est pas un incident : c'est votre périmètre. Vous l'écrirez au TP 4 de cet après-midi, et elle
ne bougera plus de quinze jours.
Perimetre autorise : localhost uniquement — devsecops-lab (5000) et juice-shop (3000).Ce n'est pas une formalité. Le jour où vous rédigerez un vrai rapport de pentest, au module 5, la section « périmètre et méthodologie » sera la deuxième page du document — juste après le résumé exécutif, et avant le premier finding. Vous commencez à l'écrire aujourd'hui.
La question qui vient toujours, et à laquelle la réponse est non : « je peux quand même lancer un scan passif sur un site public, je ne fais que regarder ? ». Non. Un scan est une série de requêtes que vous envoyez à un système qui ne vous a rien demandé. Le droit français ne distingue pas le regard de la main.
Deuxième piège, plus insidieux : Caplogy est votre employeur, vous y avez des accès légitimes. Un accès légitime pour travailler n'est pas une autorisation de test d'intrusion. Ce sont deux mandats différents, et seul le second s'écrit et se signe.
Intention — poser en tête de parcours la seule règle qui ne se négocie pas, et installer immédiatement la distinction des deux cibles. Ces deux points sont ensemble parce qu'ils disent la même chose : le périmètre. Placée avant toute notion technique, pour qu'aucune commande du parcours ne soit apprise hors de son cadre.
Ce qui coince
- Le cadre légal lu comme une clause de style, survolée en trente secondes. Signe : il enchaîne sur la leçon suivante sans avoir posé une seule question. Faire reformuler les trois interdits à voix haute au point du soir.
- Les deux cibles confondues dès le jour 2 : « je corrige aussi Juice Shop ? ». La confusion coûte cher en M5, où il perdrait des heures à chercher du code qu'il n'a pas.
- Ingénieur, il connaît peut-être déjà des outils offensifs et les a peut-être déjà employés hors cadre. Ne pas moraliser : dire le risque pénal, et le fait qu'une condamnation ferme la profession.
Questions à anticiper
- « Et les plateformes de bug bounty ? » — oui, parce qu'elles publient un périmètre et une autorisation. C'est exactement le contre-exemple utile : ce qui rend l'acte licite est l'écrit, pas l'intention.
- « Si je trouve une faille par hasard sur un site ? » — on la signale, on ne la confirme pas en l'exploitant. Le distinguo est celui qui envoie des gens au tribunal.
- « Juice Shop est open source, je peux lire son code. » — il peut, mais le parcours l'interdit méthodologiquement : la valeur pédagogique du M5 tient à la boîte noire.
Ce qu'il faut exiger — que la première ligne de journal-incidents.md soit le périmètre autorisé, écrite ce soir. C'est le critère du point de contrôle du jour 1, et c'est la trace qui prouve que le cadre a été lu.
DevSecOps : pourquoi déplacer la sécurité vers la gauche
Jour 1 · Poser le décor, le droit et le dépôt · 30 min
Pendant vingt ans, la sécurité applicative a fonctionné comme un péage : on construisait, puis un auditeur passait, trois semaines avant la mise en production, et rendait un rapport de soixante pages. Le rapport arrivait au moment où plus personne ne pouvait rien changer sans décaler la date. On corrigeait les deux failles les plus voyantes, on notait le reste dans un tableau, et le tableau vivait sa vie.
Le DevSecOps est la réponse à ce constat, et elle tient en une phrase : le contrôle doit s'exécuter au moment où corriger ne coûte encore presque rien, et il doit avoir le pouvoir d'arrêter la chaîne.
Pourquoi le moment change tout
Une injection SQL trouvée dans votre éditeur, c'est une ligne à réécrire : vous avez encore le contexte en tête, personne d'autre n'a vu le code, aucune donnée n'existe. La même injection trouvée en production, c'est un correctif à écrire en urgence, une fenêtre de déploiement à négocier, des journaux à analyser pour savoir si elle a été exploitée, et peut-être une notification à la CNIL sous 72 heures.
Les chiffres qui circulent — un facteur 10, 30, 100 selon la phase — sont des ordres de grandeur, pas des lois. Ce qui compte est le mécanisme, et il est toujours le même :
- le contexte se perd — dans six mois, plus personne ne saura pourquoi cette requête est construite ainsi ;
- le correctif devient un projet — il faut le planifier, le tester, le déployer, coordonner ;
- l'exposition est déjà réelle — les données sont en base, les clients sont connectés, l'incident a peut-être déjà eu lieu sans qu'on le sache.
Les trois boucles
« Shift left » ne veut pas dire « tout au plus tôt » : cela veut dire que chaque type de contrôle vit à l'endroit où il est le plus utile. Trois boucles se superposent, avec trois latences très différentes.
| Boucle | Où | Latence | Ce qu'elle attrape | Ce qu'elle ne peut pas attraper |
|---|---|---|---|---|
| Locale | éditeur, pre-commit | quelques secondes | secrets, motifs de code dangereux, gros fichiers | tout ce qui dépend de l'application en marche |
| Intégration | pipeline CI sur la pull request | quelques minutes | SAST, secrets sur tout le dépôt, dépendances à CVE, image | les failles de logique métier, la configuration réelle |
| Exécution | recette, préproduction, production | continu | DAST, en-têtes, comportement réel, dérive de configuration | ce qui n'est jamais atteint par le trafic de test |
Le module 1 construit les deux premières boucles : la locale au jour 2, l'intégration au jour 3. Le module 2 ajoutera la troisième avec ZAP, et le module 3 la portera sur l'image et le cluster.
Le verrou : un contrôle qui n'échoue pas n'est pas un contrôle
Un outil de sécurité produit deux choses, qu'il ne faut jamais confondre :
- un artefact — un rapport, un SARIF, un SBOM. Il informe, il s'archive, il se compare d'une semaine sur l'autre. Il ne bloque rien.
- un verrou (gate) — un code de sortie non nul qui fait échouer le job, donc échouer la vérification obligatoire, donc interdire la fusion.
Semgrep sans --error produit un artefact. Semgrep avec --error produit un verrou. C'est le même outil, la même sortie, la même seconde de calcul — et deux dispositifs de sécurité qui n'ont rien à voir.
« Security as code » : la règle vit dans le dépôt
La deuxième idée du DevSecOps est moins spectaculaire mais tout aussi structurante : les règles de sécurité cessent d'être une procédure sur un intranet pour devenir des fichiers versionnés dans le dépôt du projet.
Vous en écrirez cinq pendant ce module :
.gitignore, .pre-commit-config.yaml, .gitleaks.toml,
.github/workflows/ci.yml, et le journal-incidents.md qui trace les
exceptions. Ce qu'ils gagnent à vivre là :
- ils se relisent en revue — un durcissement de règle passe par une pull request, comme le reste ;
- ils se testent — on sait si la règle attrape ce qu'elle prétend attraper, parce qu'on lui donne un cas ;
- leur historique répond à la question qui compte — qui a désactivé cette règle, quand, et avec quelle justification.
C'est exactement ce que ne permet pas une procédure PDF : on ne peut pas répondre à la troisième question.
Ce que le DevSecOps n'est pas
- Ce n'est pas empiler des scanners. Trois outils qui trouvent la même chose et n'arrêtent rien valent moins qu'un seul qui fait échouer le build.
- Ce n'est pas transférer la sécurité aux développeurs. Le développeur corrige parce qu'il est le seul à pouvoir le faire vite ; l'équipe sécurité outille, calibre les règles et arbitre les exceptions. La responsabilité se partage, elle ne se déplace pas.
- Ce n'est pas le zéro faille. C'est un dispositif où l'on sait ce qu'on laisse passer, pourquoi, et jusqu'à quand. Vous en ferez l'expérience au jour 3 : votre pipeline passera au vert avec quatre failles encore ouvertes — datées, justifiées, assignées au module 2.
Intention — faire admettre que le DevSecOps n'est pas « ajouter des outils de sécurité » mais déplacer le moment du contrôle et rendre le contrôle bloquant. Les deux idées sont indissociables : un contrôle précoce non bloquant ne change rien, un contrôle bloquant tardif bloque tout. Toute la journée 3 est la démonstration de cette leçon.
Ce qui coince
- Ingénieur, il connaît l'intégration continue et croit connaître le sujet. Signe : il résume par « on met des scanners dans la CI ». Il manque alors le verrou — le code de sortie — et la notion d'exception tracée. Reprendre par la question : « que se passe-t-il quand le scanner trouve quelque chose ? ».
- La courbe de coût prise pour une loi chiffrée. Elle est un ordre de grandeur, pas une mesure ; ce qui compte est le mécanisme (contexte perdu, correctif à replanifier, données déjà exposées), pas le facteur.
- Le mot « gate » traduit par « rapport ». Signe, dès le jour 3 : il trouve normal qu'un pipeline vert coexiste avec dix findings.
Questions à anticiper
- « Si tout bloque, on ne livre plus rien. » — objection juste, et c'est le sujet de l'exception datée du jour 3. Répondre par la ligne de base : on bloque sur les nouveaux écarts, on planifie les anciens.
- « Le SAST ne trouve-t-il pas surtout des faux positifs ? » — oui en partie, et c'est pourquoi le M2 écrit des règles maison. Ne pas balayer : c'est la vraie objection des équipes.
- « Qui est responsable, alors — la sécurité ou le développeur ? » — le développeur corrige, la sécurité outille et arbitre. Le dire net, c'est la question de fond du modèle.
Ce qu'il faut exiger — qu'il énonce les trois boucles (locale, CI, production) avec leur latence, et qu'il distingue un artefact d'un verrou. Un échec ici ne se voit qu'au jour 3, quand il déclarera le pipeline « terminé » alors qu'il ne fait échouer aucun job.
Quiz — Cadre légal, shift-left et security as code
Jour 1 · Poser le décor, le droit et le dépôt · 10 min
1. Un site public affiche une bannière de version manifestement obsolète. Vous lancez nuclei dessus pour confirmer, sans rien exploiter. Où en êtes-vous ?
2. Laquelle de ces phrases décrit correctement le partage entre vos deux cibles ?
3. Qu'est-ce qui distingue un contrôle « décalé à gauche » d'un audit de fin de projet ?
4. Votre pipeline exécute Semgrep, publie un rapport SARIF très lisible, et se termine toujours en succès. Que lui manque-t-il ?
5. Qu'apporte le fait d'écrire une règle de sécurité dans un fichier versionné du dépôt plutôt que dans une procédure interne ?
Intention — vérifier séparément quatre acquis avant de laisser partir en TP : le périmètre légal (q1), le partage des deux cibles (q2), la nature du décalage à gauche (q3) et le verrou (q4, q5). Le score global ne dit rien ; c'est la ventilation qui compte.
Ce qui coince
- q1 ratée : à reprendre immédiatement et à distance de tout, ce n'est pas un point de cours. Faire relire l'encadré et faire reformuler les trois interdits.
- q2 ratée : la confusion des cibles est le défaut le plus coûteux du parcours, il se paie en M5. Reprendre le tableau à deux colonnes.
- q4 ratée est le signal le plus utile de ce quiz : il a lu « pipeline » et compris « rapport ». Le jour 3 n'aura aucun sens sinon.
Questions à anticiper
- « La 3, ce n'est pas aussi une question d'automatisation ? » — l'automatisation est le moyen, pas la définition ; un audit final entièrement automatisé reste un audit final. Le distinguo mérite deux minutes.
- « La 5 : une procédure écrite n'est-elle pas déjà datée ? » — datée oui, mais elle ne dit pas qui a changé quoi ni ne se relit en revue.
Ce qu'un échec révèle — 3/5 avec q1 et q4 justes : laissez partir. 4/5 avec q1 fausse : ne laissez pas partir, reprenez le cadre légal.
TP 1 — Le réseau dso-net et l'outillage du parcours
Jour 1 · Poser le décor, le droit et le dépôt · 40 min
Poser le décor Docker de quinze jours et tirer les images épinglées, une fois pour toutes.
Docker en version 24 ou supérieure, Git 2.40 ou supérieur, Python 3.12 — installés au module d'accueil. Aucun matériel, aucun compte pour l'instant : le compte GitHub attendra demain.
Le dossier que vous créez maintenant est celui de tout le parcours, jusqu'à la soutenance du 21 août.
- Vérifiez votre outillage. Les trois commandes doivent répondre — si l'une échoue, réglez-la maintenant plutôt qu'au milieu d'un TP.
docker --version # attendu : Docker version 24.x ou superieur git --version # attendu : git version 2.40 ou superieur python3 --version # attendu : Python 3.12.x - Créez le réseau Docker du parcours. Tous les conteneurs des cinq modules le rejoindront, pour se joindre par leur nom plutôt que par une adresse IP qui change à chaque redémarrage. C'est le tout premier geste, et il ne se refait jamais.
docker network create dso-net docker network ls | grep dso-net - Créez le dossier de travail. Il contiendra votre dépôt et, à côté, les sorties d'outils que vous ne versionnerez pas.
mkdir -p ~/poei-s4 cd ~/poei-s4 pwd # notez ce chemin, toutes les commandes en partent - Tirez les trois images épinglées du module. Comptez plusieurs minutes : autant les avoir maintenant, pendant qu'aucun exercice n'attend. Les versions sont fixées pour tout le parcours — c'est ce qui rendra votre démonstration du 21 août identique à celle d'aujourd'hui.
docker pull zricethezav/gitleaks:v8.18.4 docker pull semgrep/semgrep:1.85.0 docker pull bkimminich/juice-shop:v17.3.0 - Vérifiez que les trois sont bien là, avec leur étiquette. Une image tirée sans étiquette explicite serait
latest— c'est-à-dire une version différente la semaine prochaine.docker images | grep -E 'gitleaks|semgrep|juice-shop' - Éprouvez Gitleaks à vide, pour confirmer qu'il s'exécute. Retenez la forme de cette commande : on monte un répertoire dans le conteneur, jamais un fichier. Un montage sur un fichier qui n'existe pas encore fait créer par Docker un répertoire vide du même nom, et l'outil échoue sans rien expliquer.
docker run --rm zricethezav/gitleaks:v8.18.4 version - Vous avez maintenant un réseau, un dossier et trois outils. Rien de tout cela ne sera recréé : les jours suivants s'y ajoutent, ils ne le refont pas.
- Épingler
zricethezav/gitleaks:v8.18.4plutôt quelatestvous protège d'un incident, et vous en expose à un autre. Lesquels ? - Le réseau
dso-netpermet à un conteneur d'en joindre un autre par son nom. Quel intérêt cela aura-t-il quand un scanner tournera lui aussi en conteneur ?
Cannot connect to the Docker daemon ne veut pas dire que votre commande est fausse : Docker Desktop n'est pas démarré. Sous macOS, lancez l'application et attendez que la baleine cesse de s'animer.
Le réseau dso-net se crée une seule fois pour les quinze jours. Si docker network create répond already exists, tout va bien : c'est qu'il est déjà là. Ne le supprimez jamais — les conteneurs qui y sont attachés perdraient la possibilité de se joindre par leur nom, et vous chercheriez la panne dans la mauvaise direction pendant une heure.
Intention — poser en un seul geste le réseau et les trois images épinglées du module, pendant que la bande passante est disponible et que rien ne dépend encore d'elles. Ce n'est pas un TP sur Docker : c'est la suppression d'une classe entière de pannes des jours suivants.
Ce qui coince
- Le
docker pullde Semgrep prend plusieurs minutes sur une connexion moyenne. S'il enchaîne sans attendre, il croira l'image absente au TP suivant. Lui faire vérifier pardocker images. - Docker installé par Homebrew sans Desktop, donc sans démon : la commande existe, rien ne tourne. Symptôme identique au Desktop éteint, cause différente.
- Le réseau créé sous un autre nom (
dsonet,dso_net). Panne différée au TP 2, où le conteneur refusera de démarrer.
Questions à anticiper
- « Pourquoi épingler la version des images ? » — pour que son résultat d'aujourd'hui soit rejouable dans trois semaines devant le jury. Réponse à donner maintenant : elle reviendra au jour 3 à propos des actions GitHub.
- « Pourquoi un réseau, puisque tout est sur localhost ? » — parce qu'au M2 ZAP devra joindre les cibles par leur nom, depuis un conteneur. Le dire, sans détailler.
Ce qu'il faut exiger — docker network ls qui montre dso-net, et docker images qui montre les trois images avec leur étiquette de version exacte. Pas « ça a téléchargé ».
TP 2 — Juice Shop : lancer la cible et la reconnaître
Jour 1 · Poser le décor, le droit et le dépôt · 50 min
Déployer la cible boîte noire du parcours et pratiquer une première reconnaissance strictement passive.
Tout ce qui suit vise localhost:3000, un conteneur qui tourne sur votre poste. Aucune des commandes de ce TP ne doit jamais être pointée vers une adresse que vous n'hébergez pas vous-même.
- Lancez Juice Shop sur le réseau du parcours, en publiant son port sur votre poste.
docker run -d --name juice-shop --network dso-net \ -p 3000:3000 bkimminich/juice-shop:v17.3.0 - Suivez le démarrage dans un second terminal et attendez la ligne qui annonce l'écoute. Ne faites rien d'autre tant qu'elle n'est pas apparue.
docker logs -f juice-shop # attendu, apres 20 a 40 secondes : # info: Server listening on port 3000 - Vérifiez depuis votre poste — et non depuis l'intérieur du conteneur — que le port publié répond.
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/ - Ouvrez
http://localhost:3000dans votre navigateur. Parcourez l'application cinq minutes en simple utilisateur : créez un compte, ajoutez un article au panier, cherchez un produit. Vous n'attaquez rien — vous prenez la mesure de la surface.open http://localhost:3000 # macOS # xdg-open http://localhost:3000 # Linux - Première reconnaissance passive : lisez les en-têtes de réponse. C'est le geste d'ouverture de tout test, et il ne coûte rien à la cible.
curl -s -D - -o /dev/null http://localhost:3000/ - Notez ce que vous voyez, et surtout ce que vous ne voyez pas. Cherchez la présence ou l'absence de
Content-Security-Policy,Strict-Transport-Security,X-Frame-Options,X-Content-Type-Options. Un en-tête absent ne se voit pas : il faut le chercher par son nom, c'est pour cela qu'on travaille avec une liste. - Consignez votre observation dans un fichier. Ce sera votre premier artefact de reconnaissance, et le module 5 le reprendra.
mkdir -p ~/poei-s4/artefacts curl -s -D - -o /dev/null http://localhost:3000/ \ > ~/poei-s4/artefacts/recon-juice-shop-2026-08-03.txt # Puis ouvrez ~/poei-s4/artefacts/recon-juice-shop.md et ecrivez, en trois lignes : # - la technologie que les en-tetes trahissent # - les en-tetes de securite presents # - ceux qui manquent - Repérez le tableau de bord des défis, à l'adresse
http://localhost:3000/#/score-board. Regardez-le, comptez les catégories, et refermez-le : vous n'en résolvez aucun aujourd'hui. Le module 5 y consacre trois jours avec une méthode ; les résoudre au hasard maintenant vous priverait de la matière et de la démonstration. - Arrêtez proprement le conteneur pour la nuit, et retenez la paire de commandes que vous emploierez pendant quinze jours.
docker rmn'en fait pas partie.docker stop juice-shop docker start juice-shop # demain matin, et les jours suivants
- Vous n'avez pas le code de Juice Shop. Qu'est-ce que cette contrainte vous oblige à faire, que la lecture du code vous aurait épargné ?
- Un en-tête de sécurité absent n'apparaît nulle part dans une réponse HTTP. Comment s'assure-t-on de ne pas en oublier un ?
- Recréer le conteneur remet les défis à zéro. Quelle conséquence cela a-t-il sur la façon dont vous devrez consigner vos preuves au module 5 ?
Juice Shop met vingt à quarante secondes à démarrer. Pendant ce temps, curl répond Connection reset et l'on croit que le lancement a échoué. Regardez docker logs -f juice-shop et attendez la ligne Server listening on port 3000 avant de conclure quoi que ce soit.
Deuxième réflexe à prendre dès aujourd'hui : docker stop et docker start, jamais docker rm. Juice Shop garde son état en mémoire ; recréer le conteneur remet tous les défis à zéro. Ce n'est pas grave aujourd'hui — ce le sera au module 5, en pleine session d'exploitation.
Intention — mettre la cible offensive en service dès le premier jour, et surtout installer la discipline du passif avant l'actif. Il n'attaque rien aujourd'hui : il observe, il note, il constitue une trace. C'est le geste que le M5 exigera de lui sous le nom de reconnaissance, et il vaut mieux qu'il en prenne l'habitude quand rien n'est en jeu.
Ce qui coince
- L'impatience au démarrage : il conclut à l'échec en quinze secondes et relance un
docker run, qui échoue sur le nom déjà pris. Enchaînement classique de cinq minutes perdues. - La tentation d'aller lire le code source sur GitHub. Le lui interdire explicitement, avec le motif : la valeur du M5 tient à la boîte noire, et il ne la retrouvera pas s'il l'a ouverte.
- Le tableau de bord des défis pris pour une liste de tâches. Ce n'est pas l'objet du M1 ; s'il commence à résoudre des défis aujourd'hui, il aura brûlé la matière du M5.
Questions à anticiper
- « Je peux commencer les défis tout de suite ? » — non, et le dire fermement : le M5 y consacre trois jours avec une méthode. Aujourd'hui, on observe.
- « Pourquoi noter les en-têtes maintenant ? » — parce que la comparaison avec ceux de
devsecops-lab, cet après-midi, est bien plus parlante que la lecture d'une liste d'en-têtes recommandés.
Ce qu'il faut exiger — le fichier artefacts/recon-juice-shop.md avec les en-têtes réellement observés, pas recopiés. La preuve : lui demander à l'oral quel en-tête de sécurité manque ; s'il récite une liste standard sans l'avoir confrontée à sa sortie curl, l'étape n'est pas faite.
Les dix failles de devsecops-lab : CWE, OWASP, et qui les trouvera
Jour 1 · Poser le décor, le droit et le dépôt · 25 min
Vous allez écrire, dans une heure, une application Flask d'environ cent trente lignes qui contient dix failles délibérées. Elles ne sont ni aléatoires ni décoratives : chacune est là pour être trouvée par un outil précis, à un moment précis du parcours, et refermée à un module précis.
La tentation sera forte, en tapant la requête SQL du /login, de la paramétrer au passage : vous savez le faire. Ne le faites pas. Un pipeline vert dès le premier jour ne démontre rien ; le jour 3 tient tout entier sur le fait que votre pipeline échoue d'abord.
Même consigne pour requirements.txt : ses versions sont choisies pour porter de vraies CVE, et c'est la matière de l'analyse de composition du module 2. Ne les montez pas.
La table des dix
| ID | Faille | Emplacement | CWE | OWASP 2021 | Détectée par | Refermée en |
|---|---|---|---|---|---|---|
VULN-01 | Injection SQL | /login | CWE-89 | A03 | Semgrep, puis sqlmap | M1 J3 |
VULN-02 | XSS réfléchi | /search | CWE-79 | A03 | Semgrep, ZAP | M2 |
VULN-03 | Injection de commande | /ping | CWE-78 | A03 | Semgrep, Bandit | M2 |
VULN-04 | IDOR | /profile | CWE-639 | A01 | pentest manuel | M5 |
VULN-05 | Secret en dur | app.py, .env | CWE-798 | A07 | Gitleaks | M1 J2 |
VULN-06 | Hachage faible (md5) | /hash | CWE-327 | A02 | Semgrep, Bandit | M2 |
VULN-07 | Désérialisation non sûre | /config | CWE-502 | A08 | Semgrep, Bandit | M2 |
VULN-08 | SSRF | /fetch | CWE-918 | A10 | Semgrep, ZAP | M5 |
VULN-09 | debug=True, en-têtes absents | __main__, Dockerfile | CWE-489 | A05 | Bandit, ZAP, Trivy | M3 |
VULN-10 | Dépendances à CVE | requirements.txt | CWE-1104 | A06 | Trivy (SCA) | M2 |
CWE et OWASP Top 10 : deux objets qu'on confond tout le temps
Les deux colonnes du milieu ne disent pas la même chose, et un rapport de pentest qui les mélange se fait reprendre.
| CWE | OWASP Top 10 | |
|---|---|---|
| Ce que c'est | Une taxonomie des types de faiblesses logicielles, maintenue par le MITRE | Un classement des dix catégories de risque les plus répandues, publié tous les quelques années |
| Taille | Plusieurs centaines d'entrées, très fines | Dix catégories larges, chacune regroupant de nombreuses CWE |
| Ce à quoi ça sert | Nommer précisément la faiblesse dans un finding | Communiquer une priorité, structurer un programme, parler à un décideur |
| Exemple | CWE-89 — Improper Neutralization of Special Elements used in an SQL Command | A03:2021 — Injection, qui contient CWE-89, CWE-79, CWE-78 et bien d'autres |
Concrètement : dans le rapport que vous rédigerez au module 5, chaque finding portera les deux. La CWE dit au développeur exactement quoi corriger ; la catégorie OWASP dit au responsable où se situe le risque dans le paysage.
Pourquoi les commentaires VULN-NN restent dans le code
Chaque faille porte en commentaire son identifiant. On pourrait croire qu'il faut les retirer pour « ne pas tricher ». C'est l'inverse :
- ils vous permettent de vérifier ce que l'outil a manqué — le silence d'un scanner sur une faille que vous savez présente est l'information la plus utile du parcours ;
- ils rendent le dépôt lisible par votre encadrant et par le jury, qui n'ont pas à jouer aux devinettes ;
- ils donnent un identifiant stable à chaque correction : votre branche s'appellera
fix/vuln-01-sqli, votre commitfix(VULN-01): requete SQL parametree, et votre journal parlera deVULN-01. Une même faille, un seul nom, du premier jour à la soutenance.
Dans la table ci-dessus, repérez la seule catégorie du Top 10 OWASP 2021 qui n'apparaisse dans aucune ligne. Vous en trouverez la raison au module 4 : toutes les familles de risque ne se démontrent pas dans cent trente lignes de Python — certaines sont des propriétés de l'organisation, pas du code.
Intention — donner la carte avant le territoire. Il va écrire une application délibérément trouée : il doit savoir pourquoi chaque trou est là, quel outil est censé le voir, et à quel module il se referme. Sans cette carte, le TP suivant se vit comme une recopie.
Ce qui coince
- CWE et OWASP Top 10 confondus. Signe : « la CWE-89, c'est A03 ». Ce sont deux objets différents — une taxonomie exhaustive et un classement décennal des dix risques les plus répandus. Le distinguo sert dès la rédaction du premier finding.
- L'envie de « corriger tout de suite » en écrivant l'application. Signe : il paramètre la requête SQL de lui-même. Le TP perd alors sa démonstration du jour 3, puisque le pipeline sera vert d'emblée. Le lui dire avant.
- Les commentaires
VULN-NNpris pour du bruit à nettoyer. Ils sont la carte que Semgrep viendra confirmer : ils restent.
Questions à anticiper
- « Une application aussi petite, est-ce représentatif ? » — non, et c'est assumé : elle est lisible d'un bout à l'autre en dix minutes, ce qu'aucune application réelle n'est. Juice Shop tient l'autre rôle.
- « Pourquoi des dépendances à CVE plutôt que les dernières versions ? » — pour que la SCA du M2 remonte de vraies advisories. Interdire formellement de « corriger »
requirements.txtavant le M2. - « Dix failles, c'est arbitraire ? » — c'est un choix de couverture : neuf familles OWASP sur dix sont représentées. Le lui faire vérifier dans le tableau.
Ce qu'il faut exiger — qu'il sache dire, pour trois VULN au hasard, quel outil les trouve et à quel module elles se ferment. C'est ce qui prouve qu'il a lu le tableau comme un plan de travail et non comme une liste.
TP 3 — Écrire devsecops-lab : l'application, l'image, le conteneur
Jour 1 · Poser le décor, le droit et le dépôt · 1 h 45
Poser le fil rouge des quinze jours : l'application vulnérable, son image et son conteneur, et éprouver trois failles à la main.
Ce dossier est le fil rouge des quinze jours. Tous les modules s'y réfèrent : le M2 y corrige cinq failles, le M3 en durcit l'image et la déploie sur un cluster, le M4 en fait le modèle de menaces, le projet fil rouge en présente l'audit complet. Ne le déplacez pas, ne le renommez pas.
- Créez le dépôt et placez-vous dedans. Toutes les commandes de ce TP et des suivants partent de ce répertoire.
cd ~/poei-s4 mkdir -p devsecops-lab cd devsecops-lab - Créez un environnement virtuel Python et activez-le. Sans lui, les versions à CVE que vous allez installer iraient polluer votre Python système — et les distributions récentes refusent purement et simplement l'installation.
python3 -m venv .venv source .venv/bin/activate # macOS et Linux # .venv\Scripts\activate # Windows PowerShell python -c "import sys; print(sys.prefix)" # doit pointer dans .venv - Créez
requirements.txt. Ces versions sont délibérément anciennes : elles portent de vraies vulnérabilités publiées, et c'est la matière de l'analyse de composition du module 2. Ne les montez pas.Flask==2.0.1 Werkzeug==2.0.1 Jinja2==2.11.3 PyYAML==5.3.1 requests==2.25.1 urllib3==1.26.4 - Installez les dépendances. L'installateur peut vous avertir que certaines versions sont obsolètes : c'est exactement l'effet recherché, et vous relirez ces avertissements au module 2 avec Trivy.
pip install -r requirements.txt - Créez
app.pyet recopiez ce code à l'identique, commentaires compris. Chaque bloc porte l'identifiant de sa faille : ces commentaires sont la carte que les outils viendront confirmer, ils restent dans le fichier.#!/usr/bin/env python3 """devsecops-lab — application deliberement vulnerable. NE JAMAIS exposer sur un reseau.""" import hashlib import os import sqlite3 import subprocess import requests import yaml from flask import Flask, request, render_template_string, g app = Flask(__name__) # VULN-05 : secret code en dur dans la source (a detecter par gitleaks). app.config["SECRET_KEY"] = "s3cr3t-dev-key-ne-pas-committer-42" API_KEY = "AKIAIOSFODNN7EXAMPLE" # VULN-05 : faux identifiant AWS en dur DB = "lab.db" def db(): if "db" not in g.__dict__: g.db = sqlite3.connect(DB) return g.db @app.route("/") def index(): return "devsecops-lab — cible d'entrainement locale" @app.route("/login", methods=["POST"]) def login(): # VULN-01 : injection SQL — la requete est construite par concatenation. user = request.form.get("user", "") pwd = request.form.get("pwd", "") q = "SELECT * FROM users WHERE user='%s' AND pwd='%s'" % (user, pwd) row = db().execute(q).fetchone() return ("bienvenue" if row else "refuse"), (200 if row else 401) @app.route("/search") def search(): # VULN-02 : XSS reflechi — l'entree utilisateur est rendue sans echappement. terme = request.args.get("q", "") return render_template_string("<h1>Resultats pour " + terme + "</h1>") @app.route("/ping") def ping(): # VULN-03 : injection de commande — shell=True sur une entree utilisateur. hote = request.args.get("host", "127.0.0.1") out = subprocess.check_output("ping -c1 " + hote, shell=True) return out @app.route("/profile/<int:uid>") def profile(uid): # VULN-04 : IDOR — aucun controle d'autorisation sur uid. row = db().execute("SELECT user FROM users WHERE id=%d" % uid).fetchone() return {"id": uid, "user": row[0] if row else None} @app.route("/hash") def weak_hash(): # VULN-06 : hachage faible (md5) pour un mot de passe. return hashlib.md5(request.args.get("p", "").encode()).hexdigest() @app.route("/config", methods=["POST"]) def load_config(): # VULN-07 : deserialisation non sure — yaml.load complet permet l'execution de code. return str(yaml.load(request.data, Loader=yaml.Loader)) @app.route("/fetch") def fetch(): # VULN-08 : SSRF — l'URL fournie par l'utilisateur est appelee cote serveur. return requests.get(request.args.get("url", ""), timeout=3).text def init_db(): c = sqlite3.connect(DB) c.execute("CREATE TABLE IF NOT EXISTS users(id INTEGER PRIMARY KEY, user TEXT, pwd TEXT)") c.execute("INSERT INTO users(user, pwd) VALUES('admin', 'admin123')") c.commit() c.close() if __name__ == "__main__": if not os.path.exists(DB): init_db() # VULN-09 : debug=True expose la console Werkzeug ; host 0.0.0.0 sans en-tetes de securite. app.run(host="0.0.0.0", port=5000, debug=True) - Lancez l'application et vérifiez qu'elle répond. Laissez ce terminal ouvert : il sera votre console pendant tout le TP.
python app.py # Dans un SECOND terminal, depuis le meme dossier : curl -s http://localhost:5000/ - Première faille, à la main : VULN-01, l'injection SQL. Authentifiez-vous d'abord normalement, pour établir la référence, puis sans mot de passe valide. Notez les guillemets doubles autour de la charge : elle contient une apostrophe, et votre shell la supprimerait sinon.
# 1. Authentification legitime — attendu : bienvenue, code 200 curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \ --data "user=admin&pwd=admin123" # 2. Mot de passe faux — attendu : refuse, code 401 curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \ --data "user=admin&pwd=nimportequoi" # 3. Injection — attendu : bienvenue, code 200, SANS mot de passe valide curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \ --data "user=admin'--&pwd=x" - Arrêtez-vous et écrivez la requête sur papier. Le code construit
SELECT * FROM users WHERE user='%s' AND pwd='%s'par concaténation. Remplacez le premier%sparadmin'--et lisez ce que la base reçoit réellement. Tant que vous ne l'avez pas écrit vous-même, vous connaissez le nom de l'attaque, pas son mécanisme — et c'est le mécanisme que le module 5 vous demandera de reproduire sur une cible dont vous n'aurez pas le code. - Deuxième faille : VULN-02, le XSS réfléchi. Le terme de recherche est réinjecté dans la page sans échappement. Observez la réponse brute : la balise ressort telle quelle, ce qui signifie qu'un navigateur l'exécuterait.
curl -s "http://localhost:5000/search?q=<script>alert(1)</script>" # attendu, mot pour mot : # <h1>Resultats pour <script>alert(1)</script></h1> - Troisième faille : VULN-03, l'injection de commande. Le paramètre est concaténé dans une commande shell. Le point-virgule termine le
pinget ouvre la commande suivante.curl -s "http://localhost:5000/ping?host=127.0.0.1" # Puis, avec une seconde commande greffee : curl -s "http://localhost:5000/ping?host=127.0.0.1;id" - Arrêtez l'application avec Ctrl+C, puis créez le
Dockerfile. Cette version est mauvaise, et c'est voulu : elle tourne enroot, part d'une image complète, copie tout le répertoire dans l'image. Le module 3 la remplacera par une version durcie, après que Trivy en aura fait la démonstration. Ne corrigez rien maintenant.FROM python:3.12 WORKDIR /app COPY . . RUN pip install -r requirements.txt EXPOSE 5000 CMD ["python", "app.py"] - Construisez l'image et lancez le conteneur sur le réseau du parcours. Le
.final est le contexte de construction : lancez la commande depuisdevsecops-lab/, jamais ailleurs.docker build -t devsecops-lab:dev . docker run -d --name devsecops-lab --network dso-net \ -p 5000:5000 devsecops-lab:dev docker ps | grep devsecops-lab - Vérifiez que le conteneur répond depuis votre poste, puis rejouez l'injection SQL contre lui. Le comportement doit être identique à celui du lancement local : c'est ce qui prouve que l'image contient bien votre application.
curl -s http://localhost:5000/ curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \ --data "user=admin'--&pwd=x" - Dernière observation du TP, et elle prépare le module 3 : demandez au conteneur sous quelle identité il exécute votre code.
docker exec devsecops-lab whoami # attendu : root docker image inspect devsecops-lab:dev --format '{{.Size}}' \ | awk '{print $1/1024/1024 " Mo"}' root, et une image de plusieurs centaines de mégaoctets. Retenez ces deux chiffres : le module 3 vous fera produire une image:hardenedet vous demanderez de les comparer. Vous ne corrigez rien aujourd'hui — vous constatez, et vous notez.
- Vous avez lancé l'application deux fois : en local, puis en conteneur. Qu'est-ce que la seconde exécution prouve, que la première ne prouvait pas ?
- L'injection SQL vous a authentifié sans mot de passe. Quelle autre conséquence, plus grave que le contournement d'authentification, la même faille rend-elle possible ?
- Le
Dockerfilecopie tout le répertoire dans l'image, y compris ce que vous n'avez pas encore créé. Quel fichier, que vous écrirez au TP suivant, ne devrait surtout pas s'y trouver ?
Sous macOS, le port 5000 est souvent déjà pris par le service AirPlay Receiver. Symptôme : Flask démarre puis rend immédiatement la main, ou curl localhost:5000 renvoie une réponse qui n'est pas la vôtre. Vérifiez avec lsof -nP -iTCP:5000 | grep LISTEN ; si un ControlCenter apparaît, désactivez le récepteur AirPlay dans les Réglages Système, section Général puis Partage.
Deuxième point : la charge d'injection SQL contient une apostrophe. Elle doit rester entre guillemets doubles dans la ligne de commande — entre apostrophes, votre shell la mangerait et vous concluriez à tort que la faille n'existe pas.
Enfin, pip install hors environnement virtuel finit par un refus de l'installateur système sur les distributions récentes. Le venv de l'étape 2 n'est pas une préférence de style.
Intention — poser l'artefact central du parcours, et surtout faire éprouver trois failles à la main avant qu'aucun outil n'en parle. C'est l'ordre qui compte : il doit savoir ce qu'est une injection SQL par l'expérience de l'avoir faite, sinon la sortie de Semgrep au jour 3 ne sera qu'un texte de plus.
Ce qui coince
- Le port 5000 pris par AirPlay sous macOS. Panne la plus fréquente de ce TP, et la plus déroutante : Flask annonce écouter, la réponse vient d'ailleurs.
- La charge
admin'--passée entre apostrophes simples : le shell l'ampute, la réponse estrefuse, il conclut que l'injection ne marche pas et va « corriger » son code. Vingt minutes perdues si personne n'intervient. - Le
docker buildlancé ailleurs que dansdevsecops-lab/: le contexte est faux,COPY . .embarque le mauvais dossier. - La correction spontanée du SQL, malgré la consigne. Vérifier à l'oral au point du soir : lui demander de lancer la charge devant vous.
Questions à anticiper
- « Pourquoi
--à la fin de la charge ? » — c'est le commentaire SQL qui neutralise le reste de la requête. Le lui faire écrire à la main sur papier : la requête concaténée, puis la requête obtenue. Deux minutes, et l'injection est comprise pour de bon. - «
debug=True, ce n'est pas juste pratique ? » — c'est un interpréteur Python exposé sur le réseau. Ne pas le démontrer aujourd'hui, le M3 le fait. - « On lance en local ou en conteneur ? » — les deux, et c'est volontaire : le local pour éprouver vite, le conteneur parce que c'est lui que Trivy scannera au M3.
Ce qu'il faut exiger — trois preuves, faites devant vous : la réponse bienvenue sur une authentification sans mot de passe valide, la balise <script> rendue telle quelle dans la page, et docker exec devsecops-lab whoami qui répond root. Le troisième point n'est pas une faille d'aujourd'hui : c'est la dette que le M3 remboursera, et il doit l'avoir vue de ses yeux.
TP 4 — Le dépôt, le .gitignore, le README et le journal
Jour 1 · Poser le décor, le droit et le dépôt · 1 h
Poser les quatre artefacts documentaires du parcours et faire le premier commit, sans y embarquer ce qui ne doit pas s'y trouver.
- Avant tout autre geste, créez le
.gitignore. Il doit exister avant le premiergit add: un fichier déjà suivi ne s'ignore plus en ajoutant une ligne ici.# .gitignore .venv/ __pycache__/ *.pyc # La base de donnees locale : elle se recree toute seule au demarrage. lab.db # Les secrets d'execution. .env.exemple, lui, EST versionne. .env # Les sorties d'outils : volumineuses, regenerables, et parfois bavardes. artefacts/ - Créez les deux fichiers d'environnement. Le premier contient de fausses valeurs et n'est pas versionné ; le second contient les mêmes clés, vides, et l'est. C'est la convention qui permet à quelqu'un de reprendre votre projet sans jamais lire vos secrets.
# ---------- .env (ignore par git) ---------- APP_SECRET_KEY=valeur-locale-de-developpement-a-remplacer APP_API_KEY=AKIAIOSFODNN7EXAMPLE # ---------- .env.exemple (versionne) ---------- # Copiez ce fichier en .env et renseignez les valeurs. # Aucune valeur reelle ne doit figurer ici. APP_SECRET_KEY= APP_API_KEY= - Créez
journal-incidents.md. Sa première ligne n'est pas un incident : c'est votre périmètre autorisé, recopié depuis la leçon de ce matin. Elle ne bougera plus de quinze jours.# Journal d'incidents — devsecops-lab Perimetre autorise : localhost uniquement — devsecops-lab (5000) et juice-shop (3000). Jamais Internet, jamais une ressource Caplogy, jamais un tiers sans mandat ecrit. ## Convention Une entree par incident ou par exception acceptee. Chaque entree porte : date, identifiant (INC-NN ou VULN-NN), constat, decision, echeance, verification. ## Entrees ### 2026-08-03 — Ouverture du journal Perimetre pose. Dix vulnerabilites delibrees introduites dans app.py (VULN-01..10), cartographiees dans le README. Aucune correction anticipee. - Créez
README.md. Ce squelette de six sections vous suivra jusqu'à la soutenance : chaque module en remplit une ou deux. Écrivez dès maintenant les sections 1 et 2, et laissez les autres en titres.# devsecops-lab Application Flask deliberement vulnerable, support du parcours S4 (POEI 2026). **Ne jamais exposer sur un reseau.** Perimetre : localhost uniquement. ## 1. Lancer l'application python3 -m venv .venv && source .venv/bin/activate pip install -r requirements.txt python app.py # http://localhost:5000 En conteneur : docker build -t devsecops-lab:dev . docker run -d --name devsecops-lab --network dso-net -p 5000:5000 devsecops-lab:dev ## 2. Les dix vulnerabilites | ID | Faille | Emplacement | CWE | OWASP | Refermee en | |----|--------|-------------|-----|-------|-------------| | VULN-01 | Injection SQL | /login | CWE-89 | A03 | M1 J3 | | ... | ... | ... | ... | ... | ... | ## 3. Secrets et configuration _(module 1, jour 2)_ ## 4. Garde-fous locaux et pipeline _(module 1, jours 2 et 3)_ ## 5. Exceptions acceptees _(module 1, jour 3)_ ## 6. Limites connues _(a completer)_ - Complétez le tableau de la section 2 avec les dix lignes. Recopiez-les depuis la leçon précédente, mais vérifiez chaque emplacement dans votre
app.pyplutôt que de faire confiance au tableau : c'est votre fichier, et c'est vous qui répondrez des écarts. - Initialisez le dépôt. Vérifiez le répertoire courant avant de taper la commande.
pwd # doit finir par /poei-s4/devsecops-lab git init -b main - Regardez avant d'ajouter. C'est le geste que l'on saute, et c'est celui qui évite les mauvaises surprises. La sortie doit tenir en une dizaine de fichiers ; si elle en liste des centaines, vous n'êtes pas au bon niveau.
git status --short - Ajoutez et committez. Le message est à l'impératif, sur une ligne, et dit ce qui fonctionne — c'est la convention du parcours entier.
git add . git commit -m "feat: application vulnerable devsecops-lab et son image de dev" - Vérifiez ce qui est réellement suivi. Les trois commandes doivent ne rien renvoyer. Ce n'est pas la présence du
.gitignorequi prouve quoi que ce soit : c'est cette sortie vide.git ls-files | grep -E '\.env$' # doit etre vide git ls-files | grep -E '\.venv|lab\.db' # doit etre vide git ls-files | grep -c '' # le nombre de fichiers suivis : une dizaine - Vérifiez enfin que
.env.exemple, lui, est bien suivi. L'oublier est l'erreur symétrique, et elle rend le dépôt inutilisable pour un tiers.git ls-files | grep env # attendu : .env.exemple — et lui seul - Vous avez un dépôt, quatre documents et un commit. Vous n'avez pas encore de dépôt distant, pas de signature, pas de garde-fou : c'est la journée de demain. En l'état, votre historique contient déjà quelque chose que vous n'auriez pas dû committer — vous ne le savez pas encore, et c'est Gitleaks qui vous le dira demain matin.
- Un fichier ajouté au
.gitignoreaprès avoir été commité reste suivi. Pourquoi Git se comporte-t-il ainsi, et que faut-il faire de plus ? .env.exemplene contient aucune valeur. À quoi sert-il, alors ?- Votre
DockerfilefaitCOPY . .et il n'y a pas de.dockerignore. Que contient donc votre image, en plus de votre code ?
Tout ce TP tient dans un ordre : le .gitignore avant le premier git add. Un fichier ignoré après avoir été suivi reste suivi — Git ne revient pas en arrière tout seul. Si vous inversez les deux gestes, votre .venv et votre base lab.db entrent dans l'historique, et vous les y retrouverez le 21 août.
Deuxième point : vérifiez le niveau où vous lancez git init. S'il est lancé depuis ~/poei-s4 au lieu de ~/poei-s4/devsecops-lab, git status vous listera aussi les artefacts et les captures du parcours. Le signe qui ne trompe pas : des centaines de fichiers proposés au suivi.
Intention — poser les quatre livrables transverses au moment où ils coûtent une heure, et installer la discipline du commit. Onze critères de points de contrôle du module s'y réfèrent ensuite. C'est aussi la seule étape qui installe explicitement la méthode de travail en autonomie : un commit par TP terminé, le journal rempli au moment du blocage et non le soir.
Ce qui coince
git initau mauvais niveau. Signe immédiat :git statusliste des centaines de fichiers. Facile à voir aujourd'hui, coûteux demain.- Le
.gitignoreécrit après le premiergit add .. Il croira le problème réglé parce que le fichier existe ;git ls-filesdira le contraire. Exiger la vérification, pas la déclaration. - Point délicat et volontaire :
.envest ignoré, mais les secrets en dur d'app.pypartent bel et bien dans le premier commit. C'est la matière de la journée de demain. Ne pas le lui signaler — Gitleaks le fera à sa place, et la découverte vaut mieux que l'annonce. journal-incidents.mdlaissé vide. C'est le livrable qui décroche en premier quand on travaille seul, et il ne se rattrape pas : le vérifier au point de 16 h 30 de ce soir, pas au bilan de mercredi.
Questions à anticiper
- « Pourquoi versionner
.env.exemple? » — parce qu'un nouveau venu doit savoir quelles variables existent sans jamais connaître leurs valeurs. C'est la réponse à retenir mot pour mot. - « Je pousse sur GitHub maintenant ? » — non, demain : le compte, la clé de signature et la protection de branche se posent ensemble.
Ce qu'il faut exiger — deux commandes lancées devant vous : git ls-files | grep -E '\.env$|\.venv|lab.db' qui ne renvoie rien, et git log --oneline qui montre au moins un commit au message impératif. Et la première ligne du journal, qui est le périmètre légal.
M1 · J1 — Cibles déployées et dépôt initialisé
Jour 1 · Poser le décor, le droit et le dépôt · 30 min
Votre encadrant vous demandera de montrer, pas de raconter : il vous fera lancer la charge d'injection devant lui. Préparez vos terminaux et votre conteneur.
Il vous demandera aussi les trois interdits, page fermée. C'est le seul critère du parcours qui ne se rattrape pas le lendemain.
Ouvrez le bouton Mon avancement, en haut à droite de cette page : il récapitule vos étapes, vos scores et vos points de contrôle. C'est ce que vous montrez en ouverture du point.
Intention — vérifier que le décor des quinze jours est en place et que le cadre légal a été lu, avant que la moindre notion d'outillage ne s'y ajoute. Trente minutes : c'est le point quotidien de 16 h 30, pas une revue.
Ce qu'il faut exiger — quatre gestes, faits par lui devant vous, dans cet ordre :
- Il énonce les trois interdits sans relire la page. Si l'un manque, on recommence la leçon ce soir : ce n'est pas négociable et ce n'est pas un point de cours.
- Il lance la charge
user=admin'--&pwd=xcontre son conteneur et obtientbienvenue. S'il obtientrefuse, deux causes : les apostrophes du shell, ou il a « corrigé » son code — les deux se règlent tout de suite. docker exec devsecops-lab whoami→root. Il doit savoir dire que c'est une dette, pas une panne, et qu'elle se rembourse au M3.git ls-files | grep -E '\.env$'→ vide.
Ce qui coince
- Le critère bâclé neuf fois sur dix : la section 2 du README, recopiée du cours sans vérification dans
app.py. Lui demander à l'oral où vit VULN-07 : s'il hésite, il a recopié. - Le journal ouvert mais sans la ligne de périmètre. C'est le critère le plus court à vérifier et le plus significatif.
- Les défis de Juice Shop entamés malgré la consigne. Le lui faire arrêter net et expliquer pourquoi : il brûlerait la matière du M5.
Questions à anticiper
- « Je peux nettoyer le dossier ce week-end ? » — non. Ce dépôt est le socle des six modules et de la soutenance. Le dire explicitement ce soir.
- « Pourquoi ne pas corriger l'injection tout de suite, je sais le faire ? » — parce que le pipeline de mercredi doit d'abord échouer. C'est la démonstration, pas le code.
Ne pas valider si — un des trois interdits ne peut être énoncé, ou si l'injection SQL n'a jamais produit bienvenue devant témoin. Dans le second cas, tout le module 1 perd sa démonstration.
Comment un secret fuit — et pourquoi le supprimer ne suffit pas
Jour 2 · Git de confiance et garde-fous locaux · 30 min
Vous avez commité hier soir. Vous ne le savez pas encore, mais votre historique contient deux secrets. Ils y sont depuis dix-huit heures. Cette leçon explique par où ils sont entrés, ce que cela coûte, et pourquoi le réflexe naturel — supprimer la ligne — est le mauvais.
Par où passe une fuite
Le code source est la voie la plus connue, et loin d'être la seule. Un secret fuit par tous les endroits où l'on écrit sans y penser :
| Voie de fuite | Exemple typique | Ce qui la rend invisible |
|---|---|---|
| Le code | une clé d'API en constante, « le temps du développement » | elle marche, donc plus personne ne la regarde |
| La configuration commitée | un .env ajouté par un git add . distrait | un seul commit suffit, et il passe en revue |
| Les journaux de CI | un echo de débogage, un set -x oublié | les journaux sont souvent lisibles plus largement que le code |
| Les couches d'image | un COPY . . qui embarque le .env | l'image finale ne le montre pas, la couche intermédiaire le garde |
| L'historique de commandes | un jeton passé en argument, capté par ~/.zsh_history | personne ne pense à un fichier local |
| Les canaux d'équipe | une clé collée dans un message pour dépanner un collègue | la conversation est archivée et indexée pour des années |
Retenez la quatrième ligne : elle vous concerne aujourd'hui. Votre
Dockerfile fait COPY . . et vous n'avez pas de
.dockerignore. Votre image devsecops-lab:dev contient donc votre
.env, bien qu'il soit correctement exclu de Git. Deux mécanismes d'exclusion
différents, deux fichiers différents — c'est le module 3 qui refermera celui-là.
Git est un journal, pas un classeur
Git ne modifie jamais un objet existant. Quand vous supprimez la ligne contenant la clé et que vous committez, vous ajoutez un commit qui contient une nouvelle version du fichier. L'ancienne version — le blob avec la clé — reste dans la base d'objets, référencée par le commit d'origine.
Autrement dit : git log -p la retrouve, git show <sha> l'affiche, et quiconque a cloné le dépôt en possède une copie complète sur son disque. La suppression a réglé le présent et rien d'autre.
C'est pour cela que la remédiation d'une fuite se fait dans un ordre précis, et que l'inverser rend le reste inutile.
Les quatre temps, dans l'ordre
- Révoquer. Dans la console du fournisseur, invalider la clé et en émettre une nouvelle. C'est le seul geste qui supprime réellement le risque : à partir de cette seconde, la clé fuitée ne vaut plus rien, où qu'elle se trouve.
- Mesurer l'exposition. Depuis quand ? Dépôt public ou privé ? Combien de clones, de forks, de miroirs ? La clé apparaît-elle dans des journaux de CI, dans une image publiée, dans un ticket ? Et surtout : a-t-elle servi à quelqu'un d'autre — que disent les journaux d'accès du fournisseur ?
- Réécrire l'historique. Purger le secret des commits passés. C'est du nettoyage, pas de la remédiation : cela réduit la surface pour l'avenir, cela n'annule rien du passé.
- Prévenir la récidive. Un hook local, un contrôle en CI, et une entrée dans le journal d'incidents qui dit ce qui s'est passé et ce qui a été mis en place.
Entre la fuite et la réécriture, il s'écoule des heures ou des jours. Pendant tout ce temps, la clé est lisible. Si vous commencez par réécrire, vous passez une heure à manipuler un historique pendant laquelle le secret reste valide — et vous finirez peut-être par croire l'incident clos sans avoir jamais révoqué.
La règle professionnelle : on considère qu'un secret exposé est un secret compromis, sans chercher à prouver qu'il a été utilisé. La preuve du contraire n'existe pas.
Comment un détecteur trouve un secret
Gitleaks, que vous lancerez dans quelques minutes, combine deux familles de règles. Elles ont des angles morts opposés, et c'est pour cela qu'on les emploie ensemble.
| Famille | Principe | Attrape bien | Rate |
|---|---|---|---|
| Motif | une expression régulière par type de secret : AKIA… pour AWS, ghp_… pour GitHub, -----BEGIN…KEY----- pour une clé privée | les secrets de fournisseurs connus, avec très peu de faux positifs | tout ce qui n'a pas de forme reconnaissable — un mot de passe de base, un jeton maison |
| Entropie | mesure du désordre d'une chaîne : une suite aléatoire de 40 caractères a une entropie bien plus haute qu'un mot de la langue | les secrets sans forme fixe, les jetons générés | rien, mais elle produit des faux positifs — un hachage de commit, une empreinte, un identifiant encodé |
Voici à quoi ressemble un finding Gitleaks. Retenez trois champs : RuleID dit
quelle règle a mordu, Commit dit dans quel commit le secret vit, et
Fingerprint est l'identifiant stable qui vous servira à déclarer un faux positif
sans désactiver la règle entière.
[
{
"RuleID": "aws-access-token",
"Description": "AWS Access Token",
"File": "app.py",
"StartLine": 16,
"Secret": "AKIAIOSFODNN7EXAMPLE",
"Commit": "8f2a1c4e9b7d3a5c2e8f0b6d4a1c9e7b2f5d8a3c",
"Author": "Elie Zaraket",
"Date": "2026-08-03T17:12:44Z",
"Fingerprint": "8f2a1c4e:app.py:aws-access-token:16"
}
]Où un secret doit vivre
| Type de secret | Où il doit vivre | Ce qui prouve qu'il n'est pas dans le dépôt |
|---|---|---|
| Configuration locale de développement | un .env ignoré par Git, décrit par un .env.exemple versionné | git ls-files | grep '\.env$' ne renvoie rien |
| Secret de pipeline | le magasin de secrets de la forge, injecté en variable d'environnement au moment du job | aucune valeur en clair dans ci.yml, qui est versionné |
| Secret d'application en production | un gestionnaire de secrets, lu au démarrage ou monté par l'orchestrateur | l'image ne contient aucune valeur — le module 3 le vérifiera avec Trivy |
| Clé privée (SSH, TLS) | hors du dépôt, générée localement ; seule la clé publique se partage | le hook detect-private-key refuse le commit |
Vous appliquerez les deux premières lignes aujourd'hui même, et vous constaterez au passage
que votre app.py viole allègrement la première.
La phrase à retenir de cette leçon tient en six mots : supprimer n'est pas retirer. Git n'efface rien. Le commit qui « enlève » votre clé est un commit de plus, qui ajoute une version du fichier sans la clé — la version avec la clé reste dans l'historique, atteignable par n'importe qui a cloné le dépôt.
Conséquence directe sur votre réflexe : le premier geste devant un secret fuité n'est jamais git. C'est la console du fournisseur, pour révoquer.
Intention — installer la doctrine avant l'outil : révoquer d'abord, réécrire ensuite. C'est l'ordre qui sauve, et c'est celui que presque tout le monde inverse en situation réelle, sous le coup de la panique. La leçon est placée avant Gitleaks pour qu'il sache pourquoi il scanne avant de savoir comment.
Ce qui coince
- Le modèle mental de Git comme d'un système de fichiers avec historique. Signe : « j'ai supprimé la ligne et poussé, c'est réglé ». Tant que ce modèle tient, la réécriture du TP 8 sera vécue comme une formalité au lieu d'une opération à risque.
- L'idée qu'un dépôt privé protège. Il réduit l'exposition, il ne l'annule pas : clones locaux, forks internes, sauvegardes, journaux de CI, intégrations tierces. Le dire, sinon il conclura que sa fuite d'hier est sans conséquence.
- La confusion entre détection par motif et détection par entropie. Elle explique à la fois les faux positifs qui l'agaceront au TP 7 et les faux négatifs qui devraient l'inquiéter.
Questions à anticiper
- « Ma clé AWS est fausse, pourquoi tout ce cérémonial ? » — parce qu'on répète un geste pour l'avoir en réflexe le jour où elle sera vraie. Répondre par l'analogie de l'exercice d'évacuation, pas par la morale.
- « Combien de temps avant qu'une clé publique soit exploitée ? » — les ordres de grandeur publiés se comptent en minutes pour les dépôts publics, des robots surveillant le flux d'événements GitHub en continu. Donner l'ordre de grandeur, pas un chiffre précis.
- « Et si je chiffre le
.envavant de le committer ? » — question légitime, et la réponse ouvre sur SOPS et les sealed secrets. Le nommer, ne pas le déployer : ce n'est pas dans le périmètre du parcours.
Ce qu'il faut exiger — qu'il récite les quatre temps dans l'ordre : révoquer, mesurer, réécrire, prévenir. Un échec ici se lit au TP 8, où il commencera par git filter-repo et n'écrira jamais la ligne de rotation dans son journal.
Quiz — Secrets, historique Git et détection
Jour 2 · Git de confiance et garde-fous locaux · 10 min
1. Vous supprimez la ligne contenant la clé, vous committez, vous poussez. Que reste-t-il dans le dépôt ?
2. Dans quel ordre traiter une clé d'API poussée par erreur ?
3. Sur quelles familles de règles Gitleaks s'appuie-t-il ?
4. Vous installez pre-commit dans votre dépôt et vous poussez. Un collègue clone. Que se passe-t-il chez lui ?
5. Où doit vivre la clé d'API de production de votre application ?
Intention — vérifier la doctrine avant de la mettre en œuvre : le modèle mental de Git (q1), l'ordre des gestes (q2), le fonctionnement du détecteur (q3), la portée réelle d'un hook (q4), et le bon emplacement d'un secret (q5).
Ce qui coince
- q1 ratée : le modèle mental n'y est pas, et le TP 8 sera incompréhensible. À reprendre au tableau avec trois commits dessinés, cinq minutes suffisent.
- q2 ratée en choisissant l'option 3 : c'est l'erreur la plus révélatrice du quiz. « Révoquer si elle a servi » suppose qu'on puisse prouver qu'elle n'a pas servi — on ne le peut jamais.
- q4 ratée : il croira ses collègues protégés par un fichier versionné. C'est exactement l'illusion que le jour 3 vient corriger.
Questions à anticiper
- « La 5, un
.envchiffré, ce n'est pas correct non plus ? » — SOPS existe et c'est une pratique réelle ; ici la passphrase partagée par l'équipe est ce qui rend l'option fausse. La nuance mérite d'être dite.
Ce qu'un échec révèle — 4/5 avec q2 fausse : reprenez l'ordre des quatre temps avant le TP 8, il inverserait tout.
TP 5 — Gitleaks trouve ce que vous avez commité hier
Jour 2 · Git de confiance et garde-fous locaux · 55 min
Détecter les secrets de votre propre historique, lire un rapport de findings, et déclarer un faux positif sans désactiver la règle.
Votre dépôt a un commit. Vous avez correctement exclu .env. Vous pouvez donc raisonnablement penser qu'aucun secret n'est versionné. Ce TP existe pour éprouver cette conviction, pas pour la confirmer.
- Placez-vous dans le dépôt et lancez Gitleaks sur l'historique. Remarquez l'absence de
--no-git: c'est elle qui fait parcourir les commits plutôt que les seuls fichiers présents.cd ~/poei-s4/devsecops-lab docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \ detect --source=/repo -v - Relancez la même analyse en mode « fichiers », puis comparez les deux sorties. Ce sont deux questions différentes : « qu'y a-t-il dans mon historique » et « qu'y a-t-il dans mon arbre de travail ».
docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \ detect --source=/repo --no-git -v - Regardez le code de sortie. C'est lui, et non le texte affiché, qui fera échouer votre pipeline demain. Retenez la convention : 1 quand des secrets sont trouvés, 0 quand il n'y en a aucun.
docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \ detect --source=/repo -v ; echo "code de sortie : $?" - Lisez attentivement chaque finding et répondez par écrit à trois questions : combien de secrets sont remontés, dans quel commit chacun se trouve, et — la plus importante — est-ce que le compte est bon au regard de ce que vous avez écrit hier dans
app.py? Si un secret que vous savez présent n'apparaît pas, vous venez d'observer un faux négatif : notez-le, c'est un résultat, pas une panne. - Produisez le rapport en JSON, dans le dossier des artefacts. Attention au chemin :
--report-pathdoit désigner un emplacement à l'intérieur du volume monté, sinon le fichier meurt avec le conteneur.mkdir -p artefacts docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \ detect --source=/repo \ --report-format json \ --report-path /repo/artefacts/gitleaks-2026-08-04.json ls -l artefacts/ - Ouvrez le rapport et isolez les champs qui comptent.
Fingerprintest l'identifiant stable d'un finding : il ne change pas tant que le secret reste au même endroit, et c'est lui qu'on utilise pour déclarer un faux positif.python3 -m json.tool artefacts/gitleaks-2026-08-04.json | head -40 # Ou, plus lisible, les seuls champs utiles : python3 -c "import json;d=json.load(open('artefacts/gitleaks-2026-08-04.json'));\ [print(x['RuleID'],'|',x['File'],':',x['StartLine'],'|',x['Fingerprint']) for x in d]" - Vérifiez que le rapport n'est pas versionné :
artefacts/figure dans votre.gitignore. Un rapport de secrets commité serait le comble — il contient les secrets en clair, dans le champSecret. - Créez
.gitleaks.tomlpour déclarer un faux positif. Cas d'école : la chaîneAKIAIOSFODNN7EXAMPLEest l'identifiant d'exemple publié par AWS dans sa propre documentation — il ne donne accès à rien. Un vrai faux positif, donc, et l'occasion d'apprendre le geste. Notez la structure : on hérite des règles par défaut, on n'en désactive aucune, on écarte une occurrence nommément.# .gitleaks.toml [extend] useDefault = true [[rules]] id = "aws-access-token" [rules.allowlist] description = """Identifiant d'exemple publie par AWS dans sa documentation. Ne donne acces a aucune ressource. Verifie le 2026-08-04 par E. Zaraket.""" regexes = [ '''AKIAIOSFODNN7EXAMPLE''' ] - Relancez l'analyse avec votre configuration et constatez la différence. Vérifiez surtout que les autres findings sont toujours là : une allowlist trop large est pire que pas d'allowlist du tout, parce qu'elle donne le sentiment d'un dépôt propre.
docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \ detect --source=/repo --config=/repo/.gitleaks.toml -v echo "code de sortie : $?" - Consignez l'incident dans votre journal. C'est le temps n° 2 de la doctrine — mesurer l'exposition — et il s'écrit maintenant, pas ce soir.
# A ajouter dans journal-incidents.md, section Entrees : ### 2026-08-04 — INC-01 : deux secrets en dur dans l'historique (VULN-05) **Constat.** gitleaks detect sur l'historique remonte des secrets introduits par le commit initial du 2026-08-03 : cle AWS d'exemple et SECRET_KEY Flask, dans app.py. **Exposition.** Depot local, aucun distant a ce stade, aucun clone. Cle AWS = identifiant d'exemple publie par AWS, sans acces. SECRET_KEY = valeur de developpement, jamais utilisee en production. **Rotation.** Sans objet ici (valeurs factices). En situation reelle : revocation immediate dans la console du fournisseur AVANT toute manipulation de l'historique. **Decision.** Correction et purge de l'historique planifiees ce jour (TP 8). Prevention par hook pre-commit (TP 7) puis par le pipeline (J3). **Faux positif ecarte.** AKIAIOSFODNN7EXAMPLE, via .gitleaks.toml, justification inscrite dans le fichier. - Committez votre
.gitleaks.tomlet votre journal. Le secret, lui, est toujours dansapp.pyet toujours dans l'historique : on le traitera au TP 8, après avoir posé le garde-fou qui empêchera la récidive.
- Un secret que vous savez présent n'a pas été détecté. Qu'est-ce que cela vous apprend sur la valeur d'un rapport vide ?
- Le rapport JSON contient les secrets en clair. Quelles précautions cela impose-t-il sur sa conservation et sa transmission ?
- Vous avez écarté un faux positif par une allowlist plutôt qu'en désactivant la règle. Qu'auriez-vous perdu en désactivant la règle ?
Le piège de ce TP est une option d'une syllabe. gitleaks detect sans --no-git parcourt l'historique des commits ; avec --no-git, il traite le dossier comme un simple répertoire de fichiers et ignore complètement le passé. Les deux ont un usage, ils ne répondent pas à la même question — et vous croiserez les deux dans ce parcours, y compris demain dans votre pipeline.
Second point, propre à Docker : le rapport s'écrit dans le conteneur. Si le chemin de --report-path ne tombe pas dans le répertoire monté, votre fichier disparaît avec le conteneur et vous chercherez longtemps pourquoi il n'existe pas.
Intention — la découverte doit venir de l'outil, pas de nous. Il a commité hier sans le savoir ; ce matin, Gitleaks le lui apprend. Cette séquence — je crois mon dépôt propre, un outil me prouve le contraire — est l'expérience fondatrice du module. Ne pas la déflorer.
Ce qui coince
--no-gitemployé d'emblée par mimétisme du workflow : il ne verra que l'arbre de travail et ratera la dimension historique, qui est tout le sujet. L'étape 2 l'oblige à comparer les deux sorties — ne pas le laisser sauter la comparaison.- Le rapport écrit hors du volume monté : fichier introuvable. Cause de dix minutes de perplexité, symptôme trompeur car la commande se termine sans erreur.
- La tentation d'ajouter la vraie clé à l'allowlist pour « faire passer » l'outil. C'est le contresens complet, et c'est le moment de poser la règle : on n'écarte que ce qui n'est pas un secret, jamais ce qu'on n'a pas envie de corriger.
- Le code de sortie ignoré.
echo $?à l'étape 3 est le geste qui prépare toute la journée de demain.
Questions à anticiper
- « Pourquoi deux findings alors que j'ai trois lignes suspectes ? » — excellent signe : il a compté ses secrets avant de lire la sortie. Le motif AWS mord, la chaîne
s3cr3t-dev-key…peut ne pas atteindre le seuil d'entropie. C'est la démonstration vivante du faux négatif, bien plus parlante que la leçon. - « Le dépôt est privé, est-ce grave ? » — reprendre la réponse de la leçon : privé réduit, n'annule pas.
- « Faut-il corriger maintenant ? » — non, TP 8. Aujourd'hui on détecte et on documente ; l'ordre des quatre temps s'applique aussi à l'emploi du temps.
Ce qu'il faut exiger — la sortie des deux commandes, avec et sans --no-git, et qu'il sache dire laquelle des deux son pipeline de demain utilisera. Plus le code de sortie, énoncé de mémoire : 1 quand il trouve, 0 quand il ne trouve rien.
Commits signés et branche protégée : la confiance dans le dépôt
Jour 2 · Git de confiance et garde-fous locaux · 25 min
Ouvrez un dépôt quelconque et regardez la colonne des auteurs. Chaque commit affiche un nom et une adresse. La question à se poser, et que presque personne ne se pose : qui a vérifié cette information ?
Personne. C'est une déclaration.
user.name et user.email sont des variables de configuration que vous
choisissez. Rien, dans Git, ne les vérifie. La commande suivante produit un commit parfaitement
valide, attribué à quelqu'un d'autre :
git -c user.name="Linus Torvalds" -c user.email="torvalds@linux-foundation.org" \
commit -m "un commit que je n'ai pas ecrit"Ce n'est pas une faille de Git : c'est un choix de conception. Git est un système distribué où l'identité n'est pas centralisée. Le mécanisme qui rend l'auteur vérifiable est ajouté par-dessus, et il est facultatif : la signature cryptographique.
Ce que fait une signature
Signer un commit, c'est calculer une empreinte de son contenu et la chiffrer avec une clé privée que vous seul possédez. Quiconque dispose de la clé publique correspondante peut vérifier que le commit vient bien du détenteur de la clé privée, et qu'il n'a pas été modifié depuis.
| GPG | SSH | |
|---|---|---|
| Maturité dans Git | historique, disponible depuis très longtemps | plus récent — Git 2.34 et suivants |
| Ce qu'il faut gérer | un trousseau, une expiration, éventuellement un serveur de clés | un fichier de clé, comme pour se connecter en SSH |
| Vérification locale | par le trousseau GPG | par un fichier allowed_signers que vous déclarez |
| Notre choix ici | — | SSH : vous avez déjà l'outillage, rien à installer |
Vous allez manipuler deux clés dans le TP qui suit, et l'erreur classique est de n'en voir qu'une :
- la clé d'authentification prouve à GitHub que vous avez le droit de pousser. Elle se dépose dans la section Authentication keys.
- la clé de signature prouve que le commit vient de vous. Elle se dépose dans la section Signing keys.
Techniquement, une même clé peut jouer les deux rôles. Elle doit alors être déclarée deux fois chez GitHub, une fois par section. C'est la panne la plus silencieuse du TP suivant : tout fonctionne, la poussée passe, et la mention Verified n'apparaît jamais — parce que la clé n'a été déposée que comme clé d'authentification.
La protection de branche : le chemin obligé
La signature répond à « qui a écrit ce commit ? ». La protection de branche répond à une
question différente : « par où ce code est-il passé avant d'arriver sur main ? ».
Sur une branche protégée, on interdit ce qui court-circuite la revue :
| Règle | Ce qu'elle empêche | Ce qu'elle n'empêche pas |
|---|---|---|
| Exiger une pull request | la poussée directe sur main, y compris la vôtre un vendredi soir | qu'une pull request soit fusionnée sans être lue |
| Exiger des vérifications réussies | la fusion tant que le pipeline échoue | que le pipeline soit mal écrit ou ne vérifie rien d'utile |
| Exiger des commits signés | l'introduction d'un commit d'origine invérifiable | qu'un compte légitime mais compromis signe du mauvais code |
| Interdire la poussée forcée | la réécriture silencieuse de l'historique partagé | une réécriture assumée, protection temporairement levée et tracée |
La signature prouve l'origine : ce commit vient bien du détenteur de cette clé.
La protection de branche prouve le chemin : ce code est passé par une pull request et par les vérifications obligatoires.
Le pipeline prouve quelque chose sur le contenu : aucune règle de sécurité connue n'a mordu.
Aucune des trois ne remplace les deux autres, et aucune ne dit que le code est bon. Elles disent seulement qu'on sait d'où il vient, par où il est passé, et ce qui a été vérifié. C'est déjà considérable — et c'est exactement ce qu'on attend d'une chaîne d'approvisionnement logicielle.
Une remarque d'ordre pratique pour demain
Vous allez activer aujourd'hui deux règles : pull request obligatoire et commits signés. La
troisième — exiger des vérifications réussies — ne pourra pas être activée avant demain,
et pour une raison très concrète : GitHub ne propose de sélectionner une vérification
qu'après l'avoir vue s'exécuter au moins une fois. Tant que votre pipeline n'a pas
tourné, les cases secrets et sast n'existent pas dans l'interface.
Ce n'est pas un bogue, et ce n'est pas la peine de la chercher.
Intention — montrer que l'identité dans Git est déclarative par défaut, et que la signature est ce qui la rend vérifiable. Puis relier signature et protection de branche : l'une dit qui a écrit, l'autre dit par où c'est passé. Les deux ensemble constituent la chaîne de confiance sur laquelle le pipeline de demain s'appuiera.
Ce qui coince
- La croyance que
user.emailauthentifie. Elle tombe en dix secondes avec la démonstration degit -c user.email=…— la faire, elle marque durablement. - La confusion clé d'authentification / clé de signature. Ce sont deux clés SSH, deux usages, deux emplacements différents dans l'interface GitHub. Le TP suivant échoue silencieusement si la clé est déposée dans la mauvaise section — la mention Verified n'apparaît simplement jamais.
- L'illusion que la protection de branche protège du code malveillant. Elle impose un chemin, elle ne juge pas le contenu. C'est le pipeline qui juge.
Questions à anticiper
- « Pourquoi SSH plutôt que GPG ? » — pas de serveur de clés, pas de trousseau, pas d'expiration à gérer ; il a déjà une clé SSH. GPG reste la norme dans certains écosystèmes, le dire sans s'y engager.
- « La signature garantit-elle que le code est bon ? » — non. Elle garantit l'origine, rien d'autre. Un développeur compromis signe parfaitement.
- « Que se passe-t-il si je perds ma clé ? » — on en génère une nouvelle et on l'ajoute ; les anciens commits restent vérifiables tant que la clé publique demeure déclarée. Bon moment pour parler de
allowed_signers.
Ce qu'il faut exiger — qu'il distingue ce que prouve la signature (l'origine) de ce que prouve la protection (le chemin) de ce que prouve le pipeline (le contenu). Trois preuves différentes, souvent confondues en une seule.
TP 6 — Compte GitHub, clé de signature SSH, branche main protégée
Jour 2 · Git de confiance et garde-fous locaux · 55 min
Rendre l'origine de vos commits vérifiable et imposer un chemin obligé vers main.
Créez le compte avec une adresse personnelle, pas une adresse @caplogy : ce parcours ne doit dépendre d'aucune ressource d'entreprise. Et créez le dépôt privé — il contient une application délibérément vulnérable et, pour quelques heures encore, des secrets dans son historique.
- Créez un compte sur
github.comavec une adresse personnelle, si vous n'en avez pas déjà un. Activez l'authentification à deux facteurs — elle est de toute façon exigée pour contribuer, et c'est le minimum pour un compte qui va porter une clé de signature. - Créez un dépôt privé nommé
devsecops-lab, sans README, sans.gitignoreet sans licence : votre dépôt local existe déjà, et ces fichiers créeraient un historique divergent qu'il faudrait réconcilier pour rien. - Générez la clé de signature. Elle est distincte de votre clé d'authentification habituelle — c'est un usage différent, et les mélanger rend le diagnostic pénible.
ssh-keygen -t ed25519 -C "elie-devsecops" -f ~/.ssh/id_signing # Affichez la cle PUBLIQUE — c'est elle, et elle seule, qui se copie : cat ~/.ssh/id_signing.pub - Sur GitHub : Settings → SSH and GPG keys → New SSH key. Collez le contenu de
id_signing.pubet choisissez impérativement le type « Signing Key » dans le menu déroulant. C'est le seul geste du TP qui échoue sans rien dire : une clé déposée en « Authentication Key » ne fera jamais apparaître la mention Verified. - Configurez Git pour signer. Les trois premières lignes activent la signature ; la quatrième et la cinquième permettent à votre poste de vérifier les signatures, ce qu'il ne sait pas faire sans un fichier de correspondance.
git config --global gpg.format ssh git config --global user.signingkey ~/.ssh/id_signing.pub git config --global commit.gpgsign true # Pour verifier les signatures localement — sinon 'No signature' a l'affichage. # Remplacez l'adresse par celle de votre compte GitHub. echo "$(git config user.email) namespaces=\"git\" $(cat ~/.ssh/id_signing.pub)" \ >> ~/.ssh/allowed_signers git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers - Vérifiez que l'adresse de vos commits est bien celle de votre compte GitHub. Une adresse différente produit des commits orphelins, non rattachés à votre profil.
git config --global user.name "Elie Zaraket" git config --global user.email "<votre-adresse-github>" git config --global --list | grep -E 'user\.|gpg' - Signez votre commit existant en le reprenant, puis vérifiez la signature localement. Vous devez lire
Good "git" signature.cd ~/poei-s4/devsecops-lab git commit --amend --no-edit -S git log --show-signature -1 - Rattachez le dépôt distant et poussez. Employez l'URL SSH : en HTTPS, Git vous réclamerait un jeton d'accès personnel à chaque poussée.
git remote add origin git@github.com:<votre-compte>/devsecops-lab.git git remote -v git push -u origin main - Sur GitHub, ouvrez la liste des commits. Votre commit doit porter le badge Verified. S'il porte Unverified ou aucun badge, revenez à l'étape 4 : la clé a presque certainement été déposée dans la mauvaise section.
- Activez la protection de branche : Settings → Branches → Add branch protection rule, motif
main. Cochez Require a pull request before merging, Require signed commits, et surtout Do not allow bypassing the above settings — sans cette dernière case, la règle ne s'applique pas à vous, propriétaire du dépôt, et la vérification qui suit ne prouverait rien. Laissez Require status checks de côté : les vérifications n'existeront qu'après le premier passage du pipeline, demain. - Éprouvez la protection. Une poussée directe sur
maindoit être refusée par le serveur. C'est la preuve que la règle vous inclut.echo "" >> README.md git add README.md git commit -m "test: verifier que la protection de branche s'applique" git push origin main # attendu : remote: error: GH006: Protected branch update failed # Annulez ce commit de test : git reset --hard HEAD~1
- La signature prouve l'origine du commit. Qu'est-ce qu'elle ne prouve pas, et qui resterait entièrement à vérifier ?
- Vous avez coché « ne pas autoriser le contournement ». Qu'est-ce que cela change pour vous, propriétaire du dépôt, et pourquoi est-ce le bon réglage ?
- Un attaquant obtient votre clé de signature. Que peut-il faire, et que ne peut-il toujours pas faire ?
La panne silencieuse de ce TP : la clé déposée dans la mauvaise section de GitHub. Une clé de signature va dans Signing keys, pas dans Authentication keys. Si vous vous trompez, tout marche — la poussée réussit, le commit part — mais la mention Verified n'apparaît jamais et rien ne vous dit pourquoi.
Deuxième subtilité : git log --show-signature ne sait pas vérifier une signature SSH sans le fichier allowed_signers. Sans lui, vous verrez No signature ou une erreur, alors même que votre commit est correctement signé et que GitHub l'affiche comme vérifié. L'étape 5 règle ce point.
Enfin, si git push vous demande un mot de passe, vous poussez en HTTPS et non en SSH : GitHub n'accepte plus les mots de passe de compte depuis 2021. Vérifiez votre URL distante.
Intention — construire la chaîne de confiance en une séance : compte, clé d'authentification, clé de signature, dépôt distant, protection de branche. C'est beaucoup de configuration et peu de concept ; l'enjeu est que chaque geste soit vérifié par une preuve, pas seulement exécuté.
Ce qui coince
- La clé de signature déposée comme clé d'authentification. Panne n° 1, totalement silencieuse. Diagnostic : le commit apparaît sans badge sur GitHub. La reconnaître d'un coup d'œil fait gagner vingt minutes.
allowed_signersoublié : il croit sa signature cassée alors qu'elle est bonne. Symptôme inverse et tout aussi trompeur — GitHub dit Verified, son poste dit No signature.- Le dépôt créé en public par distraction. À vérifier explicitement : une application délibérément vulnérable avec des secrets dans l'historique n'a rien à faire sur un dépôt public. C'est un point de contrôle en soi.
- L'adresse de courriel du commit différente de celle du compte : GitHub n'associe pas le commit et affiche un auteur fantôme.
Questions à anticiper
- « Je peux utiliser mon adresse Caplogy ? » — non, compte personnel et adresse personnelle, c'est la décision du référentiel. Le dépôt reste privé.
- « Pourquoi ne puis-je pas cocher les vérifications obligatoires ? » — parce qu'aucune n'a encore tourné. Réponse déjà donnée en leçon ; s'il pose quand même la question, c'est qu'il a survolé, et cela se reverra demain.
- « La protection s'applique-t-elle à moi, propriétaire du dépôt ? » — seulement si l'on coche l'inclusion des administrateurs. Le lui faire cocher : sinon la démonstration de l'étape 9 échoue et la leçon avec.
Ce qu'il faut exiger — deux preuves à l'écran : la mention Verified à côté de son commit sur GitHub, et le refus d'une poussée directe sur main. La seconde est la plus importante : c'est elle qui prouve que la protection s'applique à lui aussi.
TP 7 — pre-commit : le garde-fou qui refuse le commit
Jour 2 · Git de confiance et garde-fous locaux · 1 h
Installer des contrôles qui s'exécutent avant chaque commit, et apprendre à traiter honnêtement ce qu'ils refusent.
- Installez pre-commit. La version est épinglée comme le reste de l'outillage.
cd ~/poei-s4/devsecops-lab source .venv/bin/activate pip install pre-commit==3.7.1 pre-commit --version - Créez
.pre-commit-config.yaml. Cinq contrôles : les secrets, le SAST Python, les clés privées, les fichiers trop gros, et deux vérifications d'hygiène.# .pre-commit-config.yaml — les garde-fous qui tournent AVANT le commit. repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.18.4 hooks: - id: gitleaks - repo: https://github.com/PyCQA/bandit rev: 1.7.9 hooks: - id: bandit args: ["-c", ".bandit.yaml"] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: - id: detect-private-key - id: check-added-large-files args: ["--maxkb=512"] - id: check-yaml - id: end-of-file-fixer - Créez la configuration de Bandit. Elle est volontairement vide de toute exclusion pour l'instant : on veut voir ce qu'il trouve avant de décider quoi que ce soit.
# .bandit.yaml exclude_dirs: - ./.venv - ./artefacts - Installez le hook dans votre dépôt. Cette commande écrit dans
.git/hooks/pre-commit— un répertoire qui n'est pas versionné et qu'aucun clone ne recevra.pre-commit install ls -l .git/hooks/pre-commit - Lancez tous les contrôles sur tout le dépôt. Le premier passage installe les environnements de chaque hook : comptez deux à quatre minutes, sans affichage. Attendez-vous à un échec bruyant — c'est le résultat attendu.
pre-commit run --all-files - Lisez la sortie et faites l'inventaire par écrit : quel hook a échoué, sur quel fichier, pour quelle règle. Vous devriez retrouver, sous des identifiants Bandit, plusieurs des vulnérabilités que vous avez introduites hier. Notez lesquelles ne sont pas remontées : c'est aussi instructif.
- Vous voilà devant le vrai sujet de ce TP : votre propre garde-fou refuse votre code. Il n'existe que deux réponses honnêtes. Corriger — mais VULN-02, 03, 06 et 07 sont réservées au module 2, les corriger maintenant priverait le module 2 de sa matière. Ou consigner une exception datée. C'est ce que nous faisons, et voici à quoi cela ressemble.
# .bandit.yaml — version avec exceptions TRACEES exclude_dirs: - ./.venv - ./artefacts # EXCEPTIONS TEMPORAIRES — echeance : module 2 (2026-08-06 au 2026-08-10) # Chaque test ignore correspond a une vulnerabilite DELIBEREE du support de # formation, cartographiee dans README.md section 2 et suivie dans # journal-incidents.md. Aucune ne doit survivre au module 2. # B602 -> VULN-03 injection de commande (subprocess shell=True) # B324 -> VULN-06 hachage faible (md5) # B506 -> VULN-07 deserialisation non sure (yaml.load) # B201 -> VULN-09 debug=True (echeance module 3) skips: - B602 - B324 - B506 - B201 - Ajoutez le tableau des exceptions à la section 5 de votre
README.md. Une exception sans échéance n'est pas une exception, c'est un abandon — c'est la règle du parcours, et le jury la vérifiera.## 5. Exceptions acceptees | Regle | VULN | Motif | Decidee le | Echeance | Levee le | |-------|------|-------|------------|----------|----------| | bandit B602 | VULN-03 | faille pedagogique deliberee | 2026-08-04 | M2 | | | bandit B324 | VULN-06 | faille pedagogique deliberee | 2026-08-04 | M2 | | | bandit B506 | VULN-07 | faille pedagogique deliberee | 2026-08-04 | M2 | | | bandit B201 | VULN-09 | faille pedagogique deliberee | 2026-08-04 | M3 | | | gitleaks aws-access-token | — | identifiant d'exemple AWS public | 2026-08-04 | aucune | | - Relancez. Il doit rester des échecs — notamment sur les secrets d'
app.py, que nous n'avons pas écartés et que nous allons vraiment corriger au TP suivant.pre-commit run --all-files - Éprouvez le garde-fou pour de bon : tentez de committer votre
.env, celui qui contient un faux secret. Le-fforce l'ajout malgré le.gitignore— et c'est exactement le geste distrait que l'on veut voir échouer.git add -f .env git commit -m "test: le hook doit refuser ce commit" # attendu : le hook gitleaks echoue, le commit N'EST PAS cree. # Verifiez-le : git log --oneline -1 # Puis retirez le fichier de l'index : git restore --staged .env - Dernière démonstration, et c'est celle qui prépare demain. Le même commit, avec une option de plus.
git add -f .env git commit --no-verify -m "test: contournement du hook" # Le commit passe. Le hook n'a rien vu, rien dit, rien empeche. git log --oneline -1 # Annulez immediatement — ce commit contient un secret : git reset --hard HEAD~1 git status --short - Voilà pourquoi la journée de demain existe. Un hook local aide celui qui veut bien être aidé ; il ne protège pas un dépôt. Le contrôle qui arrête vit sur un serveur que vous ne pouvez pas contourner depuis votre poste. Committez votre configuration — signée, et sans
--no-verifycette fois.
- Le hook vit dans
.git/hooks/, qui n'est pas versionné. Quelle conséquence cela a-t-il sur une équipe de dix personnes ? - Vous avez tracé quatre exceptions plutôt que de retirer quatre règles. Qu'est-ce que cela change, six mois plus tard, pour quelqu'un qui reprend le dépôt ?
--no-verifya un usage légitime. Lequel, et comment le distinguer d'un contournement ?
Deux vérités à tenir ensemble sur ce TP, et elles ne se contredisent pas.
La première : le hook est le contrôle le plus rentable du parcours, parce qu'il vous répond en trois secondes pendant que vous avez encore le contexte en tête.
La seconde : il ne garantit rien. Il vit dans .git/hooks/, qui n'est jamais transmis par un clone, et git commit --no-verify le contourne d'un geste. C'est un confort, pas un verrou. Le verrou, c'est demain.
Attendez-vous à ce que le premier pre-commit run --all-files échoue bruyamment. C'est le résultat attendu, pas un problème d'installation.
Intention — deux acquis distincts, et le second est le vrai sujet. Le premier : installer des hooks. Le second, bien plus important : que fait-on quand son propre garde-fou refuse un commit ? Il n'y a que deux réponses honnêtes — corriger, ou consigner une exception datée. Le --no-verify silencieux n'en est pas une, et c'est la discipline que ce TP installe pour tout le reste du parcours.
Ce qui coince
- Le premier passage échoue sur
app.py— normal, l'application est trouée. Il peut le vivre comme un échec d'installation et se mettre à déboguer pre-commit. Le rassurer vite : l'outil marche, c'est le code qui est mauvais, et délibérément. - La tentation de vider
.pre-commit-config.yamlde Bandit pour « avancer ». C'est exactement ce qu'il ne faut pas faire, et le moment d'expliquer pourquoi une règle retirée ne laisse aucune trace tandis qu'une exception datée en laisse une. - Le premier
pre-commit runtélécharge et installe les environnements : deux à quatre minutes sans affichage. Ne pas interrompre. pre-commit installoublié : les hooks ne se déclenchent pas au commit, seulement en lancement manuel. Symptôme : « ça passe » alors que rien ne tourne.
Questions à anticiper
- « Si
--no-verifyexiste, à quoi bon ? » — la bonne question, et elle appelle la réponse du jour 3 : le hook aide celui qui veut bien être aidé, le pipeline arrête celui qui ne veut pas. Ne pas botter en touche, c'est le pivot du module. - « Pourquoi Bandit alors qu'on a Semgrep ? » — Bandit est spécialisé Python et très rapide, ce qui convient à un hook ; Semgrep est plus large et vit en CI. Le M2 approfondit.
- « Combien de temps une exception peut-elle durer ? » — jusqu'à son échéance écrite. Sans échéance, ce n'est pas une exception, c'est un abandon.
Ce qu'il faut exiger — un commit effectivement bloqué devant vous, et le tableau des exceptions dans le README avec une échéance datée pour chaque ligne. S'il n'y a pas d'échéance, la ligne ne compte pas.
Le secret est déjà dans l'historique : la procédure
Jour 2 · Git de confiance et garde-fous locaux · 25 min
Vous savez maintenant que votre historique contient des secrets, et vous avez posé le garde-fou qui empêchera d'en ajouter. Reste le passé. Cette leçon décrit l'opération, ses effets de bord, et la seule manière de vérifier qu'elle a fonctionné.
Rappel de l'ordre — parce que c'est là qu'on se trompe
Entre le moment où le secret a été poussé et celui où vous réécrivez, il a été lisible. Qui l'a lu ? Vous ne le saurez jamais. Un dépôt peut avoir été cloné, indexé par un robot, sauvegardé par un système de sauvegarde, dupliqué dans un fork.
Un secret exposé est un secret compromis. La réécriture réduit la surface pour l'avenir ; elle ne restaure rien. Si vous n'avez pas révoqué, vous n'avez rien fait.
Les outils, et lequel choisir
| Outil | Ce qu'il vaut | Verdict |
|---|---|---|
git filter-repo | rapide, sûr, maintenu ; c'est l'outil recommandé par le projet Git lui-même ; s'installe par pipx ou par le gestionnaire de paquets | notre choix |
| BFG Repo-Cleaner | très rapide sur les gros dépôts, syntaxe simple ; demande une machine virtuelle Java | acceptable, second choix |
git filter-branch | intégré à Git, mais lent, difficile à écrire correctement, et Git affiche lui-même un avertissement invitant à ne plus l'employer | à éviter |
| Supprimer et recréer le dépôt | radical et efficace sur un dépôt de quelques jours ; perd tout l'historique et toutes les pull requests | cas particulier |
Ce que la réécriture casse — à savoir avant, pas après
- Toutes les empreintes changent en aval du premier commit modifié. Un commit est identifié par un hachage de son contenu et de son parent : modifiez le premier, et toute la chaîne se renumérote.
- Les clones existants deviennent incompatibles. Chaque personne ayant cloné
doit recloner. Un simple
git pullproduirait une fusion monstrueuse qui réintroduirait les anciens commits — donc le secret. - Les pull requests ouvertes se cassent, les branches non traitées pointent encore sur l'ancien historique.
- Les anciens objets survivent sur le serveur. GitHub conserve les commits devenus orphelins et les rend accessibles par leur empreinte, parfois longtemps ; et un fork en garde une copie que vous ne contrôlez pas. Sur un incident réel, on demande explicitement au support de purger.
- La poussée forcée est refusée par une branche protégée — ce qui est le comportement voulu. Il faut lever la protection, forcer, la remettre, et écrire dans le journal qui a fait quoi et quand. C'est une procédure d'exception, elle se trace.
La séquence complète
- Révoquer le secret chez son fournisseur. Toujours en premier.
- Sauvegarder le dépôt avant de le réécrire :
git clone --mirrordans un dossier à part. Une réécriture ratée sans sauvegarde, c'est un dépôt perdu. - Corriger le code pour que le secret ne soit plus nécessaire : lecture
depuis l'environnement, valeur documentée dans
.env.exemple. - Réécrire avec
git filter-repo --replace-text, qui remplace chaque occurrence par un marqueur dans tous les commits. - Vérifier, et c'est l'étape que l'on saute :
git log --all -p -S'<fragment>'doit ne rien renvoyer, et un Gitleaks sur l'historique complet doit sortir en 0. - Republier en poussée forcée, protection temporairement levée, puis remise.
- Prévenir : avertir ceux qui ont cloné, et consigner l'incident.
Par sécurité, git filter-repo supprime le dépôt distant origin de votre configuration après avoir réécrit l'historique. C'est délibéré : l'outil veut vous empêcher de pousser par réflexe un historique réécrit sans y avoir réfléchi.
Résultat : votre git push répondra 'origin' does not appear to be a git repository. Ce n'est pas une panne. Il faut refaire git remote add origin …, en connaissance de cause.
Avant de lire cette leçon, faites une sauvegarde mentale de la règle : on révoque d'abord. Tout ce qui suit décrit le nettoyage, et le nettoyage vient en troisième position. Si vous ne devez retenir qu'une chose de la journée, c'est l'ordre — pas la commande.
Et une précision qui surprend toujours : réécrire l'historique ne fait pas disparaître les anciens objets du serveur distant. Ils restent atteignables par leur empreinte, parfois pendant des semaines, et dans les forks pour toujours.
Intention — donner la procédure complète et ses effets de bord avant de la faire exécuter. Une réécriture d'historique est une opération destructive sur un objet partagé ; le TP qui suit doit être fait avec la conscience du risque, pas comme une recette.
Ce qui coince
- L'idée que la réécriture « nettoie » le distant. Elle change les références ; les objets orphelins restent accessibles par empreinte jusqu'au ramassage, et indéfiniment dans un fork. C'est le point qui justifie à lui seul la rotation.
- La sous-estimation de l'impact sur les autres. Toutes les empreintes changent en aval du commit touché : les clones existants deviennent incompatibles, les pull requests ouvertes cassent. Sur un dépôt d'équipe, cela s'annonce.
git filter-branchencore enseigné ici et là : lent, plein de pièges, et déconseillé par Git lui-même. Le nommer pour qu'il le reconnaisse, et l'écarter.
Questions à anticiper
- « Et si je supprime le dépôt et que j'en recrée un ? » — solution radicale, parfois la bonne sur un dépôt jeune ; elle perd tout l'historique et ne dispense toujours pas de révoquer. Réponse à donner franchement.
- «
--force-with-leaseou--force? » — toujours le premier : il refuse si le distant a bougé depuis votre dernière synchronisation. Un réflexe à installer maintenant. - « Comment savoir si quelqu'un a cloné ? » — on ne le sait pas. C'est l'argument final en faveur de la rotation.
Ce qu'il faut exiger — qu'il sache citer deux effets de bord de la réécriture avant de la lancer, et qu'il ait fait la sauvegarde miroir. Le TP suivant commence par elle.
TP 8 — Rotation, réécriture de l'historique, vérification
Jour 2 · Git de confiance et garde-fous locaux · 55 min
Fermer VULN-05 pour de bon : dans le code, dans l'historique, et dans le journal.
Ce TP réécrit l'historique de votre dépôt. Fait correctement, il ne pose aucun problème. Fait sans sauvegarde et interrompu au mauvais moment, il vous coûte deux jours de travail. L'étape 2 n'est pas facultative.
- Temps n° 1 — révoquer. Nos deux secrets sont factices : l'identifiant AWS est celui de la documentation publique, la clé Flask n'a jamais servi. Il n'y a donc rien à révoquer, et c'est justement pourquoi il faut écrire ce que l'on aurait fait. Consignez-le dans le journal avant de toucher à quoi que ce soit.
# A ajouter a l'entree INC-01 de journal-incidents.md : **Temps 1 — Rotation (2026-08-04).** Secrets factices, sans acces reel : aucune revocation possible ni necessaire. En situation reelle, la sequence aurait ete : 1. console AWS -> IAM -> desactiver puis supprimer la cle d'acces ; 2. emettre une nouvelle cle, la deposer dans le gestionnaire de secrets ; 3. relire les journaux d'acces CloudTrail sur la periode d'exposition ; 4. SEULEMENT ENSUITE, reecrire l'historique. - Sauvegardez. Un miroir complet, à côté du dépôt. Si la réécriture tourne mal, c'est votre seule issue.
cd ~/poei-s4/devsecops-lab git clone --mirror . ../devsecops-lab-avant-reecriture.git ls -d ../devsecops-lab-avant-reecriture.git # doit exister - Temps n° 3 — corriger le code. Sur une branche dédiée, faites lire les deux valeurs depuis l'environnement. Remarquez que
SECRET_KEYn'a plus de valeur par défaut utilisable : une application qui démarre sans secret configuré doit s'arrêter, pas se rabattre sur une valeur connue.git switch -c fix/vuln-05-secret # Dans app.py, remplacez les deux lignes de VULN-05 par : # VULN-05 corrigee : les secrets viennent de l'environnement, jamais du code. app.config["SECRET_KEY"] = os.environ["APP_SECRET_KEY"] API_KEY = os.environ.get("APP_API_KEY", "") - Vérifiez que l'application démarre avec l'environnement, et qu'elle refuse de démarrer sans. Le second essai est le plus important : il prouve qu'il ne reste aucune valeur de repli dans le code.
source .venv/bin/activate # Avec l'environnement : demarre normalement. set -a && source .env && set +a python app.py & sleep 2 && curl -s http://localhost:5000/ && echo kill %1 # Sans l'environnement : doit echouer sur KeyError APP_SECRET_KEY. env -u APP_SECRET_KEY python app.py - Committez la correction. Le message porte l'identifiant de la vulnérabilité — c'est la convention qui permettra au jury de relier une ligne de journal à un commit.
git add app.py git commit -m "fix(VULN-05): lire les secrets depuis l'environnement" git switch main git merge --no-ff fix/vuln-05-secret -m "merge: fix(VULN-05)" - Temps n° 4 — réécrire. Installez l'outil, puis écrivez le fichier de remplacements : une ligne par secret à purger de tous les commits.
pipx install git-filter-repo # ou : brew install git-filter-repo git filter-repo --version # Creez ../replacements.txt — HORS du depot, pour ne pas le committer : cat > ../replacements.txt <<'FIN' AKIAIOSFODNN7EXAMPLE s3cr3t-dev-key-ne-pas-committer-42 FIN - Lancez la réécriture. Chaque occurrence est remplacée par
***REMOVED***dans tous les commits de toutes les branches.git filter-repo --replace-text ../replacements.txt --force # Regardez le graphe : les empreintes ont toutes change. git log --oneline --all - Temps n° 5 — vérifier. Deux commandes, et ni l'une ni l'autre ne regarde le fichier courant. La première fouille tout l'historique à la recherche du fragment ; la seconde relance le détecteur sur l'ensemble des commits.
# 1. Aucune occurrence dans aucun commit — la sortie doit etre VIDE. git log --all -p -S 'AKIAIOSFODNN7EXAMPLE' | head git log --all -p -S 's3cr3t-dev-key' | head # 2. Gitleaks sur l'historique complet — code de sortie attendu : 0. docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \ detect --source=/repo --config=/repo/.gitleaks.toml -v echo "code de sortie : $?" - Remettez le dépôt distant :
filter-repol'a retiré exprès. Ce n'est pas une panne, c'est un garde-fou.git remote -v # vide : c'est normal git remote add origin git@github.com:<votre-compte>/devsecops-lab.git git remote -v - Temps n° 6 — republier. Votre poussée forcée sera refusée par la protection posée ce matin, et c'est la preuve qu'elle fonctionne. Procédure d'exception : sur GitHub, Settings → Branches, désactivez temporairement la règle, poussez, puis réactivez-la immédiatement.
--force-with-leaseplutôt que--force: il refuse si le distant a bougé depuis votre dernière synchronisation.git push --force-with-lease origin main # Si refuse : levez la protection sur GitHub, relancez, puis REMETTEZ-LA. # Verifiez ensuite dans l'interface que la regle est bien active. - Temps n° 7 — consigner. Complétez INC-01 : la réécriture, sa vérification, et la levée temporaire de protection. Une procédure d'exception qui n'est pas tracée est indiscernable d'un contournement.
**Temps 4-6 — Reecriture (2026-08-04, 15h40-16h05).** Sauvegarde miroir : ../devsecops-lab-avant-reecriture.git Outil : git filter-repo --replace-text (2 motifs). Verification : `git log --all -p -S` vide sur les deux motifs ; gitleaks sur l'historique complet en code 0. Protection de main levee de 16h02 a 16h05 par E. Zaraket pour la poussee forcee, puis retablie et verifiee dans l'interface. Aucun clone tiers a prevenir (depot prive, aucun collaborateur). **Statut VULN-05 : FERMEE** — code corrige, historique purge, prevention en place (hook pre-commit ; le pipeline prendra le relais au J3). - Committez le journal, signé. VULN-05 est fermée : dans le code, dans l'historique, et dans la trace. C'est la première des dix, et la seule qui aura demandé une opération sur le passé.
- Vous avez vérifié par deux commandes différentes. Qu'est-ce que la seconde apporte que la première ne donnait pas ?
- La protection de branche a refusé votre poussée légitime. Était-ce un obstacle ou le comportement attendu — et qu'est-ce qui, dans votre procédure, fait la différence ?
- Si ce dépôt avait été public pendant deux jours, qu'auriez-vous dû faire de plus, et qu'est-ce qui aurait été définitivement hors de votre contrôle ?
Ne sautez pas l'étape de sauvegarde. Une réécriture ratée sans miroir, c'est un dépôt perdu — et le vôtre porte deux jours de travail.
Deux surprises vous attendent, toutes deux normales : git filter-repo supprime votre origin après coup, et votre poussée forcée sera refusée par la protection de branche que vous avez posée ce matin. Les deux sont des sécurités qui fonctionnent, pas des pannes. Les étapes le prévoient.
Intention — faire exécuter la procédure complète, dans l'ordre, sur un cas réel qui est le sien. C'est le TP le plus délicat du module : opération destructive, deux effets de bord déroutants, et une vérification que l'on est tenté de bâcler parce qu'on croit avoir fini.
Ce qui coince
- La sauvegarde sautée. S'il rate le
filter-reposans miroir, on perd deux jours. Vérifier que../devsecops-lab-avant-reecriture.gitexiste AVANT de le laisser continuer — c'est le seul point du parcours où il faut intervenir en amont plutôt qu'en aval. origindisparu : il conclut qu'il a cassé son dépôt. Panique typique, réponse en dix secondes si on l'a lu en leçon.- La poussée forcée refusée : il désactive la protection et oublie de la réactiver. C'est le vrai risque professionnel de la séquence, et un critère du point de contrôle du soir.
- La vérification remplacée par un coup d'œil au fichier courant. Exiger les deux commandes : le
-Ssur tout l'historique et le Gitleaks complet.
Questions à anticiper
- « Le remplacement laisse
***REMOVED***dans les vieux commits, ce n'est pas sale ? » — non, c'est la trace assumée qu'une réécriture a eu lieu. Effacer la trace de l'effacement n'apporte rien et complique l'audit. - « Est-ce que je peux réécrire seulement le dernier commit ? » — s'il est le seul concerné,
--amendsuffit. Ici le secret vit dans le commit initial, donc toute la chaîne est touchée. Bonne occasion de faire lire le graphe. - « Pourquoi Flask a-t-il besoin d'un SECRET_KEY ? » — signature des cookies de session ; un SECRET_KEY connu permet de forger une session. Répondre brièvement, c'est un vrai motif.
Ce qu'il faut exiger — trois preuves, dans cet ordre : git log --all -p -S 'AKIAIOSFODNN7EXAMPLE' vide, Gitleaks sur l'historique complet en code 0, et la protection de branche réactivée, constatée dans l'interface. Le troisième point est celui qu'on oublie.
TP 9 — Rédiger INC-01 et la section « Secrets » du README
Jour 2 · Git de confiance et garde-fous locaux · 45 min
Produire les livrables écrits que le point de contrôle du soir va vérifier.
- Relisez votre
journal-incidents.mden vous posant une seule question : quelqu'un qui n'était pas là comprend-il ce qui s'est passé et pourrait-il le rejouer ? Complétez ce qui manque. Un incident se relit un an plus tard, par une autre personne, dans un contexte oublié. - Vérifiez que INC-01 contient les six temps, chacun avec sa trace. Voici la structure attendue — comparez-la à ce que vous avez écrit.
### 2026-08-04 — INC-01 : deux secrets en dur dans l'historique (VULN-05) **Constat** quoi, trouve comment, quand, par quelle commande **Exposition** depuis quand, depot public ou prive, clones connus, usage constate **Temps 1 — Rotation** ce qui a ete revoque, ou ce qui l'aurait ete **Temps 4 — Reecriture** outil, motifs, sauvegarde, horaires **Temps 5 — Verification** les DEUX commandes et leurs resultats **Temps 7 — Prevention** hook en place, pipeline a venir, exceptions tracees **Statut** FERMEE / OUVERTE / ACCEPTEE avec echeance - Rédigez la section 3 du README, « Secrets et configuration ». Elle doit permettre à quelqu'un de faire tourner votre application sans jamais vous demander une valeur.
## 3. Secrets et configuration Aucun secret ne figure dans le code ni dans l'historique Git. ### Variables attendues | Variable | Role | Obligatoire | Ou la trouver | |----------|------|-------------|---------------| | APP_SECRET_KEY | signature des sessions Flask | oui, l'appli refuse de demarrer sans | generee localement | | APP_API_KEY | identifiant fournisseur (factice ici) | non | .env local | ### Mise en route cp .env.exemple .env # puis renseignez les valeurs set -a && source .env && set +a python app.py ### Verifier qu'aucun secret n'est versionne git ls-files | grep -E '\.env$' # doit etre vide docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \ detect --source=/repo --config=/repo/.gitleaks.toml # code de sortie attendu : 0 - Rédigez la section 4, « Garde-fous locaux ». Décrivez ce que fait chaque hook, et surtout ce qu'il ne fait pas — c'est la partie que personne n'écrit et la seule qui évite les mauvaises surprises.
## 4. Garde-fous locaux et pipeline ### Hooks pre-commit (module 1, jour 2) | Hook | Ce qu'il verifie | Ce qu'il ne verifie pas | |------|------------------|-------------------------| | gitleaks | secrets dans les fichiers mis en scene | l'historique deja pousse | | bandit | motifs Python dangereux | la logique metier, l'appli en marche | | detect-private-key | cles privees | les cles publiques mal placees | | check-added-large-files | fichiers > 512 Ko | ce qui est deja versionne | **Limite structurelle.** Ces hooks vivent dans .git/hooks/, qui n'est pas versionne : un clone ne les recoit pas, et `git commit --no-verify` les contourne. Ils aident, ils ne garantissent rien. Le verrou reel est le pipeline (module 1, jour 3). ### Pipeline _(module 1, jour 3)_ - Vérifiez que votre section 5, le tableau des exceptions, porte une échéance datée sur chaque ligne. Relisez-la avec la question du jury : « qui a décidé ça, quand, et jusqu'à quand ? ». Si une ligne ne répond pas aux trois, complétez-la.
- Répondez par écrit aux questions de réflexion des TP 5 à 8 dans
reponses-reflexion.md. C'est le document dont votre encadrant tirera ses questions au point du soir — pas de vos scripts. Deux réponses développées valent mieux que huit expédiées. - Committez l'ensemble, signé, et poussez par une pull request : votre branche
mainest protégée, la poussée directe est refusée. C'est le premier aller-retour complet de votre chaîne, et vous le referez demain avec un pipeline au bout.git switch -c docs/journal-et-readme-j2 git add journal-incidents.md README.md reponses-reflexion.md git commit -m "docs: INC-01, sections secrets et garde-fous du README" git push -u origin docs/journal-et-readme-j2 # Puis ouvrez la pull request sur GitHub et fusionnez-la. git switch main && git pull
- Votre journal décrit une opération que vous venez de faire. Qu'est-ce qui, dans votre rédaction, permettrait à un tiers de la rejouer à l'identique ?
- Vous avez écrit ce que les hooks ne vérifient pas. En quoi cette section est-elle plus utile que la liste de ce qu'ils vérifient ?
Intention — la rédaction est du travail, pas une formalité de fin de journée. Ce TP existe pour qu'elle soit faite pendant le temps de formation, avec les commandes encore fraîches, et non expédiée à 17 h 25. Ses étapes sont exactement les critères du point de contrôle qui suit : il n'y a aucune surprise, et c'est voulu.
Ce qui coince
- Le journal rédigé au passé vague — « j'ai nettoyé l'historique ». Exiger les horaires, les commandes, les codes de sortie. Un journal d'incidents se relit un an après par quelqu'un d'autre.
- La section README recopiée depuis le cours plutôt que depuis son propre dépôt. Signe : elle mentionne des variables qui ne sont pas dans son
.env.exemple. - Le tableau des exceptions sans échéance, ou avec une échéance « plus tard ». C'est le défaut le plus fréquent et le plus grave : sans date, l'exception est un abandon.
Questions à anticiper
- « Pour qui j'écris ? » — pour vous dans six mois, et pour le jury du 21 août. Ni l'un ni l'autre n'aura le contexte en tête. Réponse à donner telle quelle.
- « Faut-il détailler les commandes exactes ? » — oui : un incident se rejoue. C'est la même exigence que la preuve rejouable du rapport de pentest du M5, et autant l'installer maintenant.
Ce qu'il faut exiger — que INC-01 soit lisible par quelqu'un qui n'était pas là : constat, exposition, rotation, réécriture, vérification, prévention, avec les horaires. Et le tableau des exceptions daté ligne par ligne.
M1 · J2 — Commit signé et secret bloqué au pre-commit
Jour 2 · Git de confiance et garde-fous locaux · 30 min
Votre encadrant vous fera faire trois gestes : tenter le commit interdit, fouiller votre historique, et ouvrir la page de protection de branche sur GitHub. Ce dernier point est celui qu'on oublie après une poussée forcée — vérifiez-le avant le point.
Il vous demandera aussi l'ordre des quatre temps, page fermée. Si vous commencez par « réécrire », la journée est à reprendre.
Intention — valider que la chaîne de confiance locale tient et que VULN-05 est fermée partout : code, historique, prévention, trace. Trente minutes, point quotidien de 16 h 30.
Ce qu'il faut exiger — quatre gestes, faits par lui devant vous :
- Il tente
git add -f .env && git commit: le hook refuse, etgit log --oneline -1montre que le commit n'existe pas. Le second contrôle est essentiel — sans lui, on croit sur parole. git log --all -p -S 'AKIAIOSFODNN7EXAMPLE': vide. Puis Gitleaks sur l'historique complet, code 0.- Sur GitHub : le badge Verified sur ses commits, et la règle de protection active dans l'interface. Ce dernier point est celui qu'on oublie après la poussée forcée — le vérifier soi-même à l'écran.
- Il énonce l'ordre des quatre temps sans relire, et dit pourquoi la rotation vient en premier.
Ce qui coince
- La protection de branche laissée désactivée depuis le TP 8. Défaut n° 1 de la journée, et le plus significatif : c'est le geste d'exception non refermé.
- Le tableau des exceptions sans échéance. Refuser la ligne, pas le TP.
- « C'est bon, j'ai supprimé la ligne » : le modèle mental de Git n'est pas acquis. Le faire vérifier par la commande, jamais par la lecture du fichier.
- Le badge Verified absent parce que la clé est dans la mauvaise section GitHub. Diagnostic en dix secondes une fois qu'on le connaît.
Questions à anticiper
- « Pourquoi refaire demain ce que le hook fait déjà ? » — parce qu'il a lui-même démontré
--no-verifycet après-midi. Le lui rappeler, c'est l'accroche parfaite pour la journée de mercredi. - « Les quatre exceptions Bandit, ça ne fait pas beaucoup ? » — si, et c'est assumé et daté. La question à lui retourner : que se passe-t-il le 10 août si elles sont toujours là ?
Ne pas valider si — la protection de main est restée levée, ou si la vérification de l'historique n'a pas été faite par commande devant vous. Le premier cas est une faute de procédure, le second un défaut de méthode : les deux se reproduiront en situation réelle.
Le pipeline qui refuse la faille
Jour 3 · Le pipeline qui refuse la faille · 30 min
Hier, vous avez installé un garde-fou local, puis vous l'avez contourné vous-même en ajoutant sept caractères à votre commande. C'était l'objet de la démonstration : un hook aide celui qui veut bien être aidé. Aujourd'hui, on place le contrôle là où votre poste ne peut plus l'atteindre.
La chaîne de décision, en trois maillons
Un pipeline de sécurité n'empêche rien par lui-même. Ce qui empêche, c'est une chaîne de trois maillons — et il suffit qu'un seul manque pour que tout le dispositif devienne décoratif.
- L'outil sort en erreur. Semgrep avec
--error, Gitleaks qui trouve un secret, Trivy avec--exit-code 1. Sans cela, le job réussit et le rapport part à la corbeille. - Le job échoue donc, et il est déclaré obligatoire. Un job qui échoue mais n'est pas dans la liste des vérifications requises ne bloque rien : la pull request affiche une croix rouge que l'on peut fusionner par-dessus.
- La branche est protégée, et la protection s'applique à tout le monde, y compris au propriétaire du dépôt. Sans la case « ne pas autoriser le contournement », le verrou s'ouvre de l'intérieur.
Introduisez volontairement une faille et ouvrez une pull request. Si vous parvenez à la fusionner, votre pipeline est un système d'information, pas un dispositif de sécurité.
C'est exactement ce que vous allez faire dans une heure, et dans cet ordre : d'abord prouver que ça bloque, ensuite corriger. Un pipeline que l'on n'a jamais vu échouer n'a jamais été éprouvé.
Le workflow de départ
Voici celui que vous allez poser. Deux jobs, indépendants, exécutés en parallèle : l'un cherche des secrets, l'autre analyse le code. Il grandira aux modules suivants — le module 2 y ajoutera l'analyse de composition et le DAST, le module 3 le scan d'image.
name: ci-securite
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
permissions:
contents: read
security-events: write
jobs:
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: gitleaks
run: |
docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:v8.18.4 \
detect --source=/repo --no-git -v --report-path=/repo/gitleaks.json
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: semgrep
run: |
docker run --rm -v "$PWD:/src" semgrep/semgrep:1.85.0 \
semgrep scan --config p/python --config p/flask --config p/security-audit \
--error --sarif --output /src/semgrep.sarif /src
Ce qu'il faut savoir lire dans ces vingt-cinq lignes
| Ligne | Ce qu'elle fait | Pourquoi elle est là |
|---|---|---|
on: pull_request | déclenche le pipeline sur chaque pull request visant main | c'est le moment où le contrôle est utile : avant la fusion, pas après |
permissions: contents: read | restreint le jeton fourni au job | par défaut le jeton peut écrire ; un script compromis dans une dépendance en profiterait |
fetch-depth: 0 | récupère tout l'historique et non le dernier commit | sans lui, aucune analyse du passé n'est possible — voir l'encadré ci-dessous |
docker run … zricethezav/gitleaks:v8.18.4 | exécute l'outil en conteneur épinglé | la commande est rigoureusement celle de votre poste : ce qui échoue ici se rejoue là-bas |
--error | fait sortir Semgrep en code non nul dès qu'une règle correspond | c'est le verrou — sans ce mot, tout le reste est de la documentation |
--sarif --output | produit un rapport au format standard | un format commun à tous les outils, que la forge sait afficher |
Le job secrets demande tout l'historique par fetch-depth: 0, puis lance Gitleaks avec --no-git — qui, précisément, ignore l'historique et ne regarde que les fichiers présents.
Ce n'est pas une erreur : analyser tout l'historique à chaque pull request coûte cher, et vous l'avez déjà purgé hier. Mais le terrain est préparé : le jour où vous retirerez --no-git, l'historique sera là. Repérer ce genre de détail dans un workflow écrit par quelqu'un d'autre est exactement ce qu'on attend d'un ingénieur sécurité.
La sécurité du pipeline lui-même
Un workflow exécute du code que vous n'avez pas écrit, avec un jeton d'accès à votre dépôt. C'est une cible, et elle a été attaquée en conditions réelles à plusieurs reprises ces dernières années.
- Le jeton au moindre privilège.
permissions:en tête du workflow réduit ce qu'un script compromis peut faire. Sans cette déclaration, le jeton par défaut peut souvent écrire dans le dépôt. - L'épinglage des actions.
actions/checkout@v4désigne une étiquette, que le mainteneur peut déplacer vers un autre commit. L'épinglage strict se fait par empreinte complète —actions/checkout@<40 caractères>— ce qui fige exactement le code exécuté. - Les secrets ne vivent jamais dans le fichier. Ils vont dans le magasin de secrets de la forge et sont injectés à l'exécution. La forge masque leur valeur dans les journaux — mais seulement si elle la reconnaît : une clé transformée, découpée ou encodée ressort en clair.
- Attention aux déclencheurs sur les contributions externes. Un workflow déclenché par une pull request venue d'un fork, s'il s'exécute avec les secrets du dépôt, donne ces secrets à quiconque ouvre une pull request. C'est la mauvaise configuration la plus classique du domaine.
Adopter un verrou sur un dépôt qui existe déjà
Votre dépôt a trois jours et dix failles connues. Un dépôt d'entreprise en a quinze ans et trois mille findings. Y activer un verrou absolu bloquerait toutes les équipes le premier matin, et le verrou serait désactivé avant midi. La pratique professionnelle est la ligne de base :
- on mesure l'existant, et on l'enregistre comme référence acceptée ;
- le verrou ne bloque que sur les nouveaux écarts — le code touché par la pull request en cours ;
- la dette de référence est traitée par lots, avec des échéances, exactement comme votre tableau d'exceptions d'hier ;
- la ligne de base ne peut que descendre : c'est la règle qui empêche le dispositif de devenir cosmétique.
Vous appliquerez ce principe cet après-midi à votre échelle : votre pipeline passera au vert avec quatre failles encore ouvertes — datées, justifiées, assignées au module 2. Ce n'est pas un compromis honteux, c'est la seule façon connue de faire vivre un verrou.
Le même fichier ci.yml s'exécute sur votre poste, sans GitHub, grâce à act, qui rejoue les jobs dans Docker : act -j sast. C'est le repli hors ligne du parcours — vous ne serez jamais bloqué par une coupure ou par un quota. La première exécution télécharge une image de runner assez lourde : lancez-la pendant une pause.
Intention — passer du confort au verrou. Hier il a démontré lui-même que son hook se contourne d'une option ; aujourd'hui on met le contrôle là où il ne peut plus l'atteindre. La leçon doit tenir quatre notions : le code de sortie comme verrou, le moindre privilège du jeton, l'épinglage des actions, et la ligne de base qui rend l'adoption possible sur un dépôt existant.
Ce qui coince
- Le pipeline vu comme « l'endroit où l'on met les scanners ». Il faut qu'il voie la chaîne de décision : job en échec → vérification obligatoire en échec → fusion interdite. Trois maillons, et chacun peut manquer.
- La sécurité du pipeline confondue avec la sécurité par le pipeline. Un workflow qui exécute une action non épinglée sur un dépôt tiers est lui-même une surface d'attaque — c'est le sujet des attaques de chaîne d'approvisionnement, et il vaut deux minutes ici.
- « Zéro faille ou rien ». Sur un dépôt existant, un verrou absolu est ingérable dès le premier jour ; la ligne de base est la réponse professionnelle et il faut la donner, sinon il conclura que le DevSecOps est inapplicable en vrai.
Questions à anticiper
- « Pourquoi Docker dans le job plutôt que l'action officielle ? » — pour que la commande soit rigoureusement la même sur son poste et sur le runner. C'est un choix assumé du parcours, et il rend le débogage possible.
- «
@v4n'est-il pas déjà une version ? » — c'est une étiquette mobile, que le mainteneur peut déplacer. L'épinglage strict se fait par empreinte. Donner le principe sans l'imposer aujourd'hui. - « Combien de temps un pipeline peut-il durer ? » — au-delà de dix minutes on cesse de l'attendre et on prend l'habitude de fusionner sans regarder. La durée est un paramètre de sécurité, ce qui surprend toujours.
Ce qu'il faut exiger — qu'il décrive la chaîne de décision en trois maillons et qu'il sache dire ce qui casserait chacun. C'est ce qui distingue quelqu'un qui configure un pipeline de quelqu'un qui le conçoit.
Quiz — Pipeline, verrous et exceptions
Jour 3 · Le pipeline qui refuse la faille · 10 min
1. Pourquoi déclarer permissions: contents: read en tête d'un workflow ?
2. Vous écrivez uses: actions/checkout@v4. Quel risque subsiste ?
3. Votre hook pre-commit refuse un commit. Que démontre git commit --no-verify ?
4. Semgrep signale une faille que vous ne corrigerez qu'au module suivant. Que faites-vous ?
5. Dans un diagramme de flux de données, où STRIDE demande-t-il de concentrer l'analyse ?
Intention — vérifier avant le TP les quatre points qui feront la différence entre un pipeline configuré et un pipeline compris : le moindre privilège (q1), l'épinglage (q2), la portée réelle du hook local (q3), la discipline d'exception (q4), et l'ancrage de STRIDE pour l'après-midi (q5).
Ce qui coince
- q3 est la question de contrôle du module entier. S'il choisit la troisième option, il n'a pas tiré la leçon de sa propre démonstration d'hier — reprendre avant le TP.
- q4 avec l'option « laisser le pipeline rouge » : réponse séduisante, moralement confortable, et opérationnellement désastreuse. C'est le débat le plus utile du quiz ; un pipeline rouge en permanence cesse d'être lu en trois jours.
- q2 ratée : l'idée qu'une étiquette de version est immuable. Fréquent, et à corriger avant le M2 qui ajoutera d'autres actions.
Questions à anticiper
- « La 4, ce n'est pas de la triche ? » — la question à retourner : qu'est-ce qui laisse une trace vérifiable dans six mois, l'exception datée ou la règle supprimée ? C'est là que la notion s'installe.
Ce qu'un échec révèle — 3/5 avec q3 et q4 justes : il a compris le dispositif, laissez partir. Les deux fausses : reprenez dix minutes, le TP 11 n'aura pas de sens.
TP 10 — Le premier pipeline, et il est rouge
Jour 3 · Le pipeline qui refuse la faille · 55 min
Poser le workflow de sécurité et le voir refuser une pull request — avant de chercher à le faire passer.
- Créez le workflow. Attention au chemin : deux niveaux, et le point initial compte. Une erreur ici ne produit aucun message.
cd ~/poei-s4/devsecops-lab git switch -c feat/pipeline-securite mkdir -p .github/workflows - Écrivez
.github/workflows/ci.yml. Recopiez-le tel quel : il grandira aux modules suivants, il ne se réinvente pas.name: ci-securite on: push: branches: [ main ] pull_request: branches: [ main ] permissions: contents: read security-events: write jobs: secrets: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: gitleaks run: | docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:v8.18.4 \ detect --source=/repo --no-git -v --report-path=/repo/gitleaks.json sast: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: semgrep run: | docker run --rm -v "$PWD:/src" semgrep/semgrep:1.85.0 \ semgrep scan --config p/python --config p/flask --config p/security-audit \ --error --sarif --output /src/semgrep.sarif /src - Ajoutez l'archivage des rapports. Le workflow tel quel écrit ses fichiers dans l'espace de travail du runner, qui est détruit à la fin du job : un artefact qu'on ne récupère pas n'existe pas. Insérez ces quatre lignes à la fin de chacun des deux jobs, au même niveau d'indentation que les autres étapes.
- name: archiver le rapport if: always() uses: actions/upload-artifact@v4 with: name: rapport-secrets # ou rapport-sast dans le second job path: gitleaks.json # ou semgrep.sarif dans le second job - Notez le
if: always(): sans lui, l'étape d'archivage serait sautée dès que le job échoue — c'est-à-dire précisément quand le rapport est intéressant. C'est une erreur de configuration extrêmement répandue. - Rejouez la démonstration d'hier : remettez un secret dans le code et committez-le en contournant le hook. Vous savez que votre garde-fou local ne peut pas l'empêcher ; l'objet du TP est de voir qui l'arrêtera.
# Dans app.py, ajoutez cette ligne apres les imports : # TOKEN_TEMPORAIRE = "ghp_bV3xQ9pLmN2rT8sK1wY6zC4hJ7dF0aE5uI9o" git add app.py .github/workflows/ci.yml git commit --no-verify -m "feat: pipeline de securite (avec un secret oublie)" # Le hook n'a rien dit. Le commit existe. git log --oneline -1 - Poussez la branche et ouvrez une pull request vers
main.git push -u origin feat/pipeline-securite # Puis, sur GitHub : Compare & pull request -> Create pull request - Ouvrez l'onglet Actions et regardez les deux jobs tourner en parallèle. Comptez trois à cinq minutes au premier passage : Semgrep télécharge ses jeux de règles. Si l'onglet reste désespérément vide, votre fichier n'est pas au bon endroit — revenez à l'étape 1.
- Les deux jobs échouent. C'est le résultat attendu. Ouvrez le journal de chacun et répondez par écrit : quelle règle a mordu, sur quel fichier, à quelle ligne ? Pour
secrets, vous devez retrouver le jeton que vous venez d'introduire. Poursast, faites l'inventaire complet — vous en aurez besoin dans une heure. - Revenez sur la pull request et regardez le bouton de fusion : il est indisponible. Essayez quand même. C'est ce que votre hook local ne pouvait pas faire, et c'est toute la différence entre un confort et un verrou.
- Récupérez les artefacts en bas de la page du workflow — section Artifacts — et rangez-les. Le SARIF vous servira au module 2 comme point de comparaison.
# Telechargez depuis l'interface, puis : mv ~/Downloads/rapport-sast.zip ~/poei-s4/devsecops-lab/artefacts/ cd ~/poei-s4/devsecops-lab/artefacts && unzip -o rapport-sast.zip && cd - - Repli hors ligne. Le même fichier s'exécute sur votre poste sans GitHub. Lancez-le une fois pour savoir le faire le jour où le réseau tombera. La première exécution télécharge une image de runner volumineuse.
brew install act # ou : https://github.com/nektos/act act -j sast echo "code de sortie : $?"
- Votre hook local et votre pipeline exécutent le même Gitleaks, avec la même version. Pourquoi l'un a-t-il laissé passer ce que l'autre a arrêté ?
- Le workflow demande tout l'historique puis analyse les fichiers seuls. Dans quel cas faudrait-il retirer
--no-git, et qu'est-ce que cela coûterait ? - Un pipeline qui prend vingt minutes finit par ne plus être attendu. En quoi la durée d'un pipeline est-elle un paramètre de sécurité ?
L'objectif de ce TP est un échec. Si votre pipeline passe au vert du premier coup, quelque chose ne va pas : ou bien le workflow ne s'est pas déclenché, ou bien vous avez corrigé une faille par anticipation. Un pipeline qu'on n'a jamais vu échouer n'a jamais été éprouvé.
Le workflow doit vivre dans .github/workflows/ — deux niveaux, et le point initial compte. Une erreur de chemin ne produit aucun message : l'onglet Actions reste simplement vide, et l'on cherche du côté du contenu du fichier alors que le problème est son emplacement.
Intention — faire vivre le rouge avant le vert. C'est l'ordre qui porte toute la démonstration du module : il doit voir de ses yeux une pull request qu'il ne peut pas fusionner. Et il doit réintroduire lui-même un secret avec --no-verify, pour boucler la démonstration d'hier.
Ce qui coince
- Le chemin
.github/workflows/mal orthographié —workflowau singulier, ougithubsans point. Aucun message d'erreur, l'onglet Actions reste vide. Cinq minutes perdues et beaucoup de perplexité. - Le workflow poussé directement sur
main: refusé par la protection. C'est cohérent, mais il peut y voir un blocage du TP. Il faut passer par une branche. - Le job qui échoue pour une raison qui n'est pas celle attendue — quota, réseau, image indisponible. Lui apprendre à lire le journal du job avant de conclure : un échec n'est utile que si l'on sait de quoi il parle.
- L'impatience pendant les trois à cinq minutes du premier passage. Semgrep télécharge ses rulesets.
Questions à anticiper
- « Combien de findings Semgrep va-t-il remonter ? » — ne pas donner de chiffre : cela dépend des rulesets et de leur version. C'est justement ce qu'il doit constater lui-même, et c'est pourquoi le TP 11 lui fait construire ses exceptions à partir de sa sortie et non d'une liste fournie.
- « Le rapport SARIF, où est-il ? » — nulle part : le workflow l'écrit dans l'espace de travail du runner, qui disparaît. D'où l'étape d'archivage, et la formule à retenir : un artefact qu'on ne récupère pas n'existe pas.
- « Puis-je fusionner quand même ? » — le lui faire essayer. Le bouton grisé vaut toutes les explications.
Ce qu'il faut exiger — une capture ou une démonstration à l'écran de la pull request avec ses deux jobs en échec et le bouton de fusion indisponible. Et qu'il nomme, en lisant le journal, quelle règle a mordu sur quelle ligne.
TP 11 — Du rouge au vert, sans rien cacher
Jour 3 · Le pipeline qui refuse la faille · 55 min
Corriger VULN-01 et VULN-05, tracer les quatre exceptions restantes, et rendre les deux vérifications obligatoires.
- D'abord le secret, puisqu'il est le plus grave et le plus simple. Retirez la ligne que vous avez introduite au TP précédent, et vérifiez localement avant de pousser quoi que ce soit.
# Supprimez la ligne TOKEN_TEMPORAIRE d'app.py, puis : docker run --rm -v "$(pwd)":/repo zricethezav/gitleaks:v8.18.4 \ detect --source=/repo --no-git --config=/repo/.gitleaks.toml -v echo "code de sortie : $?" # attendu : 0 - VULN-01 : la requête paramétrée. Le pilote de base de données reçoit désormais la requête et les valeurs séparément : il n'y a plus de chaîne à interpréter, donc plus rien à injecter. C'est une séparation structurelle, pas un filtrage — et c'est pour cela qu'elle est complète, là où échapper les apostrophes à la main serait une liste noire toujours incomplète.
# Dans app.py, remplacez le corps de la route /login : @app.route("/login", methods=["POST"]) def login(): # VULN-01 corrigee : requete parametree, les valeurs ne sont plus concatenees. user = request.form.get("user", "") pwd = request.form.get("pwd", "") row = db().execute( "SELECT * FROM users WHERE user=? AND pwd=?", (user, pwd) ).fetchone() return ("bienvenue" if row else "refuse"), (200 if row else 401) - Prouvez la correction par le rejeu, pas par le silence de l'outil. Trois essais : l'authentification légitime marche toujours, le mauvais mot de passe échoue, et la charge d'injection échoue désormais aussi. Les trois comptent.
set -a && source .env && set +a python app.py & sleep 2 # 1. Legitime — attendu : bienvenue [200] curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \ --data "user=admin&pwd=admin123" # 2. Mauvais mot de passe — attendu : refuse [401] curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \ --data "user=admin&pwd=faux" # 3. Injection — attendu DESORMAIS : refuse [401] curl -s -w ' [%{http_code}]\n' -X POST http://localhost:5000/login \ --data "user=admin'--&pwd=x" kill %1 - Notez le premier essai : il est le plus important des trois. Une correction qui casse l'authentification légitime n'est pas une correction, c'est une régression — et c'est l'erreur la plus fréquente quand on corrige une injection sous pression.
- Les quatre failles restantes. Elles appartiennent au module 2, avec une échéance inscrite hier. Ouvrez votre rapport de pipeline et relevez les identifiants de règle exacts que votre exécution a produits — ils dépendent de la version des jeux de règles, et une exception qui ne correspond à rien ne se voit jamais.
# Depuis votre SARIF telecharge au TP precedent : python3 -c "import json;d=json.load(open('artefacts/semgrep.sarif'));\ [print(r['ruleId'],'->',r['locations'][0]['physicalLocation']['region']['startLine']) \ for r in d['runs'][0]['results']]" - Écrivez les exceptions dans le code, sous leur forme nommée. Un
nosemgrepnu désactiverait toutes les règles sur la ligne, y compris celles qui n'existent pas encore : c'est un blanc-seing, pas une exception. Remplacez les identifiants par ceux de votre propre rapport.# Forme a utiliser, une ligne au-dessus du code concerne : # nosemgrep: <identifiant-exact-de-votre-rapport> # VULN-02 — exception acceptee le 2026-08-05, echeance module 2 (2026-08-10). # Faille pedagogique deliberee. Suivi : README section 5, journal-incidents. return render_template_string("<h1>Resultats pour " + terme + "</h1>") # Faites de meme pour VULN-03 (/ping), VULN-06 (/hash), VULN-07 (/config). - Mettez à jour le tableau des exceptions du README — il porte désormais les règles Semgrep en plus des règles Bandit — puis committez et poussez sur votre branche. Sans
--no-verifycette fois : le hook doit passer.git add app.py README.md journal-incidents.md git commit -m "fix(VULN-01): requete SQL parametree ; exceptions M2 tracees" git push - Retournez sur la pull request. Les deux jobs doivent être verts. S'il reste un échec sur
sast, c'est presque toujours qu'un identifiant de règle ne correspond pas exactement à celui de votre rapport — relisez le journal du job, il vous donne la ligne. - Fermez la chaîne. Les jobs passent, mais rien ne les rend encore obligatoires : une pull request en échec resterait fusionnable. Sur GitHub, Settings → Branches → main → Edit, cochez Require status checks to pass before merging et sélectionnez
secretsetsast. Ils apparaissent enfin dans la liste : c'est parce qu'ils ont tourné au moins une fois, exactement comme annoncé hier. - Fusionnez la pull request, puis vérifiez que
mainest verte et que votre historique est signé de bout en bout.git switch main && git pull git log --show-signature --oneline -5 git log --oneline | head - Votre pipeline est vert avec quatre failles ouvertes. Ce n'est pas une contradiction : vert signifie « aucune règle active n'a mordu », et quatre règles sont explicitement suspendues jusqu'au 10 août, nommément, dans le code et dans deux documents. C'est exactement ce que fait une équipe qui adopte un verrou sur du code existant — et c'est la différence entre une dette gérée et une dette cachée.
- Vous avez prouvé la correction par un rejeu plutôt que par le silence de Semgrep. Pourquoi le silence d'un outil ne prouve-t-il rien ?
- Les quatre exceptions expirent le 10 août. Que doit-il se passer ce jour-là, et qui s'en apercevra si rien n'est fait ?
- Vous avez coché les vérifications obligatoires après que les jobs sont passés au vert. Quel risque y aurait-il eu à les cocher pendant qu'ils échouaient ?
Ce TP se juge à une chose : ce que vous faites des failles que vous ne corrigez pas. Corriger l'injection SQL est facile. Ce qui est professionnel, c'est de traiter les quatre autres par une exception nommée, datée et signée plutôt que par une règle discrètement retirée. Le premier laisse une trace vérifiable, le second n'en laisse aucune.
N'écrivez pas les nosemgrep à partir de la liste d'un cours : utilisez votre sortie de pipeline, avec les identifiants de règle exacts que votre exécution a produits. Les identifiants varient selon la version des jeux de règles, et une exception qui ne correspond à rien ne se voit jamais.
Intention — le geste central du module. Deux corrections réelles, quatre exceptions tracées, et la fermeture de la chaîne par les vérifications obligatoires. L'enjeu n'est pas le code — il paramètre une requête SQL en trois minutes — mais la discipline de l'exception, qui est ce que le module 1 doit lui laisser.
Ce qui coince
- La tentation de tout corriger pour avoir un vert « propre ». Il priverait le module 2 de sa matière. Lui rappeler l'échéance inscrite hier : ces quatre-là appartiennent au M2.
- Le
nosemgrepnu, sans identifiant de règle ni commentaire. Il désactive alors toutes les règles sur la ligne, y compris celles qui n'existent pas encore. Exiger la forme nommée. - Les identifiants de règle recopiés d'un exemple au lieu d'être lus dans sa propre sortie. Symptôme : le pipeline reste rouge et il ne comprend pas pourquoi, puisqu'il a « mis les exceptions ».
- Les vérifications obligatoires oubliées à l'étape 8. Sans elles, tout le travail du jour ne bloque rien : les jobs échouent et la fusion reste possible. C'est le maillon n° 2 de la chaîne, et c'est celui qu'on saute.
Questions à anticiper
- « Pourquoi paramétrer plutôt qu'échapper les apostrophes ? » — parce que l'échappement manuel est une liste noire, toujours incomplète ; la requête paramétrée sépare structurellement le code des données. Réponse à exiger de lui au M5.
- « Mon exception est-elle valable pour toujours ? » — jusqu'au 10 août. Lui demander ce qui se passe le 11 : la réponse attendue est que le M2 les lève une par une.
- « Le vert, c'est fini ? » — non : vert veut dire « aucune règle active n'a mordu ». Quatre failles sont ouvertes et deux autres attendent le M5. Le vert est un état du dispositif, pas du code.
Ce qu'il faut exiger — la pull request verte, les deux vérifications cochées comme obligatoires dans l'interface, et le rejeu de la charge d'injection qui répond désormais 401. Ce dernier point est le seul qui prouve que la correction fonctionne : Semgrep qui se tait ne prouve que le silence de Semgrep.
STRIDE : six questions à poser à chaque flux
Jour 3 · Le pipeline qui refuse la faille · 25 min
Vos deux jours d'outillage ont un angle mort, et il est structurel. Semgrep trouve un motif de code dangereux. Gitleaks trouve une chaîne qui ressemble à un secret. Trivy trouve une version vulnérable. Tous les trois cherchent quelque chose qui est là.
Aucun d'eux ne trouvera jamais quelque chose qui manque — un contrôle d'autorisation absent, une donnée qui traverse une frontière sans être validée, un journal qu'on n'écrit pas. Ces défauts-là ne sont pas des motifs de code : ce sont des décisions d'architecture qu'on n'a pas prises. On les trouve en réfléchissant, pas en scannant.
Quatre questions, avant la méthode
Toute modélisation de menaces, quelle que soit la méthode, répond à quatre questions posées dans cet ordre :
- Qu'est-ce qu'on construit ? — le diagramme.
- Qu'est-ce qui peut mal tourner ? — les menaces. C'est ici que STRIDE sert.
- Qu'est-ce qu'on fait à ce sujet ? — les mesures, ou l'acceptation explicite du risque.
- A-t-on fait du bon travail ? — la revue, et le fait de recommencer au prochain changement d'architecture.
Le diagramme de flux de données
Cinq symboles suffisent, et le dernier est le plus important.
| Élément | Ce que c'est | Dans votre application |
|---|---|---|
| Entité externe | quelqu'un ou quelque chose hors de votre contrôle | le navigateur, l'URL arbitraire appelée par /fetch |
| Processus | du code qui s'exécute et transforme la donnée | l'application Flask |
| Magasin de données | un endroit où la donnée se repose | lab.db, le fichier SQLite |
| Flux de données | un déplacement de donnée entre deux éléments | la requête HTTP, la requête SQL, l'appel système |
| Frontière de confiance | la limite où la donnée change de zone de contrôle | entre le navigateur et l'application — et c'est là que tout se joue |
Une donnée qui reste dans votre zone de contrôle ne pose pas de problème. Une donnée qui franchit une frontière cesse d'être maîtrisée et devient une hypothèse — quelque chose que vous croyez savoir sur une entrée que quelqu'un d'autre a fabriquée.
C'est pour cela qu'on commence toujours par tracer les frontières : les menaces s'agglutinent dessus. Un diagramme sans frontière de confiance est un schéma d'architecture, pas un modèle de menaces.
Les six lettres
Pour chaque flux qui traverse une frontière, on pose six questions. Chacune correspond à une propriété de sécurité qui pourrait être violée.
| Lettre | Menace | Propriété violée | Sur votre application |
|---|---|---|---|
| S | Spoofing — usurpation d'identité | authentification | l'injection SQL du /login permettait de devenir admin |
| T | Tampering — altération de données | intégrité | l'injection de commande de /ping permet d'écrire sur le système |
| R | Repudiation — nier une action | non-répudiation | aucun journal applicatif : rien ne trace qui a fait quoi |
| I | Information disclosure — divulgation | confidentialité | debug=True expose la trace d'exécution et le code source |
| D | Denial of service — indisponibilité | disponibilité | /fetch sans limite : une URL lente immobilise un processus |
| E | Elevation of privilege — élévation | autorisation | /profile/<uid> : aucun contrôle, on lit le profil de n'importe qui |
Ce que STRIDE trouve et que vos outils ne trouveront jamais
Regardez la dernière ligne. VULN-04, l'IDOR, est la seule des dix qui
n'apparaîtra dans aucun rapport de votre pipeline — ni aujourd'hui, ni au module
2, ni au module 3. Aucun scanner ne peut la voir, pour une raison simple et définitive :
le code est parfaitement correct. Il lit un enregistrement par son identifiant, ce qui est exactement ce qu'on attend d'une fonction de ce nom. Ce qui manque, c'est une ligne qui n'a jamais été écrite — la vérification que l'utilisateur connecté a le droit de consulter ce profil-là. Un outil ne détecte pas l'absence d'une intention.
Il faudra un module 5 entier, et un humain qui se demande « et si je changeais ce chiffre ? », pour la trouver. C'est la meilleure justification de la modélisation des menaces que vous rencontrerez dans ce parcours, et elle est dans votre propre code.
STRIDE énumère, il ne priorise pas. Il produit une liste de menaces, pas un ordre de traitement. La priorisation viendra au module 4, avec CVSS et DREAD.
Il traite mal certaines familles : le déni de service distribué, les risques organisationnels, la chaîne d'approvisionnement logicielle s'y logent mal. Le module 4 introduira PASTA et les arbres d'attaque pour les couvrir.
Il dépend entièrement de la qualité du diagramme. Un flux oublié est une menace jamais posée — et c'est de très loin la première cause de modèle de menaces inutile.
Ce que vous produisez cet après-midi est un brouillon de travail. Le module 4 le reprendra
pour en faire rapports/threat-model.md, avec le registre de risques et les scores.
Aujourd'hui, on installe le geste.
Intention — poser la modélisation des menaces comme une méthode de conception, complémentaire des outils : les scanners trouvent des défauts d'implémentation, STRIDE trouve des défauts de conception, et aucun des deux ne voit ce que voit l'autre. C'est une introduction : le module 4 en fait un livrable complet, aujourd'hui on installe le vocabulaire et le geste.
Ce qui coince
- STRIDE récité comme un acronyme sans que le DFD soit dessiné. Le geste est le diagramme ; sans lui, la liste de menaces devient une liste de peurs générique, valable pour n'importe quelle application.
- La frontière de confiance oubliée ou placée n'importe où. C'est l'élément structurant du diagramme : mal placée, l'analyse rate ce qu'elle devrait trouver.
- La confusion menace / vulnérabilité. Une menace existe même sans faille — elle décrit ce qu'un attaquant voudrait faire. Le distinguo sert directement au M4 pour le registre de risques.
- L'attente d'une priorisation. STRIDE énumère, il ne classe pas. Le dire, sinon il cherchera un score qui n'existe pas dans la méthode.
Questions à anticiper
- « À quoi bon, puisque Semgrep trouve les failles ? » — la question centrale. Réponse : aucun scanner ne dira jamais qu'il manque une autorisation, parce qu'un contrôle absent n'a pas de motif de code. VULN-04, l'IDOR, en est la démonstration parfaite — c'est la seule que le pipeline ne verra jamais.
- « Quand modélise-t-on, en pratique ? » — à la conception, puis à chaque changement d'architecture. Pas à chaque commit.
- « Est-ce que STRIDE couvre tout ? » — non : le déni de service distribué, l'organisation, la chaîne d'approvisionnement s'y logent mal. Le M4 introduira PASTA et les arbres d'attaque.
Ce qu'il faut exiger — qu'il sache dire, avant le TP, quelle menace de son application aucun outil du parcours ne trouvera. S'il répond VULN-04, la leçon est acquise.
TP 12 — STRIDE sur devsecops-lab : le diagramme et la table
Jour 3 · Le pipeline qui refuse la faille · 55 min
Produire un modèle de menaces de votre application, et confronter ce qu'il trouve à ce que vos outils ont trouvé.
- Créez le fichier. Attention au nom :
threat-model.mdtout court est réservé au livrable du module 4, qui reprendra votre brouillon. Ne l'écrasez pas d'avance.cd ~/poei-s4/devsecops-lab mkdir -p rapports touch rapports/stride-devsecops-lab-m1.md - Dessinez le diagramme avant tout. En texte, c'est parfait — il doit être versionnable et relisible, pas beau. Le seul élément non négociable est la frontière de confiance.
# Modele de menaces — devsecops-lab (brouillon M1) ## 1. Diagramme de flux de donnees ``` ZONE NON FIABLE | ZONE APPLICATIVE | [navigateur] --- F1 ---> ( app Flask ) --- F3 ---> [[ lab.db ]] entite externe <-- F2 --- port 5000 --- F4 ---> [[ shell OS ]] | --- F5 ---> [url arbitraire] | frontiere de confiance ``` | Flux | De | Vers | Donnee transportee | Traverse la frontiere | |------|----|----- |--------------------|------------------------| | F1 | navigateur | app Flask | parametres HTTP (user, pwd, q, host, uid, url) | **oui** | | F2 | app Flask | navigateur | reponses HTML, JSON, traces d'erreur | **oui** | | F3 | app Flask | lab.db | requetes SQL construites | non | | F4 | app Flask | shell OS | ligne de commande construite | non | | F5 | app Flask | URL arbitraire | requete HTTP sortante | **oui** | - Remarquez ce que le tableau vous apprend déjà, avant toute analyse : trois flux sur cinq traversent la frontière, et F5 la traverse vers l'extérieur, ce qui est le cas le moins intuitif. Une application qui appelle une adresse fournie par l'utilisateur transforme votre serveur en client de l'attaquant. Vous y reviendrez au module 5.
- Passez chaque flux au crible des six lettres. Toutes les cases ne se remplissent pas — cinq flux fois six lettres font trente cases, et beaucoup sont sans objet. Une case vide assumée vaut mieux qu'une menace inventée. Voici la structure et deux lignes pour donner le ton.
## 2. Menaces par flux (STRIDE) | Flux | Lettre | Menace | Mesure existante | Verdict | |------|--------|--------|------------------|---------| | F1 | S | Un anonyme se fait passer pour l'administrateur en manipulant les parametres du formulaire | requete parametree depuis le 2026-08-05 (VULN-01) | **couverte** | | F1 | E | Un utilisateur authentifie consulte le profil d'un autre en changeant l'identifiant dans l'URL | **aucune** — aucun controle d'autorisation | **ouverte (VULN-04)** | | F1 | T | ... | | | | F2 | I | ... | | | | F1 | R | ... | | | | F5 | I | ... | | | - Complétez la table. Insistez particulièrement sur le R, la répudiation : c'est la lettre que tout le monde bâcle, parce qu'elle ne porte pas sur ce qui est écrit mais sur ce qui ne l'est pas. Posez-vous la question franchement : si quelqu'un s'authentifiait frauduleusement sur votre application ce soir, quelle trace en resterait-il demain matin ? Écrivez la réponse, même si elle est « aucune ».
- Le croisement — c'est le cœur du TP. Confrontez votre table à la liste des dix VULN et remplissez les trois cases. La troisième est celle qui compte.
## 3. Croisement avec les dix vulnerabilites connues ### 3.1 Menaces confirmees par mes outils _(STRIDE et le pipeline disent la meme chose — le recouvrement est rassurant)_ ### 3.2 VULN que STRIDE ne fait pas ressortir _(exemple : VULN-10, dependances a CVE. Ce n'est pas un defaut de conception du systeme que je dessine, c'est une propriete de sa chaine d'approvisionnement. Les deux approches ne se remplacent pas.)_ ### 3.3 Menaces qu'AUCUN outil du parcours ne trouvera _(la section la plus importante. Pour chacune : la menace, et POURQUOI aucun scanner ne peut la voir.)_ - Pour la section 3.3, trouvez-en au moins deux. VULN-04 en est une, et la leçon vous a donné le motif : le code est correct, c'est une ligne jamais écrite qui manque, et un outil ne détecte pas l'absence d'une intention. Cherchez-en une seconde — regardez du côté du R, et du côté de ce qui se passerait si votre application recevait dix mille requêtes par seconde.
- Terminez par les limites de votre propre modèle. C'est la section qui distingue un professionnel : savoir ce que son analyse ne couvre pas.
## 4. Limites de ce modele - Etabli le 2026-08-05 sur l'architecture du jour ; a rejouer a chaque changement d'architecture (conteneurisation K8s au module 3, notamment). - STRIDE enumere, il ne priorise pas : aucun score ici. La priorisation par CVSS et DREAD est l'objet du module 4. - Perimetre : l'application seule. Ni le poste, ni le reseau, ni la chaine de construction (le pipeline lui-meme est une surface d'attaque non modelisee ici). - Le deni de service distribue et les risques organisationnels se logent mal dans STRIDE ; le module 4 introduira PASTA et les arbres d'attaque. - Committez et poussez par une pull request. Votre pipeline doit rester vert : un fichier Markdown ne déclenche aucune règle, mais l'aller-retour complet est devenu le geste normal.
git switch -c docs/stride-m1 git add rapports/stride-devsecops-lab-m1.md git commit -m "docs: modele de menaces STRIDE (brouillon M1)" git push -u origin docs/stride-m1
- Votre section 3.3 contient au moins deux menaces qu'aucun outil ne trouvera. Qu'est-ce que cela dit du rapport entre outillage et conception ?
- Le flux F5 traverse la frontière vers l'extérieur. En quoi ce sens est-il plus difficile à raisonner que le sens entrant ?
- Vous avez trouvé des VULN que STRIDE ne fait pas ressortir. Est-ce une faiblesse de la méthode, ou autre chose ?
La faute qui vide l'exercice de son sens : passer directement à la liste des menaces sans dessiner le diagramme. On produit alors une liste générique — « il pourrait y avoir une injection », « il pourrait y avoir une fuite » — vraie pour n'importe quelle application, donc utile pour aucune. Le diagramme d'abord, et surtout la frontière de confiance.
Deuxième point : ne cherchez pas des menaces déjà connues. L'intérêt de l'étape 5 est précisément d'en trouver au moins une qui ne figure dans aucune de vos dix VULN — c'est elle qui justifie la méthode.
Intention — faire produire un modèle de menaces sur un système qu'il connaît par cœur, pour que la méthode soit le seul sujet. Et surtout : lui faire croiser sa table avec la liste des dix VULN. Trois enseignements sortent de ce croisement, et ce sont eux, pas la table, qui comptent.
Le croisement, et ce qu'il doit produire
- Des menaces que ses outils ont trouvées — le recouvrement rassure.
- Des menaces que ses outils ne trouveront jamais : VULN-04 au premier chef, et le R de répudiation, qui n'apparaît nulle part dans ses dix failles.
- Des VULN que STRIDE ne fait pas ressortir — les dépendances à CVE, par exemple, ne sont pas une menace de conception. C'est le point le plus subtil : les deux approches sont complémentaires, aucune n'est supérieure.
Ce qui coince
- La table remplie ligne à ligne sans diagramme. Résultat générique, à refaire. Vérifier que le diagramme existe avant de le laisser écrire la table.
- Le R de répudiation systématiquement bâclé — c'est la lettre la plus difficile pour un développeur, parce qu'elle porte sur ce qui n'existe pas : les journaux. Insister, c'est le manque le plus fréquent en production réelle.
- La confusion menace / vulnérabilité dans la rédaction. « Injection SQL » est une vulnérabilité ; « un attaquant se fait passer pour un administrateur » est une menace. La table décrit des menaces.
- Le fichier nommé
threat-model.md: ce nom est réservé au livrable du M4. Faire respecter le nommage, sinon le M4 écrasera son travail.
Questions à anticiper
- « Combien de menaces faut-il trouver ? » — pas de quota. Six flux fois six lettres donne trente-six cases, dont beaucoup sont sans objet ; le dire évite le remplissage mécanique.
- « Faut-il traiter les menaces que je trouve ? » — non, pas aujourd'hui. On les énumère et on note la mesure existante ou son absence. Le traitement est au M4.
Ce qu'il faut exiger — au moins une menace absente de la liste des dix VULN, et une phrase qui dit pourquoi aucun outil du parcours ne l'aurait trouvée. C'est le seul critère qui prouve que la méthode a servi.
TP 13 — La note de synthèse du module
Jour 3 · Le pipeline qui refuse la faille · 40 min
Rassembler les livrables du module et écrire ce que vous défendrez devant votre encadrant.
- Complétez la section 4 du README : elle porte maintenant le pipeline en plus des garde-fous locaux.
### Pipeline (module 1, jour 3) `.github/workflows/ci.yml` — deux jobs sur chaque pull request vers `main` : | Job | Outil | Verrou | Artefact | |-----|-------|--------|----------| | secrets | gitleaks v8.18.4 | code de sortie non nul si un secret est trouve | gitleaks.json | | sast | semgrep 1.85.0 (`--error`) | code de sortie non nul si une regle correspond | semgrep.sarif | Les deux sont des **verifications obligatoires** sur `main` : une pull request dont un job echoue ne peut pas etre fusionnee, y compris par le proprietaire du depot. Repli hors ligne : `act -j sast`. - Complétez la section 6, « Limites connues ». C'est la section que personne n'écrit et la première que lit un auditeur.
## 6. Limites connues - Six vulnerabilites restent ouvertes a la fin du module 1 : VULN-02, 03, 06, 07, 10 (echeance module 2) et VULN-04, 08, 09 (modules 3 et 5). Toutes sont tracees. - Le pipeline ne couvre que le code source : ni les dependances (module 2), ni l'image (module 3), ni l'application en execution (module 2, DAST). - Gitleaks analyse l'arbre de travail (`--no-git`) et non l'historique a chaque passage. L'historique a ete purge le 2026-08-04 et verifie a cette date. - Les hooks pre-commit ne sont pas garantis : ils vivent hors du depot versionne et se contournent. Le verrou reel est le pipeline. - Le modele de menaces est un brouillon etabli sur l'architecture du 2026-08-05. - Écrivez la note de synthèse. Une page, pas deux. Ce qui ne tient pas en une page n'est pas encore compris. La partie qui compte est la deuxième : trois arbitrages que vous défendez, pas trois choses que vous avez faites.
# Note de synthese — Module 1, DevSecOps : fondations Elie Zaraket · 3 au 5 aout 2026 ## Ce que j'ai construit _(cinq lignes : le depot, la chaine de confiance, le pipeline, le modele de menaces)_ ## Les trois arbitrages que je defends **1. Le verrou vit en CI, pas dans le hook local.** _(pourquoi : je l'ai contourne moi-meme avec --no-verify le 4 aout ; le hook aide, il ne garantit pas)_ **2. Quatre failles restent ouvertes derriere une exception datee, plutot qu'une regle retiree.** _(pourquoi : l'exception laisse une trace verifiable et une echeance ; la regle retiree ne laisse rien, et un pipeline rouge en permanence cesse d'etre lu)_ **3. La rotation precede la reecriture de l'historique.** _(pourquoi : la reecriture ne desexpose rien ; un secret expose est compromis, et la preuve du contraire n'existe pas)_ ## Ce qui manque encore _(trois lignes, en pointant vers les modules qui s'en chargent)_ - Vérifiez que vos questions de réflexion des treize TP sont répondues dans
reponses-reflexion.md. C'est le document dont votre encadrant tirera ses questions demain — pas de vos scripts. Deux réponses développées valent mieux que trente expédiées. - Faites l'inventaire de vos livrables. Les huit doivent exister et être à jour.
cd ~/poei-s4/devsecops-lab ls -1 README.md journal-incidents.md reponses-reflexion.md \ note-synthese-m1.md .pre-commit-config.yaml .gitleaks.toml \ .github/workflows/ci.yml rapports/stride-devsecops-lab-m1.md - Commit final du module, signé, fusionné par pull request avec un pipeline vert. Puis posez une étiquette : elle marquera l'état exact du dépôt à la fin du module 1, et vous pourrez y revenir depuis n'importe quel module ultérieur.
git add -A git commit -m "docs: note de synthese et limites connues du module 1" git push # Apres fusion de la pull request : git switch main && git pull git tag -a m1-fin -m "Fin du module 1 : depot, chaine de confiance, pipeline" git push origin m1-fin - Dernière vérification, et c'est celle que le bilan reprendra : votre dépôt est-il rejouable par quelqu'un d'autre ? Ne le vérifiez pas en relisant votre README — clonez-le dans un dossier neuf et suivez vos propres instructions à la lettre.
git clone git@github.com:<votre-compte>/devsecops-lab.git /tmp/verif-m1 cd /tmp/verif-m1 # Suivez VOTRE section 1, puis VOTRE section 3, sans rien deviner. # Si une valeur vous manque, c'est votre README qui est incomplet — corrigez-le. cat .env.exemple cd - && rm -rf /tmp/verif-m1
- Vous avez cloné votre dépôt dans un dossier neuf. Qu'avez-vous découvert que la relecture du README ne vous avait pas montré ?
- Vos trois arbitrages sont défendables. Lequel serait le plus difficile à faire accepter à une équipe qui livre tous les jours, et comment le présenteriez-vous ?
Intention — le temps de rédaction, pris pendant la formation et non après. Ses étapes sont exactement les critères du bilan qui suit : aucune surprise, et c'est délibéré. La note de synthèse est le document sur lequel portera l'essentiel des deux heures de revue.
Ce qui coince
- La note qui raconte ce qu'il a fait plutôt que ce qu'il défend. Signe : une suite chronologique de TP. Ce qu'on attend, ce sont trois arbitrages assumés, avec leur motif.
- Le README laissé avec ses sections en attente. La section 4 doit maintenant porter le pipeline, et la section 6 les limites connues.
- Les questions de réflexion expédiées en une ligne chacune. Deux réponses développées valent mieux que trente expédiées — le dire maintenant, pas demain.
Questions à anticiper
- « Combien de pages ? » — une, et c'est une contrainte, pas une facilité. Ce qui ne tient pas en une page n'est pas encore compris.
- « Que va-t-on me demander demain ? » — de défendre les trois arbitrages page fermée. Le dire pour qu'il travaille dans ce sens.
Ce qu'il faut exiger — les trois arbitrages nommés et motivés. Une note qui décrit sans arbitrer n'est pas une note de synthèse, c'est un compte rendu.
M1 · J3 — Pipeline rouge puis vert sur VULN-01 et VULN-05
Jour 3 · Le pipeline qui refuse la faille · 2 h
Elle a lieu mercredi à 15 h 00 et dure deux heures — c'est la durée portée par ce point de contrôle. Votre encadrant passera en revue vos livrables, vous fera expliquer les notions, et validera — ou non — le module.
Elle s'ouvre par une démonstration en direct : vous ouvrez une pull request avec une faille, le pipeline la refuse, vous corrigez, elle passe. Préparez la branche à l'avance, mais faites la manipulation devant lui.
Il partira ensuite de votre reponses-reflexion.md et de votre note de synthèse : ce sont les deux documents qui montrent ce que vous avez compris, et non ce que vous avez exécuté.
Ce qui n'est pas attendu : un dépôt sans faille. Six vulnérabilités sont encore ouvertes et c'est le programme. Ce qui est attendu, c'est que vous sachiez lesquelles, pourquoi, et jusqu'à quand.
Intention — la revue de fin de module, deux heures, mercredi 15 h. Elle porte sur les trois jours, pas sur la journée : c'est le seul moment où l'on vérifie que les décisions de lundi sont encore défendues le mercredi, page fermée.
Un déroulé qui tient en deux heures
- 30 min de démonstration : il ouvre une pull request avec une faille, montre le refus, corrige, montre le vert. C'est la démonstration centrale du module et elle doit être faite en direct, pas racontée.
- 40 min de questions tirées de
reponses-reflexion.mdet de la note de synthèse — les deux documents qui montrent ce qu'il a compris plutôt que ce qu'il a exécuté. - 25 min sur le modèle de menaces : la section 3.3, et pourquoi aucun outil ne trouvera VULN-04.
- 15 min sur la chaîne de confiance : signature, protection, verrou — trois preuves distinctes, à ne pas confondre.
- Le reste sur ce qui manque et sur l'enchaînement vers le module 2.
Les trois arbitrages à faire défendre, page fermée — le verrou en CI plutôt que dans le hook ; l'exception datée plutôt que la règle retirée ; la rotation avant la réécriture. S'il récite la règle sans le motif, le module n'est pas acquis même si tout le dispositif tourne : c'est la distinction que ce bilan existe pour faire.
Ce qui est souvent en retard — la note de synthèse et la section 3.3 du modèle de menaces. Les réclamer la veille, pas pendant la séance : deux heures ne suffisent pas à les lire et à interroger.
Questions à anticiper
- « Est-ce que je repars de zéro au module 2 ? » — non, ce dépôt est le socle des cinq modules et de la soutenance. Le dire explicitement pour qu'il ne « nettoie » rien d'ici jeudi.
- « Le pipeline est vert, le module est réussi ? » — non : vert avec quatre exceptions datées. S'il ne sait pas dire lesquelles ni jusqu'à quand, la notion centrale du module n'est pas acquise.
- « Pourquoi ne pas avoir tout corrigé ? » — parce que le M2 en a besoin, et parce qu'une dette datée est une pratique professionnelle. Les deux réponses sont bonnes, l'une est pédagogique, l'autre est vraie.
Ne pas valider si — la démonstration rouge puis vert n'est pas faite en direct, ou si les vérifications obligatoires ne sont pas réellement cochées dans l'interface. Sur ce dernier point, le juge est la tentative de fusion d'une pull request en échec, jamais la capture d'écran des réglages.