
Après le socle du dépôt, on
écrit le workflow d'intégration continue qui teste et analyse le projet.
L'objectif n'est pas seulement qu'il soit vert : il doit résister aux scanners
de sécurité de workflow. Ce guide construit le ci.yml du dépôt de référence
secure-python-pipeline job par job, applique chaque règle de durcissement
(permissions minimales, épinglage par SHA, harden-runner), puis le fait passer à
zéro finding sur les quatre scanners de workflow : actionlint, zizmor,
poutine et plumber, ce dernier en mode bloquant sur une trust policy.
Public visé : un mainteneur qui écrit déjà des workflows et veut les durcir au
niveau d'une référence auditable.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Structurer un workflow CI aux permissions minimales, job par job
- Rappeler les règles de durcissement déjà vues (SHA,
permissions,persist-credentials) - Épingler les outils du CI par hash comme les dépendances applicatives
- Valider le workflow avec actionlint, zizmor et poutine à zéro finding
- Déclarer une trust policy plumber bloquante et la faire échouer sur toute source non autorisée
Prérequis
Section intitulée « Prérequis »Le dépôt et son socle sont en
place : application FastAPI, Dockerfile multi-stage, dépendances figées par hash.
Installez les scanners en local : actionlint, zizmor, poutine et
plumber. Le lab suppose la lecture préalable d'Épingler
par SHA et de
Permissions : les deux règles
sont appliquées ici sans être réexpliquées.
Le dépôt de référence est public et consultable :
github.com/stephrobert/secure-python-pipeline.
Vous pouvez le cloner pour comparer votre ci.yml, votre .poutine.yml et votre
.plumber.yaml avec les fichiers réels commentés dans ce guide.
Le workflow CI, job par job
Section intitulée « Le workflow CI, job par job »Le workflow enchaîne cinq jobs indépendants : lint (ruff), test (pytest), sast (bandit), audit (pip-audit et Trivy) et build-check (construction de l'image et scan). Chacun tourne dans un runner propre, avec ses propres permissions. La déclaration de tête pose les garde-fous globaux, valables pour tous les jobs.
name: CI
on: push: branches: [main] pull_request: branches: [main]
# Aucune permission par défaut : chaque job demande le strict nécessaire.permissions: {}
concurrency: group: ci-${{ github.ref }} cancel-in-progress: trueDeux garde-fous tiennent déjà dans cet en-tête. permissions: {} retire tous
les droits du GITHUB_TOKEN au niveau du workflow : chaque job redemande ensuite
le strict minimum. concurrency annule les exécutions obsolètes sur une même
référence pour ne pas gaspiller de minutes ni faire tourner deux builds
concurrents sur la même branche.
Lint : le gabarit de tous les jobs
Section intitulée « Lint : le gabarit de tous les jobs »Le job de lint sert de modèle aux quatre autres. Il installe ruff épinglé par
hash et vérifie le style du code. Sa structure de tête (durcissement du runner,
checkout, installation de Python) est identique dans les autres jobs.
jobs: lint: name: Lint (ruff) runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read steps: - name: Harden runner uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 with: egress-policy: audit - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Python uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 with: python-version: "3.11" - name: Install ruff (épinglé par hash) run: pip install --require-hashes -r requirements-tools.txt - name: Run ruff run: ruff check .Test, SAST et audit : les jobs de vérification
Section intitulée « Test, SAST et audit : les jobs de vérification »Les jobs test, sast et audit reprennent la même tête (harden-runner,
checkout persist-credentials: false, actions/setup-python épinglé) et ne
diffèrent que par leur étape utile. Le job test installe les dépendances
applicatives et de test, toujours avec hash, puis lance la suite :
test: name: Tests (pytest) runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read steps: # ... harden-runner, checkout, setup-python identiques au job lint ... - name: Install dependencies run: | pip install --require-hashes -r requirements.txt pip install --require-hashes -r requirements-test.txt - name: Run pytest run: pytest -qLe job sast (name: SAST (bandit)) lance bandit sur le code source pour
repérer les motifs dangereux (usage de eval, subprocess avec shell=True,
secrets en dur). Son étape utile tient en une commande, bandit -r src, précédée de
l'installation de l'outil épinglé par hash. Son name: est le libellé exact
repris comme status check SAST (bandit) dans le ruleset de protection.
Le job audit croise deux analyses de vulnérabilités sur les dépendances :
pip-audit interroge la base des avis Python, et Trivy scanne le fichier
verrouillé en mode fs (filesystem). Le choix de Trivy plutôt que
google/osv-scanner-action n'est pas anodin : l'action OSV de Google déclenche un
finding poutine (github_action_from_unverified_creator_used), alors que
aquasecurity/trivy-action couvre le même besoin sans alerte.
audit: name: Audit dépendances (pip-audit + Trivy) runs-on: ubuntu-24.04 timeout-minutes: 10 permissions: contents: read steps: # ... harden-runner, checkout, setup-python identiques ... - name: Install pip-audit (épinglé par hash) run: pip install --require-hashes -r requirements-tools.txt - name: Run pip-audit run: pip-audit -r requirements.txt - name: Scan dépendances (Trivy filesystem) uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: scan-type: fs scan-ref: requirements.txt severity: HIGH,CRITICAL ignore-unfixed: true exit-code: "1" scanners: vulnL'option ignore-unfixed: true applique dans le CI le même arbitrage qu'au
socle (voir bootstrap) : on
ignore les CVE sans correctif amont et on garde exit-code: "1" pour
échouer sur tout ce qui est réellement corrigeable.
Build-check : construire et scanner l'image
Section intitulée « Build-check : construire et scanner l'image »Le dernier job, build-check (name: Build image + scan (trivy)), construit
l'image Docker sans la pousser (push: false, load: true) via
docker/build-push-action, puis la scanne avec Trivy en mode image. C'est le même
durcissement partout : runner figé, timeout-minutes, permissions: contents: read, actions épinglées par SHA. Ce job valide que l'image produite ne porte
aucune vulnérabilité HIGH ou CRITICAL corrigeable avant même de parler de
release. Son name: correspond au status check Build image + scan (trivy) du
ruleset.
Les règles de durcissement appliquées
Section intitulée « Les règles de durcissement appliquées »Chaque ligne des jobs ci-dessus applique une règle déjà introduite ailleurs dans la formation. Ce guide les applique, il ne les réexplique pas. Voici le rappel express et le renvoi vers le guide canonique :
runs-on: ubuntu-24.04fige le runner ;ubuntu-latestrendrait le build non reproductible.permissions: {}au workflow puiscontents: readpar job donnent le moindre privilège auGITHUB_TOKEN(voir Permissions).harden-runneren modeauditinstalle un pare-feu de sortie qui enregistre le trafic réseau du runner ; le modeblockle restreindrait.persist-credentials: falseempêche leGITHUB_TOKENde persister dans la configuration git, où il pourrait fuiter.- Actions épinglées par SHA de commit 40 caractères, suivi d'un commentaire de
version : un tag comme
@v7est mutable (voir Épingler par SHA). timeout-minutesetconcurrencyévitent les jobs zombies et les exécutions obsolètes.
Épingler les outils du CI par hash
Section intitulée « Épingler les outils du CI par hash »Les outils du CI (ruff, bandit, pip-audit) reçoivent le même verrou
par hash que le socle applicatif, décrit dans le
bootstrap : versions déclarées
dans un .in, compilé avec --generate-hashes, installé avec --require-hashes.
Seul le fichier d'entrée change, ici requirements-tools.in :
# requirements-tools.in : les trois outils du CI, versions figéesruff==0.14.9bandit==1.8.0pip-audit==2.10.1On le compile en un fichier verrouillé avec hashes, installé par chaque job en refusant tout paquet dont l'empreinte ne correspond pas :
uv pip compile --generate-hashes requirements-tools.in -o requirements-tools.txtpip install --require-hashes -r requirements-tools.txtValider avec les scanners de workflow
Section intitulée « Valider avec les scanners de workflow »Quatre scanners passent en revue les workflows, chacun sous un angle différent. L'objectif est zéro finding sur les quatre. Les trois premiers tournent en local sans configuration ; plumber exige une trust policy, traitée juste après.
actionlint valide la syntaxe et les bonnes pratiques. Sur un workflow propre,
il ne renvoie aucune sortie et un code de retour 0 :
actionlintecho "code retour : $?"code retour : 0zizmor traque les failles de sécurité des workflows (injection, permissions
excessives, action non épinglée, persist-credentials oublié). On le lance sur le
workflow CI en mode hors-ligne :
zizmor --offline .github/workflows/ci.yml INFO zizmor: 🌈 zizmor v1.26.1 INFO audit: zizmor: 🌈 completed .github/workflows/ci.ymlNo findings to report. Good job!poutine cherche les chaînes d'exploitation CI/CD en analysant tout le dépôt.
Il évalue ici treize règles et n'en fait échouer aucune :
poutine analyze_local .Summary of findings:| RULE ID | FAILURES | STATUS || default_permissions_on_risky_events | 0 | Passed || github_action_from_unverified_creator_used | 0 | Passed || injection | 0 | Passed || job_all_secrets | 0 | Passed || pr_runs_on_self_hosted | 0 | Passed || untrusted_checkout_exec | 0 | Passed || unverified_script_exec | 0 | Passed || ... (13 règles au total, 0 failure) | 0 | Passed |Acquitter le faux positif poutine
Section intitulée « Acquitter le faux positif poutine »poutine signale les actions dont le créateur n'est pas « vérifié » sur le
Marketplace, via la règle github_action_from_unverified_creator_used. Le piège :
l'action officielle getplumber/plumber, pourtant légitime et utilisée pour le
scan de confiance, n'est pas encore référencée comme verified creator. Plutôt que
de désactiver la règle pour tout le dépôt, on acquitte ce seul faux positif de
façon ciblée dans un fichier .poutine.yml :
# .poutine.yml : la règle reste active pour toute AUTRE action tierce.skip: - rule: github_action_from_unverified_creator_used purl: - pkg:githubactions/getplumber/plumberLe champ purl (Package URL) désigne exactement l'action à exempter. Toute
autre action non vérifiée continuerait de déclencher un finding : c'est la
différence entre un acquittement ciblé et une règle désactivée en aveugle, qui
laisserait passer une action réellement suspecte.
Plumber : la trust policy bloquante
Section intitulée « Plumber : la trust policy bloquante »plumber va plus loin que les trois autres : il construit un graphe de
confiance et vérifie que chaque composant tiers vient d'une source autorisée.
Il exige un fichier de politique .plumber.yaml, généré puis affiné :
plumber config generate # écrit le template complet (le mode init exige un TTY)Le contrôle central côté GitHub est githubActionMustComeFromAuthorizedSources.
Les actions officielles (actions/*, github/*) et celles de votre organisation
sont couvertes par deux bascules ; les tierces se déclarent explicitement dans
trustedGithubActions :
githubActionMustComeFromAuthorizedSources: enabled: true # Actions GitHub officielles (actions/*, github/*). Défaut : true. trustGithubOfficialActions: true # Actions du même owner que le dépôt scanné. Défaut : true. trustSameOrgActions: true minimumStars: 0 # Allowlist des sources tierces : owner/repo exact ou owner/* pour une org. trustedGithubActions: - getplumber/plumber # l'action Plumber elle-même - docker/setup-buildx-action - docker/build-push-action - step-security/harden-runner # durcissement du runner (egress) - aquasecurity/trivy-action # scan de vulnérabilités image et deps - sigstore/cosign-installer # signature keyless SigstoreDéclarer ses sources de confiance est le coeur de la démarche : on rend explicite ce à quoi on accorde sa confiance, au lieu de subir la liste implicite du Marketplace. Une action absente de cette allowlist ferait échouer le contrôle.
Dans ce lab, plumber est bloquant : le workflow qui l'exécute passe
min-points: "100" et soft-fail: false, si bien que la moindre non-conformité
fait échouer le job au lieu de simplement baisser un badge. Cette exigence
stricte garantit qu'aucune source non déclarée ne se glisse dans le pipeline
sans un échec net. Le workflow plumber.yml complet, avec le token GitHub
App qui donne à plumber la lecture complète de la protection de branche, est
détaillé dans Protection et
gouvernance.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications
À retenir
Section intitulée « À retenir »permissions: {}au workflow puis le strict minimum par job ferme la surface duGITHUB_TOKEN; ici chaque job se contente decontents: read.- Actions épinglées par SHA,
persist-credentials: false,harden-runner, runner figéubuntu-24.04,timeout-minutesetconcurrencysont les réflexes de base, appliqués sans réexplication. - Les outils du CI (ruff, bandit, pip-audit) s'épinglent par hash via un
.incompilé en.txtavec--generate-hashes, installé en--require-hashes. - actionlint, zizmor et poutine passent à zéro finding ; le faux positif poutine
sur
getplumber/plumbers'acquitte de façon ciblée dans.poutine.yml. - plumber valide les sources de confiance via
.plumber.yamlet tourne en mode bloquant (min-points: "100",soft-fail: false), ce qui fait échouer le job à la moindre source non déclarée. - Le dépôt complet est public : github.com/stephrobert/secure-python-pipeline.
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Retour au hub du lab : la série complète et le dépôt de référence à cloner.
- Auditer avec zizmor : le détail des règles de zizmor et leur correction.
- Scanner avec poutine : les règles de poutine et la syntaxe de
skipciblé.