Aller au contenu principal
Site en cours de refonte — quelques pages peuvent bouger ou évoluer.
01 avril 2026

Une Action GitHub « sécu » se fait détourner : tes secrets partent (et ça pique)

Quand une GitHub Action populaire se fait compromettre, l’attaque vise rarement ton build. Elle vise tes secrets. Voici un plan de défense réaliste, basé sur des incidents récents.

12 min de lecture
48 vues
réactions
Partager :
Une Action GitHub « sécu » se fait détourner : tes secrets partent (et ça pique)

Si tu utilises GitHub Actions en mode « je mets uses: truc/action@v1 et on verra bien », cet article est pour toi. Les compromissions récentes autour d’Actions de sécurité (Trivy, puis des Actions Checkmarx) ont remis un truc très simple au milieu de la table : le maillon faible, ce n’est pas ton code, c’est ta chaîne d’exécution. Et une Action compromise n’a pas besoin d’être subtile. Elle a juste besoin d’être exécutée une fois, au bon endroit, avec les bons droits.

L’angle ici est volontairement terrain : un scénario d’attaque crédible (pas un film), puis un plan de défense qui tient dans une vraie CI, avec ses contraintes et ses compromis. Pas une morale.

Le scénario réaliste : un tag bouge, ton workflow exécute autre chose

Le pattern qu’on voit revenir est brutalement efficace. Une Action populaire est utilisée partout, souvent sur des workflows qui ont accès à des secrets (registry, cloud, tokens internes, clés de signature, etc.). L’attaquant arrive à pousser du code malveillant dans le dépôt de l’Action, ou à faire pointer un tag vers un commit différent. Et toi, dans ton repo, tu continues à appeler la même référence, genre @v1 ou @v0.69.4. Sauf que cette référence ne désigne plus ce que tu crois.

Dans les incidents rapportés, on parle précisément de tags repointés via force-push vers des commits malveillants, et de payloads orientés exfiltration. L’Action s’exécute sur ton runner, elle a le contexte, elle voit des variables, parfois des tokens, et elle peut tenter de les récupérer par plusieurs moyens. Même si tu ne stockes pas « tout » en secrets GitHub, il suffit d’un token de cloud trop large ou d’un registry token réutilisé ailleurs pour que ça devienne un incident majeur.

Le détail qui fait mal, c’est que ça ressemble à une release normale. Tu ne vois pas un gros « malware.exe ». Tu vois un tag qui a l’air légitime, une Action qui s’appelle comme avant, et ton pipeline qui continue à tourner.

Le vrai danger : ce n’est pas « l’Action », c’est son contexte d’exécution

Une GitHub Action, ce n’est pas une librairie importée dans ton code. C’est du code exécuté dans ta CI, avec tes droits. Et ces droits sont souvent plus élevés qu’on ne l’imagine, parce qu’on a empilé des besoins au fil du temps : publier une image Docker, commenter une PR, pousser un tag, déployer sur un environnement, écrire dans un bucket, etc.

Si l’Action compromise tourne sur un job qui a accès à des secrets, elle n’a même pas besoin d’un exploit sophistiqué. Elle peut tenter de lire l’environnement, d’abuser du token GitHub, de fouiller des fichiers de config générés pendant le build, de récupérer des credentials mis en cache, ou de détourner des appels réseau sortants. Et dans la vraie vie, on laisse souvent la sortie réseau ouverte « parce que sinon ça casse ». Donc l’exfiltration passe.

Le piège classique, c’est de se dire « ok, on va juste changer l’Action ». Non. Le sujet, c’est de réduire l’impact si une Action (n’importe laquelle) devient hostile pendant quelques heures.

Mesure n°1 : pin les Actions sur un SHA, pas sur un tag

Je vais être cash : tant que tu utilises des tags mouvants dans une CI qui a des secrets, tu acceptes le risque qu’un tag pointe ailleurs demain. Même si l’éditeur est de bonne foi. Même si le repo est « connu ». Même si tu as confiance. La supply chain s’en fiche de ta confiance.

La défense la plus rentable est de pinner sur un commit SHA. Ça ne rend pas l’Action « sûre », mais ça bloque le scénario « tag repointé → tu exécutes le nouveau code sans le savoir ». Tu réintroduis une étape humaine : pour bouger la référence, il faut modifier ton workflow, donc une PR, donc du review, donc un signal.

Oui, ça ajoute de la maintenance. C’est le prix du contrôle. La bonne nouvelle, c’est que tu peux l’automatiser proprement (Renovate sait gérer les updates d’Actions, par exemple), et tu gardes une trace explicite des upgrades.

