Aller au contenu
English
English
CI/CD & Automatisation medium

Modéliser la menace d'un workflow GitHub Actions

Read this page in English

30 min de lecture

Un workflow peut passer actionlint, zizmor, poutine et un gate de posture à zéro finding, et rester dangereux. Les scanners détectent des motifs connus ; ils ignorent ce que vos secrets ouvrent, où vos runners sont branchés, et qui consomme vos artefacts. Cette page donne une méthode en sept questions pour établir ce qu'un workflow expose réellement, et décider quoi corriger en premier.

  • Situer ce qu'un scanner voit et ce qu'il ne peut pas voir
  • Dérouler les sept questions sur un workflow quelconque
  • Repérer la frontière de confiance, l'endroit où une donnée non fiable entre
  • Évaluer le rayon d'explosion plutôt que la seule probabilité
  • Choisir quels workflows modéliser en premier, et quand refaire l'exercice

Les scanners raisonnent sur le fichier. Ils lisent un permissions:, une référence d'action, une interpolation dangereuse. C'est précieux, et c'est borné : trois catégories d'information leur échappent par construction.

Ce que le scanner voitCe qu'il ignore
secrets.DEPLOY_TOKEN est passé par env:Que ce jeton ouvre la production de toute l'entreprise
runs-on: [self-hosted, prod]Que cette machine est sur le réseau d'administration
Une image est poussée sur un registreQue quarante dépôts la consomment en base
permissions: contents: writeQue ce dépôt est la source d'une action utilisée ailleurs

Aucune de ces colonnes de droite n'est inscrite dans le YAML. Elles dépendent de votre contexte, et c'est exactement ce qu'une modélisation de menace formalise. Le scanner répond « ce workflow contient-il un motif dangereux connu ». La modélisation répond « que se passe-t-il si celui-ci est compromis ».

L'enchaînement suit le trajet réel d'une attaque : entrer, exécuter, obtenir, sortir, propager. Chaque question se répond en une ligne, et c'est le passage d'une question à la suivante qui révèle les enchaînements dangereux.

1. Qui peut déclencher ce workflow ?
|
v
2. Quel code s'exécute, et d'où vient-il ?
|
v
3. Sur quelle machine, dans quel segment réseau ?
|
v
4. Avec quel jeton, et quelles permissions ?
|
v
5. Quels secrets sont présents dans l'environnement ?
|
v
6. Vers où la machine peut-elle émettre du trafic ?
|
v
7. Quel artefact est produit, et qui le consomme ensuite ?

C'est la question qui détermine toutes les autres, parce qu'elle fixe la population d'attaquants possibles. Un workflow que seuls trois mainteneurs déclenchent et un workflow que n'importe quel inconnu déclenche par une pull request n'appellent pas le même niveau d'exigence.

DéclencheurQui peut l'actionner
push sur une branche protégéeLes personnes ayant le droit d'écriture, après revue
pull_requestN'importe qui, sur un dépôt public
pull_request_targetN'importe qui, et le job dispose des secrets
issue_comment, issuesN'importe qui
workflow_dispatchLes personnes ayant accès au dépôt
schedulePersonne, mais s'exécute sans surveillance

Les deux lignes du milieu concentrent l'essentiel des incidents publics. La ligne schedule mérite une mention particulière : un workflow planifié tourne sans témoin, souvent la nuit, ce qui allonge le délai de détection.

pull_request_target demande d'être décomposé, parce qu'on le résume souvent par « il donne les secrets à un inconnu ». C'est faux dit comme ça, et cela fait manquer la seule étape sur laquelle vous pouvez agir. Trois choses se distinguent :

  • Qui déclenche : n'importe qui, par une pull request venue d'un fork.
  • Quel code tourne par défaut : celui de la branche par défaut du dépôt de base, et non le contenu de la pull request. C'est donc votre workflow, celui que vous avez écrit et relu.
  • Quand cela devient dangereux : au moment où ce workflow checkoute la référence de la pull request et en exécute quelque chose, build, tests ou simple installation de dépendances. C'est là, et seulement là, que le code d'un inconnu rencontre vos secrets et un jeton en écriture.

La documentation GitHub le dit sans détour : « Avoid using this event if you need to build or run code from the pull request. » Le déclencheur n'est pas le problème, l'exécution de code non fiable l'est.

Il ne suffit pas que le déclencheur soit sûr : encore faut-il savoir quel code tourne. Quatre sources se cumulent, et la troisième est la plus souvent oubliée.

  • Le code du dépôt, à la référence checkoutée. Laquelle exactement ? Un ref: pointant vers la HEAD d'une pull request change tout.
  • Les actions tierces, qui sont du code exécuté avec les mêmes droits que vos propres étapes.
  • Les dépendances installées pendant le job. Un npm ci exécute les scripts d'installation des paquets, y compris transitifs.
  • Les outils téléchargés par une action, dont la version n'est pas toujours figée.

