Blog / Sécurité

GhostAction : mettre vos clés de déploiement hors de portée

GhostAction vole les secrets GitHub Actions avec des comptes compromis : 30 000 dépôts visés. Auditer un vieux dépôt et protéger vos clés de déploiement.

Par Romain Barberi 8 min de lecture 8 sources

Plus de 30 000 dépôts visés, environ 1 900 comptes GitHub compromis : c’est le bilan que GitGuardian a ajouté le 13 octobre à son suivi de la campagne GhostAction. Le mode opératoire tient en une ligne : avec les identifiants volés d’un développeur, l’attaquant pousse dans ses dépôts un workflow baptisé « sécurité » qui envoie les secrets de la CI vers un serveur à lui. Aucune faille de GitHub n’est en jeu, et c’est ce qui rend l’affaire intéressante pour un projet qui a quelques années : la clé SSH du serveur de production, rangée en secret de dépôt en 2018 et jamais revue depuis, est exactement ce que la campagne cherche. Ma position : on ne gagne pas cette partie en surveillant mieux, on la gagne en faisant en sorte qu’un push, même légitime en apparence, ne suffise plus à lire une clé de déploiement.

Une campagne sans exploit : un compte volé et un fichier YAML

GitGuardian a documenté GhostAction une première fois en septembre 2025. Son article du 7 octobre, signé Gaëtan Ferry et Guillaume Valadon, décrit le retour de la campagne. Entre le 31 août et le 30 septembre 2026, un workflow malveillant a été poussé dans 772 dépôts publics appartenant à 373 comptes et organisations, avec 2 577 secrets visés. Les commits portent l’identité des victimes elles-mêmes, ce qui laisse penser à des identifiants volés plutôt qu’à une intrusion dans GitHub.

La mécanique est rustique. L’attaquant lit l’historique des workflows existants pour récupérer les noms des secrets (VPS_HOST, VPS_SSH_KEY, PYPI_PASSWORD…), les écrit en dur dans un fichier github_actions_security.yml qui se déclenche sur workflow_dispatch et sur tout push, et une seule commande curl envoie les valeurs. Le fichier ne vide pas tout l’environnement : il demande précisément ce que vos propres workflows utilisent.

La variante du 8 octobre, analysée par Socket et StepSecurity, va plus loin. Le fichier s’appelle security-audit.yml, le checkout se fait avec fetch-depth: 0, puis la charge utile parcourt git log -p --all à la recherche de treize motifs : clés AWS, jetons GitHub classiques et fine-grained, clés Anthropic, OpenAI, Slack, SendGrid. StepSecurity le résume ainsi : un secret commité une fois en 2019 et supprimé le lendemain est récolté exactement de la même façon.

Deux nuances sur l’efficacité. En septembre, sur 3 669 exécutions collectées par GitGuardian, 499 seulement ont réellement tourné, dans 32 dépôts, GitHub ayant retenu la plupart des autres en attente d’approbation ; 26 secrets ont été exfiltrés dans 13 dépôts. Et selon la mise à jour du 13 octobre, GitHub empêche le workflow malveillant de s’exécuter depuis le 9 octobre. Mais la vague du 8 octobre, elle, a tourné, et Socket a confirmé des exfiltrations.

Pourquoi un vieux dépôt est la cible idéale

Regardez la répartition des secrets visés en septembre, telle que GitGuardian la publie : 446 clés SSH et identifiants de serveurs de déploiement, 112 identifiants de base de données, 92 identifiants FTP. Ce n’est pas le profil d’une architecture cloud récente, c’est celui d’un déploiement installé il y a des années : un rsync ou un scp vers un VPS, parfois un envoi FTP vers un hébergement mutualisé, avec la clé collée dans les secrets du dépôt par quelqu’un qui a quitté l’équipe depuis.