name: CI
on:
  push:
    branches: [ main ]

permissions:
  contents: read

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # pin SHA

      # Exemple : au lieu de @v1 / @v0.x
      - name: Run scanner
        uses: aquasecurity/trivy-action@3f0c6c2d8d9c7b2a2f0b5f7f4d8b1f3b9b1e2c3d # pin SHA
        with:
          scan-type: fs
          ignore-unfixed: true

Deux détails importants ici. D’abord, le pin SHA ne remplace pas le review quand tu upgrades. Il le rend simplement inévitable. Ensuite, tu veux aussi éviter les références « confort » comme @main ou @master. C’est pratique… jusqu’au jour où ça ne l’est plus.

Mesure n°2 : permissions minimales sur GITHUB_TOKEN, sinon tu offres un couteau suisse

Le token GitHub injecté par défaut dans les workflows est pratique, mais il est aussi souvent surdimensionné. Beaucoup de repos laissent les permissions par défaut, ou ajoutent des droits « pour que ça marche » sans revenir dessus. Résultat : une Action compromise peut lire/écrire des issues, pousser du code, publier des releases, modifier des artefacts, interagir avec des packages… selon ton contexte.

Ma règle est simple : je mets un bloc permissions: partout, au niveau du workflow ou du job, et je le garde minimal. Si un step a besoin d’écrire, je l’isole dans un job séparé, avec des permissions spécifiques, idéalement derrière une validation d’environnement.

Ce n’est pas sexy, mais c’est incroyablement efficace. Quand un payload tente un truc, il se heurte à « permission denied » au lieu de réussir en silence.

Mesure n°3 : OIDC plutôt que des secrets long-lived (et tant pis si c’est un peu pénible)

Les secrets statiques (clé cloud, token registry, credentials API internes) sont un cadeau pour l’attaquant : si ça sort une fois, ça vit souvent des semaines. L’approche moderne côté CI, c’est d’utiliser OIDC pour obtenir des credentials temporaires, scoppés, expirables, et rattachés à des claims précis (repo, branche, environnement, etc.).

Ce n’est pas un truc « enterprise ». C’est juste une façon de limiter le blast radius. Si une Action compromise arrive à déclencher l’obtention d’un token OIDC, ce token peut expirer vite, et surtout tu peux exiger des conditions strictes côté cloud. Typiquement, « uniquement depuis ce repo », « uniquement depuis main », « uniquement depuis un environnement protégé », et avec des permissions IAM minimales.

Le point à ne pas rater : OIDC ne te sauve pas si tu accordes quand même un rôle admin, ou si tu laisses n’importe quel événement de workflow demander un token. OIDC, c’est une brique. Pas une incantation.

Mesure n°4 : secrets et environnements GitHub, pour arrêter de tout exposer à tout le monde

Si tous tes secrets sont au niveau « repo », tous les jobs les voient potentiellement dès qu’ils tournent dans un contexte autorisé. Je préfère découper : secrets d’environnement (staging/prod), environnements protégés avec reviewers, et déclenchement explicite des jobs qui déploient. L’objectif n’est pas de ralentir tout le monde. L’objectif est d’éviter qu’un job de lint sur une PR ait, par accident, accès à des credentials de prod.

En pratique, ça force à clarifier ce qui est vraiment nécessaire. Et ça évite un anti-pattern que je vois trop souvent : « on a mis le secret ici parce que c’était le plus simple ».

Le piège qui revient tout le temps : pull_request_target et les PR externes

Le moment où beaucoup d’équipes se font piéger, c’est quand elles veulent exécuter des workflows sur des PR venant de forks, tout en gardant accès aux secrets. GitHub a des garde-fous, mais il existe des événements et des patterns (dont pull_request_target) qui peuvent te faire exécuter du code dans un contexte où les secrets sont disponibles. Et si tu checkout le code de la PR et que tu l’exécutes avec des secrets, tu as littéralement construit une passerelle d’exfiltration.

Mon avis : si tu ne comprends pas parfaitement la différence entre pull_request et pull_request_target, tu dois considérer que c’est dangereux par défaut. Et même si tu la comprends, tu dois limiter ce que tu fais dans ce contexte : pas de secrets, pas de déploiement, pas de publication, pas d’accès large au repo.

Runners : l’isolation, c’est là que ça se joue (surtout en self-hosted)