Les runners hébergés par GitHub de la gamme standard sont des machines jetables, sur l'internet public, détruites après le job. Cette propriété tient au type de runner, pas au fait qu'il soit hébergé : vérifiez-la pour la gamme que vous utilisez plutôt que de la supposer acquise. Un runner auto-hébergé, lui, est votre infrastructure, avec vos accès réseau, et il persiste sauf configuration contraire.

La question à se poser n'est pas « ce runner est-il durci », mais « que joint cette machine » : bases internes, registres, API d'administration. Le cloisonnement par runner groups traite ce point en détail.

Le GITHUB_TOKEN du job, avec les permissions déclarées. La question utile est de traduire chaque permission en capacité offensive : contents: write permet de modifier le code, donc d'ajouter un workflow, donc de persister. packages: write permet de publier une version piégée sous votre nom. id-token: write permet d'obtenir un jeton cloud, si une trust policy l'accepte.

Listez ce qui est réellement présent dans l'environnement du job, pas ce que le workflow est censé utiliser. Un secret d'environnement n'est délivré qu'après validation des règles de protection ; un secret de dépôt est là dès le démarrage du job, qu'on s'en serve ou non.

Pour chaque secret, la question qui compte n'est pas « est-il masqué » mais « qu'ouvre-t-il, et combien de temps ». Un jeton statique de production vaut infiniment plus qu'un jeton OIDC de dix minutes restreint à un dépôt.

Toute exfiltration a besoin d'un canal de sortie. Par défaut, un runner joint n'importe quelle adresse. Restreindre les destinations autorisées ne prévient pas la compromission, et ne la rend pas inoffensive : elle en réduit l'exfiltration et le rayon d'explosion. Ce qui est déjà beaucoup, puisque le secret volé ne part plus vers une adresse arbitraire et que la tentative devient un échec de job, donc un signal.

Ce qu'un filtrage de sortie ne bloque pas mérite d'être su, sous peine de s'en remettre à une protection qu'on croit totale : un code compromis peut encore altérer un artefact publié, empoisonner un cache réutilisé par les exécutions suivantes, modifier le workspace, ou se servir du GITHUB_TOKEN vers les points d'accès GitHub restés autorisés. Le canal de sortie ferme la porte de l'exfiltration, pas celle du sabotage.

C'est la question de la propagation, et celle qu'on oublie le plus. Un workflow compromis qui ne produit rien nuit à un seul dépôt. Un workflow compromis qui publie une image consommée par quarante services, ou une action réutilisée par toute l'organisation, transforme un incident local en incident de chaîne d'approvisionnement.

La notion centrale tient en une phrase : la frontière de confiance est l'endroit où une donnée que vous ne contrôlez pas entre dans un contexte que vous contrôlez. Un workflow dangereux est presque toujours un workflow où cette frontière est franchie sans qu'on s'en aperçoive.

Les entrées non fiables sont peu nombreuses et se reconnaissent :

  • le contenu d'une pull request de fork, code et fichiers de configuration ;
  • les champs libres remplis par un tiers : titre et corps d'issue ou de pull request, commentaire, nom de branche, message de commit ;
  • les entrées de workflow_dispatch, saisies par un humain ;
  • les artefacts d'une autre exécution, dans un workflow déclenché par workflow_run ;
  • les réponses d'API externes consommées pendant le job.

Le test se formule ainsi : cette valeur peut-elle être choisie par quelqu'un qui n'a pas le droit d'écrire sur le dépôt ? Si oui, elle est non fiable, et tout ce qu'elle atteint devient suspect.

Le rayon d'explosion, plus utile que la probabilité

Section intitulée « Le rayon d'explosion, plus utile que la probabilité »

Classer les risques par probabilité conduit à repousser indéfiniment les scénarios rares. Or en sécurité de chaîne d'approvisionnement, les incidents marquants sont précisément des événements qu'on jugeait improbables. Le classement par rayon d'explosion est plus opérant : il demande ce que coûte l'incident, pas sa fréquence.

RayonCe qui est atteintExemple
Le jobUne exécution, rien ne persisteUn runner hébergé sans secret
Le dépôtCode, releases, paquets de ce dépôtcontents: write détourné
L'organisationPlusieurs dépôts, secrets partagésSecret d'organisation exfiltré
Les consommateursLes utilisateurs de vos artefactsImage ou action publiée piégée
L'infrastructureRéseau interne, productionRunner auto-hébergé compromis

Les deux dernières lignes justifient à elles seules l'exercice : ce sont celles où la remédiation dépasse largement votre dépôt, et où il faut prévenir des tiers.

Prenons un workflow de publication réaliste : déclenché à la publication d'une release, il construit une image, la pousse sur un registre, l'atteste et la signe.

QuestionRéponseVerdict
Qui déclenchePublication d'une release, donc les mainteneursPopulation restreinte
Quel codeLe dépôt au tag, plus sept actions tierces épinglées par SHASurface tierce réelle
Quelle machineRunner hébergé, jetableFaible
Quel jetonpackages: write, id-token: write, attestations: write, contents: writeÉlevé, mais justifié
Quels secretsLe GITHUB_TOKEN du job, aucun secret statiqueBon
Sortie réseauNon restreintePoint faible
ArtefactImage publique consommée par des tiersRayon maximal

