Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

Chapitre 818 minIntermédiaire

Secrets et variables dans un pipeline

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • distinguer un secret d'une variable de configuration ;
  • créer et utiliser des secrets sans les exposer dans les journaux ;
  • régler les permissions du GITHUB_TOKEN et comprendre OIDC ;
  • réagir dans le bon ordre à la fuite d'un secret.

4 exercices corrigés

Un secret de pipeline est une valeur sensible, comme un mot de passe, une clé SSH ou un jeton d'API, dont un job a besoin pour déployer ou publier : il est stocké chiffré par la forge, injecté au moment de l'exécution, masqué dans les journaux, et n'apparaît jamais dans le dépôt. Les valeurs non sensibles (une URL, un nom d'environnement) sont des variables de configuration. Ce chapitre explique comment créer et utiliser les deux sur GitHub et GitLab, comment fonctionne le jeton automatique du pipeline, comment se passer de secrets permanents grâce à OIDC, et quoi faire en cas de fuite.

Ce qui est un secret, et ce qui n'en est pas

ValeurTypePourquoi
Clé SSH privée de déploiementSecretDonne accès au serveur
Mot de passe de la base de productionSecretDonne accès aux données
Jeton d'un registre d'images, d'un service de paiement, d'une APISecretPermet d'agir au nom de l'organisation
URL de déclenchement d'un déploiement (deploy hook)SecretQuiconque la connaît peut lancer un déploiement
Adresse du serveur de recetteVariableUtile, mais pas dangereuse si elle est vue
Empreinte publique du serveur SSH (known_hosts)VariableInformation publique, qui protège contre l'usurpation du serveur
Version de Node.js, nom de l'imageValeur en clair dans le workflowFait partie de la configuration versionnée
Mot de passe d'une base de test jetable créée dans le jobValeur en clair dans le workflowLa base est vide et détruite à la fin du job

Les secrets dans GitHub Actions

Un secret se crée dans Settings, Secrets and variables, Actions, New repository secret, ou en ligne de commande :

bash
gh secret set DEPLOY_SSH_KEY < ~/.ssh/sorbier_deploy      # contenu d'un fichier
gh secret set RECETTE_DB_PASSWORD                         # la valeur est demandée sans écho
gh secret list                                            # liste les noms, jamais les valeurs

Il existe trois niveaux :

NiveauVisible parUsage
OrganisationLes dépôts choisis de l'organisationUn jeton partagé par plusieurs projets
DépôtTous les workflows du dépôtLe cas le plus courant
EnvironnementLes seuls jobs qui déclarent cet environnement (environment: production)Les secrets de production, protégés par une approbation (chapitre 10)

Si le même nom existe à plusieurs niveaux, le plus précis l'emporte : environnement, puis dépôt, puis organisation. Un nom ne contient que des lettres, des chiffres et _, ne commence ni par un chiffre ni par GITHUB_, et un secret pèse au maximum 48 Ko.

Dans le workflow, on lit un secret avec ${{ secrets.NOM }}, de préférence en le passant à une variable d'environnement de l'étape qui en a besoin, pas de tout le job :

yaml
      - name: Déclencher le déploiement
        env:
          DEPLOY_HOOK: ${{ secrets.RECETTE_DEPLOY_HOOK }}
        run: curl --fail --silent --show-error -X POST "$DEPLOY_HOOK"

Les variables de configuration : vars

Les valeurs non sensibles se rangent dans l'onglet Variables du même écran, aux mêmes trois niveaux, et se lisent avec ${{ vars.NOM }} :

yaml
      - name: Tester la recette
        run: curl --fail --silent "${{ vars.URL_RECETTE }}/health"

Une variable n'est pas masquée dans les journaux. Son intérêt : changer une adresse ou un réglage sans modifier le workflow, et donner des valeurs différentes à chaque environnement.

Ce que GitHub fait pour protéger les secrets, et ses limites

  • Chiffrement. La valeur est chiffrée dans ton navigateur avant l'envoi ; personne ne peut la relire dans l'interface après l'enregistrement, pas même un administrateur. On ne peut que la remplacer.
  • Masquage. Si la valeur exacte apparaît dans un journal, elle est remplacée par ***.
  • Forks. Les workflows déclenchés par la pull request d'un fork ne reçoivent pas les secrets (sauf GITHUB_TOKEN, en lecture seule).