Trois habitudes rendent ces dépôts vulnérables. Les secrets sont déclarés au niveau du dépôt, donc lisibles par n’importe quel workflow de n’importe quelle branche : il suffit de pousser un fichier sur une branche feature/x pour qu’il s’exécute avec eux. Les jetons d’accès personnels des développeurs ont souvent tous les droits et pas de date d’expiration, et Socket cite les journaux d’infostealers comme source probable des identifiants. Enfin, l’historique Git d’un projet de dix ans contient presque toujours un .env commité par erreur un jour, puis supprimé, que plus personne ne regarde.

Les migrations que j’ai menées m’ont appris à traiter le pipeline de déploiement comme une partie du legacy à part entière. Il a le même âge que le code, et personne ne l’a relu depuis sa mise en place.

Auditer un dépôt en dix minutes

Avant de durcir quoi que ce soit, vérifiez que vous n’êtes pas déjà touché. Commencez par un clone complet (git clone --mirror, ou git fetch --all sur un clone existant), sinon --all ne verra que vos branches locales.

# 1. Qui a ajouté ou modifié un workflow, sur toutes les branches
git log --all --format='%h %ad %an %s' --date=short -- .github/workflows/

# 2. Les noms de fichiers connus de la campagne
git log --all --name-only --format= -- .github/workflows/ | sort -u \
  | grep -E 'github_actions_security|security-audit|security-check'

# 3. Ce que l'attaquant récupère dans vos workflows : les noms des secrets
grep -rhoE 'secrets\.[A-Za-z0-9_]+' .github/workflows/ | sort | uniq -c

# 4. Ce que la variante d'octobre lit dans l'historique
git log -p --all | grep -E 'AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36}|github_pat_|sk-ant-|BEGIN (RSA|OPENSSH) PRIVATE KEY'

Ces quatre commandes ont été exécutées avec Git 2.43 sur un dépôt de démonstration : un workflow de déploiement SSH et FTP, un .env.prod commité puis supprimé en 2019, et une branche feature/x portant un faux github_actions_security.yml sans charge utile. La première commande fait ressortir le commit « Add Github Actions Security workflow » sur la branche, la deuxième le nom du fichier, la troisième les trois secrets FTP_PASSWORD, VPS_HOST et VPS_SSH_KEY, et la dernière la clé AWS supprimée depuis sept ans, deux fois (ajout et suppression).

Le motif de la quatrième commande est volontairement court. Pour un vrai balayage, un outil dédié comme gitleaks ou trufflehog couvre bien plus de formats ; ces lignes servent à savoir en cinq minutes si le problème existe. Côté GitHub, consultez aussi l’historique des exécutions d’Actions depuis le 31 août : StepSecurity recommande de considérer toute exécution terminée du workflow malveillant comme une exfiltration confirmée.

Si vous trouvez quelque chose, l’ordre compte. GitGuardian insiste : faire tourner les secrets ne suffit pas. Il faut d’abord révoquer l’identifiant qui a servi au push (jeton, session, clé SSH du compte), sinon l’attaquant reviendra lire les nouveaux secrets. Ensuite seulement, faites tourner tout ce que le workflow et l’historique exposaient, et supprimez le fichier sur toutes les branches, pas seulement sur la branche par défaut.

Mettre les clés derrière un environnement protégé

La parade principale existe depuis longtemps et reste peu utilisée : les environnements de déploiement. La documentation GitHub est nette : les secrets d’un environnement ne sont disponibles que pour les jobs qui référencent cet environnement, et avec des relecteurs obligatoires, le job n’y accède qu’après approbation.

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

permissions:
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # secrets portés par l'environnement, plus par le dépôt
    steps:
      - uses: actions/checkout@v4
      - run: ./bin/deploy.sh
        env:
          VPS_HOST: ${{ secrets.VPS_HOST }}
          VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }}