La lecture croisée donne la priorité immédiatement, et elle n'est pas celle qu'on attendait. Le risque n'est ni le déclencheur, ni le jeton : c'est la combinaison de la dernière ligne et de l'avant-dernière. Une action tierce compromise parmi les sept dispose d'un canal de sortie libre et d'un artefact à large diffusion.

Les deux corrections prioritaires en découlent : restreindre le trafic sortant du job de publication, et maintenir les épinglages pour réduire la fenêtre pendant laquelle une action compromise reste en place. Le durcissement des permissions, qui aurait été le réflexe, arrive après.

Une analyse faite de tête se perd, et surtout elle ne se compare pas. Six mois plus tard, personne ne sait si le point faible relevé a été traité, ni quelles réponses ont changé depuis. Le remède est un fichier, rangé dans le dépôt à côté des workflows qu'il décrit, qui passe en revue de code comme le reste.

.github/threat-models/release.yml
workflow: .github/workflows/release.yml
revu_le: 2026-09-22
revu_par: equipe-plateforme
reponses:
qui_declenche: "Publication d'une release, donc les mainteneurs"
quel_code: "Le dépôt au tag, plus 7 actions tierces épinglées par SHA"
quelle_machine: "Runner hébergé standard, jetable"
quel_jeton: "packages:write, id-token:write, attestations:write, contents:write"
quels_secrets: "GITHUB_TOKEN du job uniquement, aucun secret statique"
sortie_reseau: "Non restreinte"
quel_artefact: "Image publique consommée par des tiers"
frontiere_de_confiance: >-
Le code tiers entre par les 7 actions épinglées. Tout le reste du job
est sous contrôle des mainteneurs.
rayon_d_explosion: consommateurs
actions:
- quoi: "Restreindre le trafic sortant du job de publication"
etat: a_faire
echeance: 2026-10-15
- quoi: "Rejouer l'épinglage des actions (npm run actions:update)"
etat: fait
le: 2026-09-22

Trois champs font tout le travail et sont ceux qu'on a envie de sauter. La date de revue dit si l'analyse est encore valable. La frontière de confiance nomme par où entre le code que vous ne contrôlez pas, ce qui est la question à laquelle un attaquant, lui, a déjà répondu. Le rayon d'explosion fixe la priorité entre plusieurs workflows, sans discussion d'opinion.

La liste d'actions transforme enfin le constat en travail suivi : un point faible sans échéance ni état n'est pas une décision, c'est une observation.

Modéliser tous les workflows d'une organisation n'a pas de sens. Trois critères désignent ceux qui le méritent, et un workflow qui en cumule deux passe en tête.

  1. Il est déclenchable par un tiers : pull_request sur dépôt public, pull_request_target, issue_comment.

  2. Il dispose de secrets ou de permissions d'écriture : publication, déploiement, signature.

  3. Il produit quelque chose que d'autres consomment : image, paquet, action réutilisable, workflow réutilisable.

L'exercice se refait sur événement, pas sur calendrier. Quatre changements invalident une modélisation existante : l'ajout d'un déclencheur, l'ajout d'un secret ou d'une permission, le passage à un runner auto-hébergé, et le fait qu'un artefact gagne des consommateurs. Les trois premiers se voient dans le diff, le quatrième non, et c'est celui qui transforme silencieusement un risque local en risque de chaîne.

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

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

  • Un workflow peut passer tous les scanners et rester dangereux : ils voient des motifs connus, pas ce que vos secrets ouvrent ni où vos runners sont branchés.
  • La méthode tient en sept questions qui suivent le trajet d'une attaque : déclencheur, code, machine, jeton, secrets, sortie réseau, artefact.
  • La frontière de confiance est l'endroit où une donnée choisie par quelqu'un sans droit d'écriture entre dans un contexte privilégié.
  • Classez par rayon d'explosion plutôt que par probabilité : les incidents marquants étaient tous jugés improbables.
  • Un workflow sans canal de sortie réduit l'exfiltration et le rayon d'explosion, sans empêcher le sabotage d'un artefact ou d'un cache.
  • La question la plus oubliée est la septième : qui consomme l'artefact produit, et donc jusqu'où l'incident se propage.
  • Modélisez en priorité les workflows déclenchables par un tiers, porteurs de secrets ou producteurs d'artefacts diffusés, et refaites l'exercice sur événement, pas à date fixe.
  • Sécuriser pull_request_target : Le déclencheur qui franchit la frontière de confiance par construction, et les gardes qui la rétablissent.
  • OIDC : authentification sans secrets : Réduire la valeur de la cinquième question en remplaçant les jetons statiques par des jetons de quelques minutes.
  • Checklist sécurité : Le passage systématique qui transforme les conclusions de cette modélisation en points de contrôle avant fusion.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens ce site gratuitement, sans publicité, sans profilage et sans compte à créer. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn