Aller au contenu
CI/CD & Automatisation medium

Checklist sécurité des workflows GitHub Actions

18 min de lecture

Cette page rassemble les points de contrôle à passer en revue avant de fusionner un workflow GitHub Actions. Elle s'adresse aux développeurs et aux équipes DevSecOps qui relisent du code de pipeline ou conduisent un audit de sécurité. Utilisez-la comme support de revue de code : chaque case cochée ferme une voie d'attaque connue — permissions trop larges, action non épinglée, secret exposé, injection de commandes. Les sections suivent l'ordre d'un workflow : permissions, actions tierces, secrets, déclencheurs, runners, puis les outils qui automatisent ces vérifications.

  • Auditer les permissions du GITHUB_TOKEN au niveau workflow et job
  • Vérifier l'épinglage des actions tierces par SHA de commit
  • Contrôler l'usage des secrets et repérer les fuites dans les logs
  • Détecter les déclencheurs risqués et les injections de commandes
  • Durcir les runners self-hosted et les environnements de déploiement
  • Automatiser ces contrôles avec Scorecard, Checkov et actionlint

Le GITHUB_TOKEN est un jeton généré automatiquement pour chaque exécution. Par défaut, son périmètre dépend des paramètres du dépôt — souvent trop large. Réduire ces droits est la première barrière : un step compromis ne peut écraser le code ou publier un package que si le jeton le lui permet.

Déclarez le bloc permissions: explicitement. Partez de permissions: {} au niveau workflow — aucun droit — puis accordez à chaque job le strict nécessaire. Évitez write-all, qui ouvre toutes les portées d'un coup.

  • Bloc permissions: déclaré explicitement au niveau workflow
  • permissions: {} par défaut, droits accordés job par job
  • Aucun permissions: write-all
  • Permissions write portées au niveau du job qui en a besoin, jamais du workflow
# Aucun droit par défaut, chaque job demande le minimum
permissions: {}
jobs:
test:
permissions:
contents: read
publish:
permissions:
contents: read
packages: write

Les permissions par défaut se règlent aussi côté dépôt, dans les réglages des Actions. Choisir le mode lecture seule garantit qu'un workflow qui oublie son bloc permissions: hérite quand même d'un jeton restreint.

  • Settings > Actions > General : "Read repository contents and packages permissions" sélectionné
  • Les workflows déclenchés par des forks nécessitent une approbation manuelle

Chaque uses: exécute du code écrit par quelqu'un d'autre, avec vos secrets et votre jeton. La compromission de tj-actions/changed-files en 2025 l'a rappelé : une action populaire piégée contamine des milliers de pipelines. Trois réflexes ferment ce vecteur : épingler, évaluer, mettre à jour.

