Changer le thème

Sécurité GitHub Actions : 3 protections clés après l'incident tj-actions

Easton editorial illustration: large CI pipeline shield, immutable commit block, secret vault, token-scope ring, audit trail

En mars 2025, un GitHub Advisory a bouleversé ma façon de voir les choses. tj-actions/changed-files — une Action que j’utilisais depuis trois ans — a été signalée sous CVE-2025-30066. Plus de 23 000 dépôts se sont retrouvés du jour au lendemain exposés au risque de fuite de Secrets.

Ce qui m’a vraiment glacé, c’est la méthode d’attaque. L’attaquant a d’abord volé un PAT sur un projet, puis modifié les tags de version de tj-actions pour faire pointer du code auparavant sûr vers un script malveillant. Les Secrets — clés AWS, mots de passe de base de données, tokens API — ont alors coulé discrètement vers des logs publics.

J’étais assez démoralisé. Mon propre pipeline CI/CD était devenu le tremplin de l’attaquant. J’ai fouillé tous mes dépôts : références de version des Actions, permissions GITHUB_TOKEN, paramètres des journaux d’audit. Une journée entière — et j’ai réalisé combien de détails j’avais ignorés.

Cet article est la checklist de protection que j’ai tirée de ce moment de panique. Nous parlerons des schémas d’attaque supply chain, des bonnes pratiques de gestion des Secrets, des détails du contrôle des permissions GITHUB_TOKEN, et des nouveautés de la feuille de route sécurité GitHub 2026 qui valent la peine d’être préparées dès maintenant.

L’essentiel : tout cela se configure aujourd’hui, sans attendre de nouvelles fonctionnalités.

L’attaque supply chain CI/CD vue à travers tj-actions

Comment l’attaque s’est produite

Commençons par tj-actions/changed-files. Cette Action est très populaire sur GitHub — elle détecte quels fichiers ont changé dans une PR, et beaucoup de pipelines CI/CD s’en servent. Plusieurs de mes projets l’utilisaient pour décider des déploiements incrémentaux.

La chaîne d’attaque ressemblait à ceci :

L’attaquant a d’abord volé un PAT (Personal Access Token) sur le projet reviewdog/action-setup. Ce PAT avait des droits d’écriture sur le dépôt. Avec ce token, il a modifié les tags de version du dépôt tj-actions — le tag v45, qui pointait vers du code sûr, a été discrètement redirigé vers un nouveau commit contenant une logique malveillante.

Les projets qui utilisaient encore uses: tj-actions/changed-files@v45 ont téléchargé le code malveillant sans le savoir. Le script faisait une chose simple : imprimer tous les Secrets de l’environnement CI/CD dans les logs. Les logs étant publics, les Secrets ont fuité.

Selon le GitHub Advisory, plus de 23 000 dépôts ont été touchés. L’équipe sécurité de Coinbase a ensuite révélé que l’attaque avait affecté les données de plus de 70 000 clients.

Mon erreur : référence par tag vs pinning SHA

En inspectant mes dépôts, j’ai constaté que beaucoup utilisaient des références par tag :

# Mauvais exemple : tag mutable
- name: Check changed files
  uses: tj-actions/changed-files@v45

Les tags sont vivants. Le mainteneur (ou un attaquant disposant des droits d’écriture) peut les faire pointer vers un nouveau commit à tout moment. Vous croyez référencer v45, mais le code exécuté peut être du code compromis.

La bonne pratique est d’épingler avec le SHA complet :

# Bonne pratique : SHA complet
- name: Check changed files
  uses: tj-actions/changed-files@6cbf527e7a7b6d61c4e7f25e5ce5f7b7c8f3c72a

Le SHA est immuable. Tant que vous ne mettez pas à jour la référence, c’est toujours le même code qui s’exécute.

En corrigeant, je me suis dit : c’est pourtant une base de sécurité évidente — comment ai-je pu l’ignorer si longtemps ?

Le coût de confiance des Actions tierces

L’incident tj-actions expose aussi un autre problème : nous faisons confiance trop facilement aux Actions tierces.