Le masquage a des limites : il ne reconnaît que la valeur exacte. Une valeur transformée (encodée en base64, coupée, mise dans un JSON) s'affiche en clair. Si un job calcule une valeur sensible, il doit la déclarer comme telle avant de l'afficher ou de la transmettre :

yaml
      - name: Obtenir un jeton temporaire
        run: |
          JETON=$(./scripts/obtenir-jeton.sh)
          echo "::add-mask::$JETON"
          echo "JETON=$JETON" >> "$GITHUB_ENV"

Utiliser une clé SSH de déploiement

Le cas le plus fréquent en BTS : le job se connecte en SSH au serveur de recette pour y lancer le déploiement. On crée une paire de clés dédiée au pipeline, sans phrase de passe, dont la clé publique est autorisée sur le serveur pour un compte de déploiement aux droits limités :

bash
ssh-keygen -t ed25519 -C "deploiement sorbier-frais" -f sorbier_deploy -N ""
# sorbier_deploy      : clé privée, à enregistrer comme secret DEPLOY_SSH_KEY
# sorbier_deploy.pub  : clé publique, à ajouter dans ~/.ssh/authorized_keys du compte deploy sur le serveur
ssh-keyscan -t ed25519 recette.sorbier.example
# la sortie (empreinte du serveur) est à enregistrer comme variable DEPLOY_KNOWN_HOSTS

Dans le workflow :

yaml
      - name: Préparer la connexion SSH
        env:
          SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          KNOWN_HOSTS: ${{ vars.DEPLOY_KNOWN_HOSTS }}
        run: |
          install -m 700 -d ~/.ssh
          printf '%s\n' "$SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts

      - name: Déployer
        run: ssh deploy@recette.sorbier.example "/opt/sorbier-frais/deployer.sh sha-${GITHUB_SHA::7}"

L'empreinte du serveur, enregistrée dans known_hosts, garantit que le job parle bien au vrai serveur. L'option StrictHostKeyChecking=no, souvent recopiée des forums, supprime cette vérification : un serveur usurpé recevrait alors la connexion.

Le jeton GITHUB_TOKEN

À chaque job, GitHub crée un jeton temporaire, disponible sous ${{ secrets.GITHUB_TOKEN }} (ou ${{ github.token }}). Il agit sur le dépôt en cours uniquement et expire à la fin du job. Ses droits se règlent avec permissions :

PermissionElle permet deExemple d'usage
contents: readLire le codeToute CI
contents: writePousser des commits et des tags, créer une releasePublier une version (chapitre 13)
packages: writePublier dans le registre de GitHub (ghcr.io)Pousser une image Docker (chapitre 9)
pull-requests: writeCommenter une pull requestPoster un résumé des tests
security-events: writePublier des alertes d'analyse de codeCodeQL
id-token: writeDemander un jeton OIDCS'authentifier auprès d'un cloud sans secret (voir plus bas)

Un détail utile : les événements provoqués par GITHUB_TOKEN (un push fait par le workflow, par exemple) ne déclenchent pas de nouveaux workflows, sauf workflow_dispatch et repository_dispatch. C'est une protection contre les boucles infinies. Si un workflow doit vraiment en déclencher un autre, on utilise un jeton d'application GitHub ou un jeton personnel à droits restreints (fine-grained personal access token), avec une date d'expiration.

Se passer de secrets permanents : OIDC

Un secret stocké dans GitHub est permanent : s'il fuit, il reste valable jusqu'à ce que quelqu'un s'en aperçoive et le révoque. Pour les déploiements dans un cloud (AWS, Azure, Google Cloud) ou la publication sur npm, la bonne pratique actuelle est OIDC (OpenID Connect) :

  1. Le job demande à GitHub un jeton d'identité signé, valable quelques minutes, qui dit : « je suis le workflow deploy.yml du dépôt sorbier/sorbier-frais, sur la branche main, pour l'environnement production ».
  2. Le fournisseur cloud, configuré à l'avance pour faire confiance à GitHub sous ces conditions précises, vérifie la signature et les conditions.
  3. Il délivre en échange des accès temporaires, limités au rôle prévu.

Aucun secret n'est stocké dans GitHub. Dans le workflow, il suffit d'autoriser la demande de jeton, puis d'utiliser l'action officielle du fournisseur (aws-actions/configure-aws-credentials, azure/login, google-github-actions/auth) :

yaml
permissions:
  contents: read
  id-token: write     # autorise le job à demander un jeton OIDC à GitHub

GitLab propose le même mécanisme avec le mot-clé id_tokens.

Les variables dans GitLab CI

Dans GitLab, secrets et variables se créent au même endroit : Settings, CI/CD, Variables. Chaque variable a des options qui en font, ou non, un secret :

OptionEffet
Protect variableLa variable n'est transmise qu'aux pipelines des branches et tags protégés : une branche de travail ne peut pas la lire
MaskedLa valeur est remplacée par [MASKED] dans les journaux ; elle doit tenir sur une ligne, sans espace, et faire au moins 8 caractères
Masked and hiddenEn plus, la valeur ne peut plus être affichée dans l'interface après l'enregistrement
Type FileGitLab écrit la valeur dans un fichier temporaire, et la variable contient le chemin du fichier : idéal pour une clé SSH ou un fichier de configuration
Environment scopeLa variable n'est transmise qu'aux jobs d'un environnement (production)

Dans .gitlab-ci.yml, une variable se lit comme une variable d'environnement : $RECETTE_DB_PASSWORD. Avec le type File :

yaml
deploiement-recette:
  stage: deploy
  image: alpine:3.24
  before_script:
    - apk add --no-cache openssh-client
    - install -m 700 -d ~/.ssh
    - cp "$DEPLOY_SSH_KEY" ~/.ssh/id_ed25519      # DEPLOY_SSH_KEY contient le chemin du fichier
    - chmod 600 ~/.ssh/id_ed25519
    - echo "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
  script:
    - ssh deploy@recette.sorbier.example "/opt/sorbier-frais/deployer.sh $CI_COMMIT_SHORT_SHA"
  rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Les gestionnaires de secrets

Dans les grandes organisations, les secrets ne sont pas copiés dans chaque outil : ils vivent dans un coffre centralisé, qui trace chaque accès et peut délivrer des secrets temporaires.

OutilType
HashiCorp Vault, OpenBaoCoffre auto-hébergé
AWS Secrets Manager, Azure Key Vault, Google Secret ManagerCoffre du fournisseur cloud
SOPSChiffrement de fichiers de configuration versionnés : seules les valeurs sont chiffrées, la structure reste lisible

Le pipeline s'authentifie auprès du coffre, de préférence par OIDC, et récupère les secrets au moment du déploiement.

Les secrets côté application

Le pipeline n'est pas le seul à manipuler des secrets : l'application en production a besoin du mot de passe de sa base. Les règles :

  • l'application lit sa configuration dans des variables d'environnement (process.env.DB_PASSWORD), jamais en dur dans le code ;
  • le dépôt contient un .env.example avec les noms des variables et des valeurs factices ; le vrai .env est dans le .gitignore ;
  • sur le serveur, le fichier de secrets appartient au compte de service et n'est lisible que par lui (chmod 600), ou les secrets sont fournis par l'orchestrateur (secrets Docker Compose, Kubernetes) ;
  • un secret n'est jamais copié dans une image Docker : toute personne qui récupère l'image peut lire ses couches (chapitre 9).

Rotation et réaction à une fuite

Changer régulièrement les secrets (la rotation) limite la durée d'utilité d'un secret volé : clés de déploiement chaque année, jetons à durée limitée, départ d'un membre de l'équipe.

En cas de fuite (secret commité, affiché dans un journal, poste volé), l'ordre est :

  1. révoquer immédiatement le secret chez son émetteur (supprimer la clé SSH du serveur, régénérer le jeton) ;
  2. créer un nouveau secret et le mettre à jour dans la forge ;
  3. chercher dans les journaux du serveur et de la forge (audit log) si le secret a été utilisé ;
  4. nettoyer l'historique Git si le secret y figure, en sachant que des copies peuvent déjà exister ;
  5. documenter l'incident et la cause, pour qu'elle ne se reproduise pas (réagir à une fuite de secret).

Exercices

Exercice 1 sur 4 Secret, variable ou valeur en clair ? Réussi À revoir

Range chaque valeur : secret, variable de configuration, ou valeur écrite en clair dans le workflow.

  1. https://recette.sorbier.example
  2. Le jeton d'accès au service d'envoi de courriels
  3. node-version: 24
  4. Le mot de passe de la base MySQL de recette
  5. L'empreinte publique du serveur SSH de production
  6. Le mot de passe root d'une base MySQL de test créée par services dans le job
Voir la réponseMasquer la réponse de l'exercice 1
  1. Variable : utile à configurer, pas dangereuse.
  2. Secret : il permet d'envoyer des courriels au nom de l'entreprise.
  3. En clair dans le workflow : c'est de la configuration versionnée.
  4. Secret, idéalement au niveau de l'environnement recette.
  5. Variable : c'est une information publique, qui sert à vérifier l'identité du serveur.
  6. En clair : la base est vide, jetable, inaccessible hors du job ; en faire un secret n'apporte rien.

Tu avais trouvé ?

Exercice 2 sur 4 Quatre erreurs Réussi À revoir

Trouve les quatre problèmes de sécurité de ce workflow de déploiement.

yaml
name: Déploiement
on:
  push:
    branches: [main]
permissions: write-all
jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      DB_PASSWORD: Sorbier2026!
      SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
    steps:
      - uses: actions/checkout@v7
      - run: echo "Clé utilisée = $SSH_KEY"
      - run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh -o StrictHostKeyChecking=no deploy@prod.sorbier.example "./deployer.sh"
Voir la réponseMasquer la réponse de l'exercice 2
  1. DB_PASSWORD: Sorbier2026! : un mot de passe en clair dans un fichier versionné. Il faut le stocker comme secret (${{ secrets.DB_PASSWORD }}), et changer ce mot de passe, désormais dans l'historique.
  2. permissions: write-all : le jeton a tous les droits en écriture alors que le job n'a besoin que de lire le code. Écrire permissions: contents: read.
  3. echo "Clé utilisée = $SSH_KEY" affiche le secret. GitHub le masquera probablement, mais c'est inutile et risqué (une clé sur plusieurs lignes, une transformation, un journal téléchargé). À supprimer.
  4. StrictHostKeyChecking=no supprime la vérification de l'identité du serveur : un serveur usurpé recevrait la connexion. Enregistrer l'empreinte du serveur dans ~/.ssh/known_hosts à partir d'une variable.

Améliorations bonus : passer SSH_KEY à la seule étape qui l'utilise plutôt qu'à tout le job, et rattacher le job à un environnement production protégé, qui détient le secret.

Tu avais trouvé ?

Exercice 3 sur 4 Un secret vide Réussi À revoir

La pull request ouverte par une contributrice extérieure, depuis son fork, échoue avec ce journal :

text
Run test -n "$API_GEO_KEY" || { echo "Secret absent"; exit 1; }
Secret absent
Error: Process completed with exit code 1.

Le même workflow réussit sur les pull requests de Lina et Hugo. Explique et propose une solution.

Voir la réponseMasquer la réponse de l'exercice 3

Les workflows déclenchés par une pull request venant d'un fork ne reçoivent pas les secrets du dépôt : API_GEO_KEY est vide. C'est une protection voulue : sinon, n'importe qui pourrait modifier le workflow dans sa pull request et lire les secrets. Lina et Hugo travaillent dans des branches du dépôt lui-même, d'où la différence.

Solution : les tests qui tournent sur les pull requests ne doivent pas dépendre d'un vrai service externe. On simule l'API de géolocalisation (mock) dans les tests ; les tests qui ont vraiment besoin de la clé tournent seulement après la fusion, sur main. Passer à pull_request_target pour obtenir le secret serait une faille (chapitre 7).

Tu avais trouvé ?

Exercice 4 sur 4 Régler une variable GitLab Réussi À revoir

Hugo enregistre dans GitLab la clé SSH privée qui permet de déployer en production. Quelles options choisit-il, et pourquoi ?

Voir la réponseMasquer la réponse de l'exercice 4
  • Type File : une clé SSH tient sur plusieurs lignes ; GitLab l'écrit dans un fichier dont le chemin est donné par la variable.
  • Protect variable : seuls les pipelines des branches et tags protégés (main, les tags de version) la reçoivent ; une branche de travail ne peut pas la lire.
  • Environment scope : production, pour qu'elle ne soit transmise qu'au job de déploiement en production.
  • Le masquage n'est pas possible pour une valeur sur plusieurs lignes ; c'est une raison de plus pour protéger la variable et contrôler qui peut modifier .gitlab-ci.yml sur main.

Tu avais trouvé ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : Construire et publier une image Docker dans la CIRevoir le sommaire du cours