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.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- 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
Prérequis
Section intitulée « Prérequis »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.
Pourquoi les secrets CI/CD sont critiques
Section intitulée « Pourquoi les secrets CI/CD sont critiques »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.
Le problème des tokens longue durée
Section intitulée « Le problème des tokens longue durée »La pratique encore courante :
# ❌ Token AWS stocké dans les secrets du dépôtenv: 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é".
Modèle de menace d'un pipeline CI/CD
Section intitulée « Modèle de menace d'un pipeline CI/CD »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'attaque | Scénario | Impact |
|---|---|---|
| Workflow / job modifié | Un contributeur modifie un workflow pour exécuter env | curl attacker.com dans un step | Exfiltration de tous les secrets injectés dans le job |
| Dépendance malveillante | Un package npm/pip compromis lit process.env ou os.environ et exfiltre les variables | Vol silencieux des tokens, difficile à détecter |
| Action GitHub / image Docker compromise | Une GitHub Action tierce ou une image Docker utilisée dans le pipeline contient du code malveillant | Accès aux secrets du job, à GITHUB_TOKEN, au filesystem |
| Runner compromis ou mal isolé | Un runner self-hosted partagé entre projets, sans isolation | Un job malveillant accède aux secrets d'un autre projet via /tmp, le disque, ou les process |
Fork + pull_request_target | Un fork soumet une PR qui modifie le workflow et s'exécute avec les secrets du repo cible | Exfiltration 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 |
Supports de fuite souvent oubliés
Section intitulée « Supports de fuite souvent oubliés »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
/tmpou 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'imageARG NPM_TOKENRUN echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > /tmp/.npmrc && npm ci && rm /tmp/.npmrcdocker 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 RUNRUN --mount=type=secret,id=npm_token \ echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" > /tmp/.npmrc \ && npm ci && rm /tmp/.npmrcdocker build --secret id=npm_token,env=NPM_TOKEN -t mon-app:1.4.0 .Configurer les secrets : bonnes pratiques
Section intitulée « Configurer les secrets : bonnes pratiques »GitHub Actions
Section intitulée « GitHub Actions »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.
| Niveau | Accès | Usage |
|---|---|---|
| Repository secrets | Ce dépôt uniquement | Secrets spécifiques à un projet |
| Environment secrets | Un environnement (prod, staging) | Avec protection et approbation |
| Organization secrets | Tous les dépôts de l'org | Secrets partagés (registry) |
Configuration d'un environnement protégé :
- Settings → Environments → New environment
- Nom :
production - 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.shGitLab CI
Section intitulée « GitLab CI »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.
| Type | Comportement |
|---|---|
| Variable | Visible dans les logs (éviter pour secrets) |
| Masked | Chaîne exacte remplacée par [MASKED] (contournable par transformation) |
| Protected | Disponible 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: productiondeploy: stage: deploy script: - echo "Deploying with $DB_PASSWORD" # Affiché comme [MASKED] rules: - if: $CI_COMMIT_BRANCH == "main" environment: name: productionOIDC : éliminer les tokens longue durée
Section intitulée « OIDC : éliminer les tokens longue durée »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é :
-
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.
-
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.
-
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é.
Claims OIDC : la base de la confiance
Section intitulée « Claims OIDC : la base de la confiance »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 :
| Claim | Contenu | Usage dans la trust policy |
|---|---|---|
sub | repo:org/repo:ref:refs/heads/main | Limiter par repo + branche |
aud | sts.amazonaws.com | Vérifier l'audience cible |
repository | org/repo | Filtrer par dépôt |
environment | production | Limiter à un environnement |
ref | refs/heads/main | Limiter par branche |
workflow | deploy.yml | Limiter à un workflow précis |
Claims GitLab CI :
| Claim | Contenu | Usage dans la trust policy |
|---|---|---|
sub | project_path:group/project:ref_type:branch:ref:main | Limiter par projet + branche |
aud | Audience configurée dans id_tokens | Vérifier la cible |
namespace_path | group | Filtrer par groupe |
project_path | group/project | Filtrer par projet |
ref | main | Limiter par branche |
environment | production | Limiter à un environnement |
GitHub Actions + AWS (OIDC)
Section intitulée « GitHub Actions + AWS (OIDC) »Configuration AWS :
# 1. Créer l'identity provider OIDCresource "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 STRICTEresource "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: Deployon: push: branches: [main]
# Permissions minimales au niveau du workflowpermissions: {}
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 :
- Un Workload Identity Pool qui regroupe les identités externes
- Un Provider dans ce pool qui configure les claims GitHub/GitLab
- Un mapping d'attributs qui lie les claims OIDC aux attributs GCP
- Un binding IAM entre le principal et un service account (si impersonation)
name: Deploy GCPon: 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/appGitLab CI + AWS (OIDC)
Section intitulée « GitLab CI + AWS (OIDC) »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: productionGitLab CI : points d'attention
Section intitulée « GitLab CI : points d'attention »Quelques spécificités GitLab à connaître :
id_tokensvsCI_JOB_JWT:id_tokensest la méthode actuelle. Les anciensCI_JOB_JWTsont 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_pathetenvironmentdans 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.
Intégration avec HashiCorp Vault
Section intitulée « Intégration avec HashiCorp Vault »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.
GitHub Actions + Vault (OIDC)
Section intitulée « GitHub Actions + Vault (OIDC) »Configuration Vault :
# 1. Activer l'auth method JWTvault auth enable jwt
# 2. Configurer le provider GitHub OIDCvault 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 policyvault 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 Vaulton: 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 CI + Vault (OIDC)
Section intitulée « GitLab CI + Vault (OIDC) »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: productionLa 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éthode | Quand l'utiliser |
|---|---|---|
| 1 | OIDC / fédération d'identité | Cloud (AWS, GCP, Azure), Vault, tout service compatible JWT |
| 2 | Secret dynamique via Vault | Base de données, credentials à courte durée de vie |
| 3 | Secret statique protégé dans le CI | API tierce à clé fixe, service legacy non fédérable |
| 4 | Secret dans le code ou le dépôt | Jamais |
Quand OIDC ne suffit pas
Section intitulée « Quand OIDC ne suffit pas »OIDC couvre les clouds majeurs et Vault, mais certains cas imposent encore un secret stocké :
| Cas | Pourquoi OIDC ne marche pas | Solution |
|---|---|---|
| API SaaS à clé fixe | Le fournisseur n'implémente pas OIDC/JWT | Secret statique dans un environment protégé, rotation planifiée |
| Système legacy on-prem | Pas de trust federation prête | Passer par Vault comme intermédiaire (le CI obtient le secret depuis Vault via OIDC) |
| Outil sans support OIDC | Client CLI ou SDK non compatible | Secret court avec scoping strict (un secret par usage) |
| Infra sans accès réseau | Le pipeline ne peut pas atteindre le provider OIDC | Secret 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
Durcir vos workflows
Section intitulée « Durcir vos workflows »Permissions minimales (GitHub Actions)
Section intitulée « Permissions minimales (GitHub Actions) »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éfautpermissions: {}
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.
Pinning des actions par SHA
Section intitulée « Pinning des actions par SHA »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.1persist-credentials: false
Section intitulée « persist-credentials: false »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: falseRunners dédiés et isolation
Section intitulée « Runners dédiés et isolation »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èges courants et solutions
Section intitulée « Pièges courants et solutions »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"Auditer l'usage des secrets d'un workflow
Section intitulée « Auditer l'usage des secrets d'un workflow »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.
# ❌ Workflow qui reprend les erreurs courantes sur les secretsjobs: 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: inheritMesuré avec zizmor 1.30.0 : code de sortie 14 et un constat par erreur.
| Constat zizmor | Ligne visée | Pourquoi c'est une fuite |
|---|---|---|
unredacted-secrets (x2) | fromJSON(secrets.DB_CREDS).password | GitHub masque la valeur entière du secret, pas un champ extrait : le mot de passe peut s'afficher en clair |
overprovisioned-secrets | toJSON(secrets) | Le job reçoit tous les secrets du dépôt, y compris ceux dont il n'a pas besoin |
secrets-inherit | secrets: inherit | Le workflow appelé hérite de tous les secrets, sans liste explicite |
artipacked | actions/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 PRon: pull_request_target
# ✅ Plus sûr : exécute le code du repo cible, pas de secrets exposés aux forkson: pull_requestpull_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.
Piège 3 : token trop permissif
Section intitulée « Piège 3 : token trop permissif »# ❌ Token admin avec tous les droitsenv: GITHUB_TOKEN: ${{ secrets.ADMIN_PAT }}Solution : utiliser le GITHUB_TOKEN automatique avec permissions minimales.
permissions: contents: read packages: write # Seulement ce qui est nécessairePiège 4 : secret dans le cache ou les artefacts
Section intitulée « Piège 4 : secret dans le cache ou les artefacts »# ❌ 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.jsonMême logique pour le cache : ne jamais cacher ~/.docker, ~/.aws, ou un répertoire contenant des .npmrc avec tokens.
Migration vers OIDC
Section intitulée « Migration vers OIDC »-
Inventaire des secrets CI/CD
Listez tous les tokens longue durée stockés dans vos dépôts et environnements CI.
-
Prioriser par criticité
Commencez par les credentials cloud (AWS, GCP, Azure), puis les accès Vault.
-
Configurer l'identity provider
Créez le provider OIDC côté cloud ou Vault. Vérifiez les thumbprints dans la documentation officielle du moment.
-
Créer les rôles avec trust policy stricte
Limitez systématiquement par repo, branche, et environnement. Évitez les wildcards.
-
Migrer les workflows
Remplacez les secrets par l'authentification OIDC. Ajoutez
permissions: {}au niveau workflow, les permissions minimales au niveau job. -
Supprimer et révoquer les anciens secrets
Révoquez les access keys devenues inutiles. Ne les laissez pas en parallèle.
Dépannage
Section intitulée « Dépannage »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ôme | Cause probable | Solution |
|---|---|---|
Not authorized to perform sts:AssumeRoleWithWebIdentity | Trust policy trop restrictive ou claim sub incorrect | Comparer le sub du JWT avec la condition dans la trust policy. Activer CloudTrail pour voir le claim reçu |
Error: No subject claim found | permissions: id-token: write manquant | Ajouter la permission au niveau du job |
Audience in token does not match | Audience (aud) configurée différente de celle attendue | Vérifier client_id_list côté AWS et aud dans le workflow GitLab |
Unable to verify thumbprint | Thumbprint AWS obsolète ou incorrect | Recalculer 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 Pool | Vérifier le mapping attribute.repository / assertion.repository dans le provider |
Vault login failed | Rôle JWT mal configuré ou bound_claims incorrect | Vé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ères | Utiliser un secret suffisamment long |
À retenir
Section intitulée « À retenir »- 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-argreste lisible dans l'image : mesuré, deux occurrences dansdocker history;--mount=type=secretn'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.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »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
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
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Pipeline sécurisé : gates, secrets, signature
Continuer le cours : Sécuriser une pipeline CI/CD : les quatre surfaces de contrôle
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications