Skip to content

Cet outil n'est ni affilié à GitHub, Inc. ou à Microsoft Corporation, ni approuvé ou sponsorisé par elles. GitHub et GitHub Actions sont des marques de GitHub, Inc. Les autres noms sont des marques de leurs propriétaires respectifs.

Fuite de secrets GitHub Actions : enquête et exfiltration

Comment les secrets GitHub Actions fuient malgré le masquage *** : workflows piégés, nouvelles branches, sorties encodées, actions tierces. Les preuves à lire.

Publié le 8 min de lecture

En bref. Un workflow peut lire tous les secrets de sa portée, et le masquage ne cache que la valeur exacte : echo "$KEY" | base64 | base64 l'affiche en clair. Les attaquants y arrivent en poussant un workflow modifié sur une nouvelle branche, en abusant de pull_request_target, ou en compromettant une action tierce (tj-actions/changed-files, 2025). Les preuves : workflows.created_workflow_run sur une branche jamais vue dans le journal d'audit, plus l'archive de logs de l'exécution, où l'on voit à la fois le script de l'étape et le blob encodé. Téléchargez les archives avant que quelqu'un ne les supprime, puis renouvelez tous les secrets que le workflow pouvait lire.

GitHub Actions, c'est l'endroit où le code source devient un accès au cloud. Les workflows qui déploient en production détiennent des clés AWS, des jetons de registre et des clés de signature sous forme de secrets. Pour un attaquant qui dispose d'un jeton avec le scope workflow, ou d'une prise dans une action dont vous dépendez, exfiltrer ces secrets est souvent le véritable objectif.

Comment les secrets arrivent chez l'attaquant

VoieCe qui se passeOù cela se voit
Workflow poussé sur une nouvelle brancheL'attaquant pousse un .github/workflows/*.yml modifié sur une branche neuve ; un déclencheur push l'exécute avec les secrets du dépôtgit.push + workflows.created_workflow_run avec un head_branch inédit
Mauvais usage de pull_request_target / workflow_runDu code de PR non fiable s'exécute dans un contexte privilégiéExécutions déclenchées par des PR venant de forks, voir GitHub Security Lab sur les « pwn requests »
Action tierce compromiseUn tag que vous utilisez pointe désormais vers du code malveillantToutes les exécutions qui ont utilisé ce tag, avec la même sortie étrange
Runner auto-hébergéSecrets et jetons lus sur un hôte persistantVoir la sécurité des runners auto-hébergés
Protection d'environnement suppriméeRelecteurs obligatoires ou politiques de branche supprimés : les secrets d'environnement deviennent accessiblesenvironment.remove_protection_rule, environment.delete

ATT&CK dispose désormais d'une technique pour cette famille, T1677 Poisoned Pipeline Execution. L'entrée de glossaire sur l'exécution de pipeline empoisonné explique les variantes directe et indirecte.

Un exemple public

En mars 2025, la CISA a publié une alerte sur la compromission de tj-actions/changed-files (CVE-2025-30066) et de reviewdog/action-setup (CVE-2025-30154), qui lui était liée. Les tags de version de l'action avaient été modifiés pour pointer vers du code malveillant qui déversait les secrets de CI dans les logs des workflows. Dans les dépôts publics, n'importe qui peut lire ces logs. Chaque organisation qui utilisait un tag déplacé a dû vérifier ses logs d'exécution sur la période concernée. La question de détection était : lesquelles de nos exécutions ont affiché un blob encodé dans cette étape ?

Pourquoi le masquage n'est pas un contrôle

GitHub remplace par *** les valeurs des secrets enregistrés dans les logs. La référence d'utilisation sécurisée est explicite : le masquage n'est pas garanti, et un secret transformé (Base64, encodage URL) doit être enregistré séparément pour être masqué. Ainsi :

echo "$AWS_ACCESS_KEY_ID:$AWS_SECRET_ACCESS_KEY" | base64 -w0 | base64 -w0

affiche une longue chaîne qu'aucun masque ne reconnaît. Décodez-la deux fois et vous avez la paire de clés. Les variantes passent par xxd, rev, od, le découpage de la valeur, toJSON(secrets), ou l'envoi direct de la valeur avec curl -d vers un service de capture de requêtes. L'entrée de glossaire sur le masquage des secrets résume ce que le masquage couvre.

Les preuves, par ordre de valeur

1. L'archive de logs de l'exécution

Dépôt → Actions → exécution → ⋯ → Download log archive. Le ZIP contient un log combiné par job et un fichier par étape. On y trouve le script de l'étape (le bloc ##[group]Run … reprend les commandes) et sa sortie. C'est le seul endroit où l'on voit ce que le workflow a fait d'un secret. Par défaut, les logs sont conservés 90 jours, et toute personne ayant un accès en écriture peut les supprimer : téléchargez-les en premier.

L'analyseur lit ces ZIP directement. Deux règles s'y appliquent :

  • actions-secret-exfil (critique) : une ligne de script qui mentionne un secret, un jeton, une clé, l'environnement ou une variable $ et qui la fait passer par un encodeur (base64, xxd, rev, od), vide l'environnement (printenv, env |, toJSON(secrets)) ou envoie des données vers un point de terminaison externe (curl -d, wget --post, /dev/tcp/, services courants de capture de requêtes et de tunnel). Le simple décodage (base64 -d) sans appel réseau est exclu pour limiter les faux positifs. Si la ligne nomme des identifiants AWS, Google Cloud ou Azure, le constat affiche l'étape suivante côté cloud.
  • actions-encoded-output (moyenne) : une ligne de sortie qui contient un jeton base64 ou hexadécimal d'un seul tenant de 60 caractères ou plus. C'est exactement l'allure d'une clé doublement encodée.