Une Action avec beaucoup d’étoiles et d’utilisateurs, et on la glisse directement dans le CI/CD de production. Mais qui sait si le mainteneur est rigoureux en sécurité ? Si son PAT peut être volé ?

La feuille de route sécurité GitHub 2026 évoque une solution : le verrouillage des dépendances au niveau workflow. Comme un package-lock.json, tous les SHA des Actions seraient figés dans un fichier de lock. La fonctionnalité n’est pas encore disponible, mais nous pouvons déjà le faire manuellement — SHA pour chaque Action, audits réguliers.

Une autre recommandation : réduire le nombre d’Actions tierces. Si une Action officielle suffit, évitez le tiers. Pour détecter les fichiers modifiés, actions/checkout combiné à un script shell peut suffire — pas besoin de tj-actions.

Trois niveaux de gestion des Secrets et pratiques avancées

Les trois niveaux de stockage GitHub Secrets

Les Secrets intégrés à GitHub se répartissent en trois niveaux : organisation, dépôt et environnement.

Les Secrets d’organisation peuvent être partagés entre plusieurs dépôts — idéal pour les clés AWS ou tokens cloud génériques. Les Secrets de dépôt ne sont visibles que dans ce dépôt — adaptés aux mots de passe de base de données propres au projet. Les Secrets d’environnement sont plus fins et peuvent s’appuyer sur des règles de protection — par exemple exiger l’approbation d’une PR avant d’accéder aux Secrets de production.

Côté stockage, GitHub chiffre avec libsodium sealed box. Une fois écrit, un Secret ne peut plus être relu — seulement accédé à l’exécution via le contexte secrets :

steps:
  - name: Deploy to AWS
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Une erreur fréquente consiste à interpoler les Secrets directement dans une commande shell :

# Dangereux : les Secrets peuvent apparaître dans les logs
- name: Configure AWS
  run: aws configure set aws_access_key_id ${{ secrets.AWS_ACCESS_KEY_ID }}

Si la valeur contient des caractères spéciaux ou si la commande échoue, les logs peuvent exposer le Secret. La bonne approche passe par des variables d’environnement :

# Sûr : passage par variable d'environnement
- name: Configure AWS
  env:
    AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  run: aws configure set aws_access_key_id "$AWS_ACCESS_KEY_ID"

Masquage dynamique : ::add-mask::

Certaines données sensibles ne sont pas des Secrets préenregistrés, mais générées à l’exécution — par exemple un token temporaire produit par un script. Utilisez alors ::add-mask:: pour masquer dynamiquement :

- name: Generate temporary token
  run: |
    token=$(generate-token.sh)
    echo "::add-mask::$token"
    echo "TOKEN=$token" >> $GITHUB_ENV

La valeur masquée s’affiche *** dans les logs. Attention : le masquage doit intervenir avant toute impression. Si la valeur est loguée d’abord, le masquage ne la efface pas.

Approche avancée : intégration HashiCorp Vault OIDC

Si votre projet exige un niveau de sécurité élevé, les Secrets GitHub intégrés peuvent ne pas suffire. Des credentials longue durée stockés sur GitHub : si le dépôt est compromis, tout part avec.

Une meilleure option est HashiCorp Vault avec OIDC (OpenID Connect) pour un accès sans credential stocké. GitHub Actions prouve son identité à Vault ; Vault vérifie et délivre un token court. Aucun credential longue durée sur GitHub.

Les étapes de configuration ressemblent à ceci :

Étape 1 : configurer le rôle OIDC dans Vault

Vault doit faire confiance au fournisseur OIDC de GitHub. Configurez un rôle qui définit quels dépôts peuvent obtenir quels Secrets :

resource "vault_jwt_auth_backend_role" "github_actions" {
  backend        = "jwt"
  role_name      = "github-actions-role"
  bound_audiences = ["https://github.com/your-org"]
  user_claim     = "repository"
  role_type      = "jwt"
  
  token_policies = ["ci-policy"]
  token_ttl      = "1h"
}