Sur les runners hébergés par GitHub, tu as déjà une isolation et un environnement éphémère relativement propre. Sur des runners self-hosted, c’est une autre histoire. Si ton runner est long-lived, mutualisé, avec des caches partagés, des credentials persistants, ou des volumes Docker qui traînent, une Action compromise peut laisser des traces, récupérer des infos d’un job précédent, ou préparer le terrain pour la suite.

Je n’ai rien contre le self-hosted. J’en utilise. Mais je veux les mêmes propriétés que sur du managed : éphémère si possible, ou au minimum cloisonné par repo/orga, avec une politique de nettoyage agressive et des permissions réseau strictes. Et je refuse l’idée du « gros runner partagé pour toute la boîte » sans segmentation. C’est confortable jusqu’au jour où tu dois enquêter sur une exfiltration.

Détection : accepter que ça arrive, et se donner une chance de le voir

Le meilleur plan du monde n’empêche pas tout. Donc tu veux des signaux. Dans ces incidents, il y a des IOCs possibles : domaines d’exfiltration, changements suspects de tags, comportements réseau inattendus depuis la CI. Ce que je trouve utile, c’est d’avoir au moins deux niveaux de détection.

Le premier niveau, c’est GitHub lui-même : audit logs (sur GitHub Enterprise / orga), surveillance des changements de références, et vigilance sur les PR qui modifient les workflows. Un changement dans .github/workflows doit être traité comme un changement de prod.

Le second niveau, c’est réseau et exécution : egress filtering si tu peux, ou au minimum une visibilité sur les connexions sortantes de tes runners self-hosted. Si tu vois un job CI qui se met à parler à un domaine bizarre, ce n’est pas « peut-être normal ». C’est un incident potentiel.

Mini runbook : quoi faire si tu penses avoir exécuté une Action compromise

Le réflexe à avoir, c’est d’éviter le déni. Si une Action que tu utilises a été compromise sur une fenêtre donnée, et que tes workflows ont tourné dedans, tu dois partir du principe que tout secret accessible à ces jobs est potentiellement exfiltré. C’est dur à avaler, mais ça te fait gagner du temps.

Ensuite, tu agis dans le bon ordre. Tu bloques l’hémorragie en stoppant temporairement les workflows concernés et en pinant une version saine. Puis tu fais une rotation des credentials, pas seulement les secrets GitHub, mais aussi les tokens en aval (cloud, registry, SaaS, clés de signature). Tu révoques ce qui doit l’être, tu invalides ce qui peut l’être, et tu vérifies les accès récents côté providers. Si tu as des artefacts ou des images publiées pendant la fenêtre, tu les considères aussi comme suspectes tant que tu n’as pas tracé le chemin.

Enfin, tu rejoues proprement. Tu rebuild dans un environnement que tu estimes sain, tu purges ce qui doit être purgé (caches, artefacts), et tu documentes la timeline. Le but n’est pas d’écrire un roman. Le but est de savoir quoi répondre la prochaine fois, et d’éviter de redécouvrir les mêmes angles morts.

Mon avis (assumé) : « on a un scanner de sécurité » n’a jamais été une stratégie

Le plus ironique, c’est que ces attaques touchent souvent des Actions « sécurité ». Et c’est logique : elles sont massivement installées, elles tournent tôt dans les pipelines, elles ont souvent accès à beaucoup de contexte, et elles profitent d’un capital confiance énorme. Bref, c’est une cible parfaite.

La bonne posture, ce n’est pas d’arrêter d’utiliser des Actions externes. Ce serait irréaliste. La bonne posture, c’est de les traiter comme du code tiers exécuté avec tes clés. Pin SHA, permissions minimales, OIDC, segmentation, garde-fous sur les PR, runners propres. Ce n’est pas du luxe, c’est le minimum viable d’une CI moderne.

Conclusion : rends l’attaque chère, et l’impact petit

Tu n’empêcheras pas toutes les compromissions upstream. Par contre, tu peux faire en sorte qu’une compromission ne se transforme pas automatiquement en fuite de secrets et en accès cloud persistant. Si tu dois retenir une seule chose : réduis ce que la CI peut faire par défaut, et rends explicite tout ce qui donne du pouvoir.

Et si tu veux aller plus loin, le prochain chantier logique c’est de mettre des contrôles d’intégrité et de provenance plus stricts sur ce que tu exécutes en CI, et d’industrialiser la review des changements de workflows comme tu le ferais pour de l’infra.

Sources

Cet article vous a plu ?

Commentaires

Laisser un commentaire

Entre 10 et 2000 caractères

Les commentaires sont modérés avant publication.

Aucun commentaire pour le moment.

Soyez le premier à donner votre avis !