Chaque ligne de log apparaît deux fois dans une archive (fichier combiné et fichier par étape). L'analyseur déduplique : une étape malveillante donne un seul constat.

2. Les événements d'exécution de workflow dans le journal d'audit

workflows.created_workflow_run et workflows.completed_workflow_run portent repo, name (le workflow), event (push, pull_request, workflow_dispatch…), head_branch, head_sha, workflow_run_id et l'acteur. La règle workflow-new-branch (faible) signale un workflow qui s'exécute sur une branche où il ne s'est jamais exécuté, pour les déclencheurs push et workflow_dispatch. Seule, elle est bruyante, puisque chaque branche de fonctionnalité la déclenche. Elle devient significative quand l'acteur ou le jeton a d'autres constats, ou quand le nom de branche a l'air d'une maintenance (ci/cache-fix, dependabot-fix) et que la branche a été supprimée ensuite.

3. Les événements Git

Un git.push juste avant l'exécution, depuis le même jeton et la même IP, relie la modification du workflow à l'identifiant. Sans événements Git, il vous reste head_sha, que vous pouvez retrouver dans le dépôt si la branche existe encore.

4. Les changements de métadonnées des secrets

repo.create_actions_secret, org.update_actions_secret et environment.create_actions_secret (règle actions-secret-created, faible) relèvent du travail de CI courant. Ils comptent quand l'acteur est suspect : un attaquant peut remplacer un secret de déploiement pour détourner des artefacts. Notez ce que le journal d'audit n'enregistre pas : la lecture d'un secret par un workflow n'est pas un événement.

Délimiter et remédier

  1. Listez toutes les exécutions du workflow modifié, et de tout workflow sur les branches de l'attaquant, avec les valeurs workflow_run_id du journal d'audit.
  2. Renouvelez tous les secrets que le workflow pouvait lire : secrets du dépôt, de l'organisation (si le dépôt y a accès) et d'environnement. Y compris ceux qui n'ont pas été affichés : un script qui exécute env envoie tout.
  3. Supprimez les logs des exécutions qui ont affiché des secrets, après les avoir archivés pour le dossier, pour que les blobs encodés ne soient plus lisibles dans l'interface.
  4. Supprimez les branches de l'attaquant et relisez .github/workflows sur toutes les branches restantes.
  5. Suivez les clés jusque dans AWS, Google Cloud ou Azure : voir les clés cloud fuitées.

Le durcissement qui réduit vraiment l'exposition

  • Épinglez les actions tierces sur un SHA de commit complet. Pour GitHub, c'est actuellement le seul moyen d'utiliser une action comme une version immuable.
  • Réglez la permission par défaut de GITHUB_TOKEN en lecture seule et accordez l'écriture job par job.
  • Protégez les secrets de déploiement avec des environnements qui exigent des relecteurs et restreignent les branches. L'analyseur signale la suppression de cette protection (environment-protection-removed, élevée).
  • Remplacez les clés cloud stockées par la fédération OIDC : un jeton à courte durée de vie, limité à un dépôt et à une branche, vaut beaucoup moins pour un attaquant.
  • Exigez une relecture CODEOWNERS sur .github/workflows/ et protégez ce dossier avec un ruleset de dépôt.
  • Utilisez les contrôles d'OpenSSF Scorecard (permissions des jetons, dépendances épinglées, workflows dangereux) pour repérer les motifs à risque.

FAQ

Les secrets GitHub Actions peuvent-ils fuir alors qu'ils sont masqués par *** ?

Oui. Le masquage remplace la valeur exacte du secret dans les logs. Un script qui l'encode en base64, l'inverse ou le découpe affiche une chaîne qui ne correspond plus : elle apparaît en clair et se décode. La documentation de GitHub prévient elle-même que le masquage n'est pas garanti pour les secrets transformés.

Le journal d'audit indique-t-il quand un workflow lit un secret ?

Non. Le journal d'audit enregistre la création, la modification et la suppression des secrets, ainsi que la création et la fin des exécutions de workflow. La lecture d'un secret pendant une exécution n'est pas un événement d'audit : le log d'exécution est la seule preuve de ce qu'une étape en a fait.

À lire aussi

Articles liés

Runner auto-hébergé GitHub malveillant ou compromis : les événements du journal d'audit à vérifier, les preuves dans _diag, le périmètre et la reconstruction.
Attaque supply chain GitHub fictive analysée de bout en bout : PAT volé, 38 dépôts clonés, clés AWS affichées par un workflow, protection de branche retirée.
Clés AWS, Google Cloud ou Azure fuitées depuis GitHub Actions ou un dépôt : désactivez la clé, tracez-la dans les logs d'audit cloud, puis passez à OIDC.

Cet outil n'est ni affilié à GitHub, Inc. ou à Microsoft Corporation, ni approuvé ou sponsorisé par elles. GitHub et GitHub Actions sont des marques de GitHub, Inc. Les autres noms sont des marques de leurs propriétaires respectifs.