Aller au contenu
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, avec les secrets du dépôt cible
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.

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.

Un runner hébergé par GitHub est une machine jetable, sur l'internet public, détruite après le job. Un runner auto-hébergé 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, mais la rend stérile : le secret volé ne part nulle part, et la tentative devient un échec de job, donc un signal.

C'est la question de la propagation, et celle qu'on oublie le plus. Un workflow compromis qui ne produit rien nuit à un 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 est 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.

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 rend une compromission stérile : le secret volé ne part nulle part et la tentative devient un signal.
  • 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 +700 guides gratuits, sans pub ni tracking. 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