Étape 2 : utiliser vault-action dans GitHub Actions

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Import Secrets from Vault
        uses: hashicorp/[email protected]
        id: vault
        with:
          url: https://vault.example.com:8200
          role: github-actions-role
          method: jwt
          secrets: |
            secret/data/ci/aws accessKey | AWS_ACCESS_KEY_ID ;
            secret/data/ci/aws secretKey | AWS_SECRET_ACCESS_KEY
      
      - name: Deploy with AWS credentials
        run: |
          echo "Accessing AWS with Vault-provided credentials"
          aws s3 ls

Dans ce flux, vault-action s’authentifie auprès de Vault avec le token OIDC fourni par GitHub. Vault renvoie les Secrets injectés en variables d’environnement. Le token expire après 1 h ; la prochaine exécution en obtient un nouveau.

Azure Key Vault propose une intégration OIDC similaire, avec azure/login et Azure Key Vault Secrets.

Contrôle des permissions GITHUB_TOKEN en pratique

Qu’est-ce que GITHUB_TOKEN

Chaque exécution de workflow GitHub Actions reçoit automatiquement un GITHUB_TOKEN. C’est un token OAuth temporaire pour opérer sur le dépôt courant — créer une Release, pousser du code, commenter une PR.

Le problème : GITHUB_TOKEN a par défaut des permissions trop larges.

Dans les anciennes versions de GitHub Actions, GITHUB_TOKEN avait quasiment tous les droits en lecture/écriture. Un workflow pouvait modifier le dépôt, créer des branches, pousser du code. Si un workflow est détourné (par exemple via un script malveillant déclenché par une PR), GITHUB_TOKEN devient l’arme de l’attaquant.

Configuration complète de la clé permissions

GitHub a introduit la clé permissions en avril 2021 pour contrôler précisément la portée de GITHUB_TOKEN.

La syntaxe de base :

permissions:
  actions: read|write|none      # Gestion des Actions
  contents: read|write|none     # Contenu du dépôt
  issues: read|write|none       # Issues
  packages: read|write|none     # GitHub Packages
  pull-requests: read|write|none # Pull requests
  security-events: read|write|none # Événements de sécurité
  deployments: read|write|none  # Déploiements
  statuses: read|write|none     # Statuts de commit

Mécanisme clé : dès que permissions est défini dans un workflow, toute permission non spécifiée devient none. C’est la frontière du moindre privilège.

Permissions au niveau workflow vs job

permissions peut être défini à deux niveaux : workflow (global) et job (local).

Les permissions au niveau workflow s’appliquent à tous les jobs :

name: CI Pipeline
permissions:
  contents: read    # Tous les jobs : lecture seule du dépôt
  issues: write     # Tous les jobs : écriture sur les Issues

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Lint with read-only access"
  
  test:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Test with read-only access"

Les permissions au niveau job peuvent surcharger le workflow :

name: Release Pipeline
permissions:
  contents: read    # Lecture seule par défaut

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read  # Hérite de la lecture seule
    steps:
      - run: echo "Build needs read only"
  
  release:
    runs-on: ubuntu-latest
    permissions:
      contents: write  # Écriture pour créer la Release
      packages: write    # Publication sur Packages
    steps:
      - name: Create Release
        uses: actions/create-release@v1

Cas concret : sur un projet, le job build ne lit que le code ; le job release crée la Release et pousse l’image Docker. En isolant les permissions par job, même si build est compromis, l’attaquant ne peut pas écrire dans le dépôt.

Mode de permissions au niveau dépôt

Outre la configuration dans les workflows, GitHub propose deux modes dans les paramètres du dépôt :

  • Permissive : GITHUB_TOKEN a par défaut tous les droits en lecture/écriture. S’applique quand le workflow ne définit pas permissions.
  • Restricted : GITHUB_TOKEN n’a par défaut que la lecture sur contents et packages. Le workflow doit déclarer explicitement les droits d’écriture.

Recommandation : passer tous les dépôts en mode Restricted. Dans Dépôt > Settings > Actions > General, section « Workflow permissions », choisissez « Read repository contents and packages permissions ».

Ainsi, même sans clé permissions dans un workflow, il n’y a pas de sur-privilège. La frontière de sécurité est verrouillée au niveau du dépôt.

Journaux d’audit et conformité

Les exécutions de workflow dans les journaux d’audit

Depuis février 2021, GitHub intègre les exécutions de workflow Actions aux journaux d’audit de l’organisation. Vous pouvez tracer qui a déclenché quel workflow, avec quelles permissions, et quels Secrets ont été accédés.

Accès : Organisation > Settings > Security > Audit log.

Champs importants :

  • action : type d’événement, par ex. workflow_run.create, workflow_run.complete
  • actor : déclencheur — utilisateur, App ou github-actions[bot]
  • repo : chemin du dépôt
  • token_scopes : portée des permissions du token utilisé
  • request_id : identifiant de requête pour corréler les logs

Usage concret : en cas d’exécution anormale, remonter la source via les journaux d’audit. Si un Secret est accédé de façon suspecte, filtrer les événements workflow_run pour identifier le déclencheur.

API d’audit entreprise

Avec GitHub Enterprise, l’Audit Log API permet d’interroger toutes les opérations :

curl -H "Authorization: Bearer YOUR_TOKEN" \
  "https://api.github.com/enterprises/YOUR_ENTERPRISE/audit-log?phrase=workflow_run"

La réponse JSON contient le détail des événements. Export possible vers un SIEM (Splunk, Datadog, etc.) pour une surveillance continue.

Conformité en bref

Pour SOC 2 ou ISO 27001, la sécurité CI/CD est incontournable. Les journaux d’audit en sont la preuve directe — ils montrent que vous pouvez tracer l’activité CI/CD, détecter les anomalies et réagir.

Configuration recommandée : exporter les journaux vers un système externe et définir des alertes sur les événements clés, par exemple :

  • Pic soudain du taux d’échec des workflows
  • Accès anormal aux Secrets (lectures massives en peu de temps)
  • Déclenchement depuis une IP inconnue

Feuille de route sécurité GitHub 2026

GitHub a publié en mars 2026 une feuille de route sécurité Actions avec six évolutions majeures. Certaines sont déjà disponibles, d’autres en développement.

Verrouillage des dépendances au niveau workflow

C’est la réponse officielle aux attaques de type tj-actions. Comme un package-lock.json, toutes les références d’Actions sont verrouillées sur leur SHA. Le workflow peut encore afficher uses: tj-actions/changed-files@v45, mais le fichier de lock enregistre le SHA correspondant. Quand le mainteneur met à jour l’Action, le lock ne change pas automatiquement — vous devez valider et mettre à jour manuellement.

La fonctionnalité n’est pas encore en ligne ; le pinning SHA manuel reste la solution d’aujourd’hui.

Pare-feu sortant natif couche 7

Contrôle natif de l’accès réseau externe des pipelines CI/CD. Par exemple limiter un workflow à votre API AWS, sans accès libre à Internet.

Aujourd’hui, cela passe par des runners self-hosted et des politiques réseau personnalisées. En 2026, les runners cloud GitHub pourront aussi configurer un pare-feu sortant.

Scoped Secrets

Contrôle plus fin de la portée des Secrets — par exemple un Secret accessible uniquement depuis une branche ou un job donné. Aujourd’hui, la granularité s’arrête au dépôt ou à l’environnement.

Contrôles d’exécution pilotés par politique

Définir des frontières de confiance, des approbations et des portes de vérification. Par exemple exiger une approbation manuelle pour toute PR depuis un fork, ou imposer un scan de sécurité avant l’exécution d’un workflow.

Proche des règles de protection d’environnement, mais plus fin et plus expressif.

Actions Data Stream

Visibilité en temps réel de l’activité CI/CD. Comme les journaux d’audit, mais en flux push plutôt qu’en requête a posteriori — intégration SIEM pour la surveillance en direct.

Attributs personnalisés OIDC

Renforcement de l’authentification auprès des fournisseurs cloud. Le token OIDC peut porter des attributs supplémentaires — labels de dépôt, branche, etc. Vault ou AWS peuvent s’en servir pour des décisions d’autorisation plus fines.

Conclusion

Depuis l’incident tj-actions, trois leçons ressortent :

Première : épingler par SHA. N’utilisez pas de tags pour les Actions tierces — utilisez le SHA complet. C’est la leçon de 23 000 dépôts.

Deuxième : moindre privilège. GITHUB_TOKEN est trop permissif par défaut — limitez avec permissions et passez le dépôt en mode Restricted.

Troisième : journaux d’audit. Les événements Actions y sont intégrés — surveillez les exécutions anormales et exportez vers un système de monitoring.

Tout cela se configure maintenant. Pas besoin d’attendre la feuille de route 2026 ni de nouveaux outils. Ouvrez vos dépôts, vérifiez les références d’Actions, les permissions et les journaux d’audit. Une demi-heure suffit pour la protection de base.

Au fond, la sécurité CI/CD n’est pas de la haute technologie — ce sont des habitudes de détail. Chaque SHA épinglé, chaque permission retirée réduit le risque. tj-actions nous rappelle : l’attaquant n’a pas besoin de moyens sophistiqués — il suffit qu’on néglige un petit détail.

Configurer la sécurité GitHub Actions

Du pinning SHA au contrôle des permissions : trois étapes clés pour renforcer la sécurité CI/CD.

⏱️ Estimated time: 30 min

  1. 1

    Step 1: Épingler les Actions par SHA

    Parcourez tous les fichiers de workflow et remplacez uses: action@tag par uses: action@full-sha pour éviter qu'un tag soit modifié. Utilisez des outils comme pinact pour convertir les références existantes en lot.
  2. 2

    Step 2: Configurer la clé permissions

    Ajoutez permissions au niveau workflow ou job, en n'accordant que les droits nécessaires. Exemple : permissions: contents: read pour la lecture seule, permissions: contents: write pour créer une Release. Passez le dépôt en mode Restricted comme filet de sécurité.
  3. 3

    Step 3: Activer la surveillance des journaux d'audit

    Consultez les événements d'exécution de workflow Actions dans Organisation > Settings > Security > Audit log. Configurez des alertes sur : pic de taux d'échec, accès anormal aux Secrets, déclenchement depuis une IP inconnue. Exportez les logs vers un SIEM pour une surveillance continue.

FAQ

Pourquoi épingler par SHA plutôt que par tag ?
Les tags sont mutables : le mainteneur ou un attaquant peut les faire pointer vers un nouveau commit. Le SHA est fixe — tant que vous ne mettez pas à jour la référence, le code exécuté ne change pas. L'incident tj-actions a démontré ce risque.
Comment contrôler les permissions de GITHUB_TOKEN ?
Utilisez la clé permissions pour déclarer précisément les droits requis. Dès que permissions est défini, les permissions non spécifiées passent automatiquement à none. Recommandation : mode Restricted dans Dépôt > Settings > Actions > General.
Comment détecter qu'un workflow a été compromis ?
Consultez les événements workflow_run dans les journaux d'audit de l'organisation : accès anormaux, IP inconnues, échecs fréquents. Exportez vers un SIEM et configurez des alertes automatiques.
Quels avantages OIDC + Vault par rapport aux Secrets intégrés ?
Les Secrets GitHub sont des credentials longue durée : si le dépôt est compromis, tout fuit. OIDC + Vault permet un accès sans credential stocké — GitHub prouve son identité à Vault, obtient un token court (1 h), ce qui réduit fortement la fenêtre d'attaque.
Peut-on utiliser des Actions tierces ?
Oui, avec prudence. Privilégiez les Actions officielles, épinglez par SHA, auditez les dépendances régulièrement. La feuille de route 2026 prévoit un verrouillage des dépendances au niveau workflow pour gérer les Actions tierces plus sûrement.

12 min de lecture · Publié le: 16 mai 2026 · Mis à jour le: 27 juil. 2026

Commentaires

Connectez-vous avec GitHub pour laisser un commentaire

Easton BlogEaston Blog