Le YAML ne fait pas tout : la protection vient des réglages de l’environnement. Voici ceux que je pose sur un environnement production :

  • les secrets de déploiement déplacés vers l’environnement (gh secret set VPS_SSH_KEY --env production), puis supprimés au niveau du dépôt, sans quoi rien n’a changé ;
  • une politique de branches limitée à main, ou aux branches protégées, en sachant que si aucune branche n’a de règle de protection, la documentation précise que toutes les branches peuvent alors déployer ;
  • un relecteur obligatoire, au moins pour les dépôts où l’on déploie rarement ;
  • une branche main protégée, qui exige une pull request : un workflow poussé sur feature/x ne pourra plus atteindre l’environnement, et un push direct sur main sera refusé.

Deux mesures complètent le dispositif. Côté développeurs, un jeton classique sans le scope workflow ne peut ni ajouter ni modifier un fichier de workflow, selon la documentation des scopes ; c’est une restriction gratuite pour tous les jetons qui servent à autre chose qu’à éditer la CI. Côté dépôt, les push rulesets, disponibles pour les dépôts privés et internes, savent bloquer tout commit qui touche un chemin donné, par exemple .github/workflows/**, en ne laissant passer que les personnes autorisées.

Pour le cloud, le mieux est de ne plus stocker de clé du tout : avec OIDC, le workflow obtient un jeton de courte durée, et la politique de confiance AWS peut exiger un sub de la forme repo:mon-org/mon-depot:environment:production. La documentation met d’ailleurs en garde contre le joker repo:mon-org/mon-depot:*, qui laisserait n’importe quelle branche assumer le rôle.

Les limites

Un environnement protégé ne résiste pas à un compte administrateur compromis : l’administrateur peut retirer la protection, et la seule défense est alors l’authentification forte de ce compte et la durée de vie courte des jetons. Les secrets d’environnement ont aussi un coût : sur un dépôt privé, ils demandent GitHub Pro, Team ou Enterprise. Un relecteur obligatoire ralentit les déploiements, ce qui est un vrai sujet pour une équipe qui livre dix fois par jour, beaucoup moins pour une application en maintenance qui déploie une fois par mois.

Tout le monde ne peut pas passer à OIDC : un hébergement mutualisé en FTP n’a pas de fédération d’identité, et la clé restera une clé. Dans ce cas, les environnements protégés et un compte FTP limité au seul dossier du site sont le minimum. Enfin, les chiffres de la campagne divergent selon les sources (le 9 octobre, Socket parlait de dizaines de milliers de dépôts sans chiffre précis, GitGuardian de plus de 30 000 le 13) ; je n’ai retenu que ceux publiés par les équipes de recherche, et cet article s’appuie sur leurs analyses et sur la documentation de GitHub, sans reproduction de l’attaque elle-même.

Concrètement, par où commencer

  1. Aujourd’hui : lancez les quatre commandes ci-dessus sur vos dépôts qui déploient, en commençant par les plus anciens.
  2. Cette semaine : listez les secrets de dépôt (gh secret list) et déplacez chaque clé de déploiement vers un environnement protégé, puis supprimez la version de dépôt.
  3. Faites l’inventaire des jetons personnels qui ont accès à vos organisations : expiration, scopes, et retrait de workflow partout où il n’est pas nécessaire.
  4. Activez le secret scanning avec la protection au push. Depuis le 13 octobre, GitHub y bloque par défaut les clés d’API Anthropic, y compris sur les dépôts publics gratuits.
  5. Posez la question à l’équipe : si le jeton d’un développeur fuitait demain, quels serveurs pourrait-on atteindre avec, et en combien de commandes ?

La prochaine campagne changera de nom de fichier et de serveur de collecte ; la question de savoir qui peut lire vos clés de production, elle, ne changera pas.

Sources

Sur ce sujet, l’offre de l’atelier : Audit gratuit

À lire aussi

Un socle à migrer, une architecture à fiabiliser ?

Renfort senior en régie, ou forfait à périmètre et prix fixés : l’atelier intervient à distance sur PHP, Symfony, les systèmes distribués et l’adoption des agents de code, avec des tests de caractérisation avant chaque migration.