Une variable de CI marquée « masquée » ou « protégée » a l’air prise en charge. Elle est stockée chiffrée, cachée du log de job, restreinte à certaines branches. À partir de là, la plupart des équipes arrêtent de s’en inquiéter, ce qui est exactement le mauvais moment pour arrêter : le masquage contrôle seulement si la valeur est affichée. Il ne dit rien sur la durée pendant laquelle cette valeur reste valide, sur ce qui se passe le jour où elle fuit quand même, ni sur ce qu’un job compromis peut en faire entre-temps. C’est dans cet écart entre « caché » et « sûr » que se produisent la plupart des incidents de secrets en CI, et c’est aussi ce qu’un motif plus récent, l’identité fédérée, est conçu pour fermer.

Comment un credential statique fuite vraiment

Trois chemins ordinaires expliquent la majorité des fuites, et aucun ne demande un attaquant sophistiqué.

La sortie de debug. Un job qui lance env pour dépanner, ou un script qui affiche une variable en cherchant une étape en échec, imprime le secret directement dans le log. La plupart des plateformes de CI scannent les valeurs de secret connues et les masquent dans la vue brute du log, mais ce scan n’attrape que la chaîne exacte qu’on lui a désignée : une copie encodée en base64, une sous-chaîne, ou une valeur reconstruite à l’exécution passe sans être masquée.

Les pull requests de forks avec des déclencheurs privilégiés. pull_request_target sur GitHub Actions s’exécute avec les permissions et les secrets du dépôt de base, mais checkout et peut exécuter du code venant du fork. Un workflow qui checkout aveuglément github.event.pull_request.head.sha sous ce déclencheur donne au code d’un contributeur externe un chemin vers vos secrets de production. C’est un piège documenté, pas un cas limite, précisément parce que le workflow qui détient les secrets n’est pas celui qui relit le code exécuté.

Les clés longue durée sans pression de rotation. Une clé d’accès statique déposée en variable de CI il y a trois ans, encore valide, encore scopée large parce que la resserrer « plus tard » n’a jamais eu lieu, est une dette permanente indépendante de toute fuite ponctuelle : quiconque a un jour eu un accès en lecture à cette variable, à n’importe quel moment de son histoire, l’a encore effectivement tant que personne ne l’a fait tourner à la main.

Ce que change la fédération OIDC

La correction structurelle consiste à arrêter de stocker un credential du tout. Avec la fédération OpenID Connect, la plateforme de CI elle-même émet un jeton JSON Web Token signé et de courte durée, identifiant l’exécution précise : quel dépôt, quelle branche, quel workflow. Le fournisseur cloud est configuré pour faire confiance à cet émetteur et, sur demande, échange le jeton contre des credentials cloud temporaires, valides pour la durée du job et scopés sur un rôle défini à l’avance.

# .github/workflows/deploy.yml
permissions:
  id-token: write   # autorise ce job à demander un jeton OIDC à GitHub
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/ci-deploy
          aws-region: eu-west-1
      # aucun AWS_ACCESS_KEY_ID, aucun AWS_SECRET_ACCESS_KEY nulle part dans ce dépôt

Côté AWS, la politique de confiance du rôle nomme exactement le dépôt GitHub et la branche autorisés à l’endosser, à partir des claims embarqués dans le jeton :

{
  "Effect": "Allow",
  "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringEquals": {
      "token.actions.githubusercontent.com:sub": "repo:acme/app:ref:refs/heads/main"
    }
  }
}

Le credential rendu est scopé, borné dans le temps (typiquement moins d’une heure), et jamais stocké nulle part ; un job qui se termine perd simplement l’accès. Il ne reste rien à faire fuiter dans un log, puisqu’il n’y a pas de secret longue durée assis dans une variable de CI au départ. GitLab CI propose l’équivalent avec id_tokens, Azure et GCP ont leurs propres dispositifs de fédération d’identité de workload, et la forme est la même chez les trois : l’exécution de CI s’authentifie en prouvant qui elle est, pas en présentant quelque chose qu’on lui a remis.

Là où ça se range, et là où ça ne remplace rien

Ce n’est pas un remplacement pour Vault ou Sealed Secrets : ces outils résolvent le stockage et la rotation de credentials qu’une application lit une fois qu’elle tourne dans le cluster. La fédération OIDC résout un problème plus étroit et plus en amont : comment le pipeline lui-même obtient un accès cloud pour builder et déployer, avant même qu’un secret applicatif entre en scène. Un dispositif mature combine généralement les deux : OIDC pour la poignée de main CI-vers-cloud, et un vrai gestionnaire de secrets pour ce dont l’application a besoin ensuite.

Le cas restant pour un credential statique est un fournisseur ou une cible auto-hébergée sans support de fédération. Là, la bonne parade est un scope IAM étroit, une rotation forcée sur calendrier, et le même traitement pour pull_request_target quel que soit le contexte : ne jamais laisser un workflow détenteur de secrets exécuter du code de fork non vérifié sous le même déclencheur.

Ce qu’il faut vérifier en premier

Cherchez dans votre configuration de CI les variables AWS_SECRET_ACCESS_KEY, AZURE_CLIENT_SECRET, ou équivalent, posées dans les paramètres du projet. Chacune est une dette permanente dont personne n’a fixé la date d’expiration. Migrer en premier le pipeline le plus privilégié (typiquement celui qui déploie en production) vers OIDC retire le credential unique dont la fuite ferait le plus mal, et c’est l’une des étapes concrètes de l’industrialisation d’une chaîne CI/CD : l’objectif n’est pas plus d’automatisation, c’est un pipeline qui a moins de secrets permanents à perdre en premier lieu.