Référencez chaque action par son SHA de commit complet (40 caractères), pas par un tag comme @v4 qui peut être redéplacé sur un autre commit. Ajoutez en commentaire la version lisible pour suivre les mises à jour. Le guide Épingler par SHA détaille la manœuvre.

  • Toutes les actions épinglées par SHA (jamais @v1, @latest, @main)
  • Commentaire de version après chaque SHA pour la lisibilité
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Avant d'ajouter une action du Marketplace, vérifiez son créateur, sa maintenance et sa popularité. Une action obscure ou abandonnée est un vecteur idéal. Pour les actions critiques, relisez le code source de la version que vous épinglez.

  • Actions du Marketplace évaluées avant utilisation
  • Actions officielles (actions/*) ou éditeurs reconnus privilégiées
  • Code source relu pour les actions critiques

Épingler fige une version — y compris ses failles. Dependabot ouvre automatiquement des pull requests de mise à jour pour l'écosystème github-actions, avec le nouveau SHA et le changelog. Reste à les relire avant de fusionner.

  • Dependabot configuré pour l'écosystème github-actions
  • Pull requests de mise à jour relues régulièrement
.github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

Un secret qui fuit dans un log public est compromis pour de bon : il faut le révoquer. Trois angles à contrôler : où les stocker, comment les utiliser sans les exposer, et comment détecter ceux commités par erreur.

Placez les secrets sensibles dans des Environments plutôt qu'au niveau dépôt : vous y ajoutez des règles de protection et limitez les branches autorisées. Pour les fournisseurs cloud, préférez OIDC aux credentials statiques longue durée.

  • Secrets sensibles rangés dans des Environments, pas au niveau repo
  • Aucun secret en clair dans les fichiers de workflow
  • OIDC utilisé pour les cloud providers (pas de credentials statiques)

Passez toujours un secret par un bloc env:, jamais directement dans run: ni en argument de commande visible. Un echo d'un secret, même involontaire, l'écrit en clair dans les logs d'exécution.

  • Secrets passés via env:, jamais interpolés dans run:
  • Aucun echo ${{ secrets.XXX }} ni affichage volontaire d'un secret
- name: Déclencher le déploiement
run: |
curl --fail --silent --show-error \
-X POST https://api.netlify.com/api/v1/sites/blog-stephane-robert/builds \
-H "Authorization: Bearer $NETLIFY_TOKEN"
env:
NETLIFY_TOKEN: ${{ secrets.NETLIFY_TOKEN }}

Un secret commité reste dans l'historique Git même après suppression du fichier. Un scanner comme Gitleaks dans le pipeline et le secret scanning GitHub repèrent ces fuites tôt, avant qu'elles n'atteignent une branche partagée.

  • Gitleaks ou un scanner équivalent intégré au pipeline
  • Secret scanning GitHub activé sur le dépôt

Le déclencheur décide quel code s'exécute et avec quels droits. Certains événements — notamment pull_request_target — mélangent du code non fiable et des secrets : c'est là que se logent les failles les plus graves.

pull_request_target s'exécute avec les secrets du dépôt cible, même pour une pull request issue d'un fork. Checkouter puis exécuter le code de la PR revient à lancer le code d'un inconnu avec vos clés. Le guide pull_request_target détaille les contournements sûrs.

  • Pas de pull_request_target sauf nécessité réelle
  • Si utilisé : jamais de checkout du code de la PR suivi de son exécution
# ❌ pull_request_target qui exécute le code d'un fork : RCE avec vos secrets
name: PR Build
on: pull_request_target
jobs:
build:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm ci && npm run build
# ✅ Déclencheur pull_request : le code du fork tourne sans accès aux secrets
name: PR Build
on: pull_request
permissions: {}
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- run: npm ci && npm run build

Le déclenchement manuel workflow_dispatch accepte des inputs. Restreignez-les avec type: choice (liste fermée) quand c'est possible, et traitez-les comme des données non fiables : via env:, jamais interpolés directement dans run:.

  • Inputs en type: choice quand le domaine de valeurs est connu
  • Inputs consommés via env:, jamais interpolés dans run:
  • Utilisateurs autorisés restreints si l'action est sensible
on:
workflow_dispatch:
inputs:
environment:
type: choice
options:
- staging
- production

L'injection de commandes est la faille la plus courante des workflows. Interpoler avec ${{ }} une donnée contrôlable par un tiers — titre d'issue, nom de branche, commentaire — dans un bloc run: permet d'exécuter du code arbitraire sur le runner. La parade tient en une règle : passer la donnée par une variable d'environnement.

# ❌ Vulnérable : le titre de l'issue est injecté dans le shell
- run: echo "Issue: ${{ github.event.issue.title }}"
# ✅ Sécurisé : le titre passe par une variable d'environnement
- run: echo "Issue: $ISSUE_TITLE"
env:
ISSUE_TITLE: ${{ github.event.issue.title }}

Les contextes suivants sont alimentés par des données externes et ne doivent jamais être interpolés dans un run: :

  • github.event.issue.title
  • github.event.issue.body
  • github.event.pull_request.title
  • github.event.pull_request.body
  • github.event.comment.body
  • github.head_ref
  • github.event.*.user.login

Le runner est la machine qui exécute vos jobs. Sur les runners GitHub-hosted, GitHub la recrée à neuf à chaque job. Sur un runner self-hosted, c'est votre infrastructure — et un job malveillant peut y persister d'une exécution à l'autre.

Un runner self-hosted ne doit jamais servir un dépôt public ni traiter les PR de forks : ce serait offrir l'exécution de code arbitraire sur votre réseau. Privilégiez des runners éphémères, détruits après chaque job.

  • Jamais branché sur un dépôt public
  • Runners éphémères, détruits après chaque job
  • Pools séparés par environnement (dev / staging / prod)
  • Compte d'exécution sans privilèges sudo
  • Logs centralisés et supervision en place

Pour la majorité des cas, les runners GitHub-hosted sont le meilleur choix : isolés, jetables, sans maintenance. Ajoutez un timeout-minutes pour éviter les jobs zombies qui consomment vos minutes sans fin.

  • Runners GitHub-hosted préférés par défaut
  • timeout-minutes défini sur chaque job
jobs:
build:
runs-on: ubuntu-24.04
timeout-minutes: 30

Un Environment GitHub représente une cible de déploiement — staging, production. Il porte des règles de protection et des secrets dédiés, ce qui en fait le bon endroit pour cloisonner les accès de production.

Exigez une approbation manuelle sur l'environment production et limitez les branches autorisées à main. Un wait timer laisse une fenêtre pour annuler un déploiement déclenché par erreur.

  • Environment production avec approbation requise
  • Branches autorisées limitées (main uniquement pour la prod)
  • wait timer configuré pour permettre une annulation

Rangez les secrets de production uniquement dans l'environment production. Des jetons distincts par environment limitent l'impact d'une fuite : un token de staging compromis ne touche pas la prod.

  • Secrets de production isolés dans l'environment production
  • Jetons différents pour chaque environment

Une attestation est une preuve cryptographique liant un artefact à son workflow de build et à son code source. Elle permet de vérifier, avant déploiement, qu'un binaire est bien celui qu'on croit.

Générez une attestation de provenance et un SBOM (Software Bill of Materials — l'inventaire des composants) pour chaque release. Le guide Attestations détaille la mise en place.

  • Attestation de provenance générée pour chaque release
  • SBOM généré et publié avec la release
- name: Générer l'attestation de provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: my-app.tar.gz

Une attestation non vérifiée ne protège de rien. Contrôlez-la avant tout déploiement avec gh attestation verify. Le guide Vérifier les attestations montre comment l'imposer en CI/CD.

  • Attestation vérifiée avant chaque déploiement
Fenêtre de terminal
gh attestation verify my-app.tar.gz --owner mon-org --repo mon-app

Ces contrôles se passent en grande partie automatiquement. Trois outils complémentaires couvrent la syntaxe, la configuration et la posture de sécurité globale d'un dépôt.

OpenSSF Scorecard note un dépôt sur une série de critères de sécurité — permissions du jeton, épinglage des dépendances, workflows dangereux. Visez 10/10 sur les contrôles liés aux Actions.

  • Score Token-Permissions à 10/10
  • Score Pinned-Dependencies à 10/10
  • Aucun Dangerous-Workflow détecté
Fenêtre de terminal
scorecard --local . --checks Token-Permissions,Pinned-Dependencies,Dangerous-Workflow

Checkov analyse les workflows comme du code d'infrastructure et repère les mauvaises configurations : permissions larges, actions non épinglées, secrets exposés.

  • Scan des workflows sans erreur critique
Fenêtre de terminal
checkov -d .github/workflows/ --framework github_actions

actionlint valide la syntaxe des workflows : expressions invalides, clés inconnues, erreurs shell dans les blocs run:. C'est le filet de sécurité le plus rapide à mettre en place — voir le guide actionlint pour l'intégrer en pre-commit.

  • Workflows syntaxiquement valides
Fenêtre de terminal
actionlint .github/workflows/*.yml

La checklist prend tout son sens en revue de pull request. Un workflow modifié mérite la même attention qu'un changement de code applicatif — c'est lui qui détient les droits et les secrets du dépôt.

Six points bloquants à vérifier systématiquement avant d'approuver une pull request qui touche un fichier .github/workflows/.

  1. Permissions explicites et minimales
  2. Actions épinglées par SHA
  3. Aucune injection de commandes possible
  4. Secrets utilisés via env: uniquement
  5. Aucun pull_request_target dangereux
  6. timeout-minutes défini sur chaque job

La sécurité d'un pipeline se dégrade avec le temps : nouvelles actions, permissions qui s'élargissent, SHA qui prennent du retard sur les correctifs. Un audit périodique rattrape cette dérive.

  • Audit trimestriel de l'ensemble des workflows
  • Pull requests Dependabot traitées sans accumulation
  • Permissions réelles comparées aux permissions nécessaires

Voici un workflow CI/CD complet qui applique l'ensemble de cette checklist : permissions minimales, actions épinglées, OIDC pour le déploiement, timeouts et concurrency. Servez-vous-en comme base de départ.

name: Secure CI/CD
on:
push:
branches: [main]
pull_request:
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
test:
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Récupérer le code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Installer Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: npm
- name: Installer les dépendances
run: npm ci
- name: Lancer les tests
run: npm test
build:
needs: test
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Récupérer le code
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Installer Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 20
cache: npm
- name: Installer les dépendances
run: npm ci
- name: Construire l'application
run: npm run build
- name: Publier l'artefact de build
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
with:
name: dist
path: dist/
deploy:
if: github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-24.04
timeout-minutes: 10
environment: production
permissions:
contents: read
id-token: write
steps:
- name: Télécharger l'artefact de build
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: dist
path: dist/
- name: Authentification AWS via OIDC
uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: eu-west-1
- name: Synchroniser le build vers S3
env:
S3_BUCKET: ${{ vars.S3_BUCKET }}
run: aws s3 sync dist/ "s3://$S3_BUCKET"
  • Une checklist n'a de valeur qu'appliquée en revue : passez-la sur chaque pull request qui modifie un workflow.
  • Les permissions minimales et l'épinglage par SHA ferment les deux voies d'attaque les plus exploitées.
  • Un secret ne transite jamais par run: directement : toujours par env:, pour ne pas le voir fuir dans les logs.
  • L'injection de commandes se neutralise en passant les données externes par une variable d'environnement.
  • Un runner self-hosted sur un dépôt public expose votre infrastructure : réservez-le aux dépôts privés et préférez l'éphémère.
  • Scorecard, Checkov et actionlint automatisent ces vérifications — intégrez-les au pipeline pour ne pas dépendre d'une relecture humaine.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn