Adieu mots de passe : authentification OIDC sans secrets pour vos pipelines GitLab CI

La gestion des mots de passe, et plus particulièrement le stockage de ces mots de passe, a toujours été quelque chose de compliqué. En effet, se pose la question de savoir comment les stocker de manière sécurisée.
Gitlab nous offre la possibilité de stocker les variables de projet, par exemple. Cependant, cette solution n’est pas entièrement satisfaisante, car elle n’offre pas la possibilité de garantir et d’auditer les accès et les mises à jour de ces secrets (la fameuse triade CIA : Confidentiality, Integrity & Availability) On peut aussi les stocker dans un outil dédié, de type Hashicorp Vault. Mais dans ce cas, comment s’authentifier sur ce service ? Avec un couple login/mot de passe (ou un token) ? Comment stocker ce dernier dans ce cas ? C’est un cercle sans fin.
Et si on avait la possibilité de ne plus du tout utiliser de mots de passe ? Plus de mots de passe, plus de problèmes ! C’est là qu’entre en scène OIDC, ou OpenID Connect.
OpenID Connect
OpenID Connect est un protocole d’authentification basé sur OAuth 2.0, conçu pour vérifier l’identité des utilisateurs et fournir des informations sur leur profil.
OAuth 2.0 est un protocole d’autorisation, permettant à une application d’accéder à des ressources au nom d’un utilisateur.
OIDC ajoute une couche d’authentification par dessus OAuth 2.0, pour confirmer l’identité de l’utilisateur.
Le protocole utilise des jetons JSON Web Token (JWT) pour transmettre des informations d’identité.
- Principe de fonctionnement
L’OIDC permet à un service Tiers de vérifier l’identité d’un utilisateur ou d’une machine en s’appuyant sur un jeton numérique signé cryptographiquement par un Fournisseur de confiance, éliminant ainsi le besoin de partager ou de stocker des mots de passe.
Voici un diagramme de fonctionnement, en prenant comment exemple Gitlab, comme Fournisseur de confiance, et Vault, comme service Tiers :
sequenceDiagram
autonumber
box rgb(240, 240, 255) Environnement CI/CD
participant Job as GitLab CI Job
participant GitLab as GitLab (Provider OIDC)
end
box rgb(255, 245, 245) Infrastructure Secrets
participant Vault as HashiCorp Vault
end
note over Job, Vault: Prérequis: Vault est configuré pour faire confiance à l'émetteur GitLab (Issuer URL)
%% Phase 1 : Obtention de l'identité
rect rgb(230, 245, 255)
note right of Job: Phase 1 : Obtention du JWT OIDC
Job->>GitLab: Demande du jeton ID interne (Variable CI)
GitLab-->>Job: Génère et fournit le JWT signé
note right of Job: Le Job possède maintenant<br/>sa preuve d'identité cryptographique
end
%% Phase 2 : Authentification auprès de Vault
rect rgb(255, 250, 230)
note right of Job: Phase 2 : Login Vault
Job->>Vault: Requête API Auth : envoi du JWT OIDC
note right of Vault: Vault doit valider ce jeton...
Vault->>GitLab: Requête GET pour les clés publiques (JWKS URI)
GitLab-->>Vault: Retourne les clés publiques de signature
note right of Vault: Vault vérifie la signature du JWT<br/>et valide les "claims" (iss, sub, aud)
Vault-->>Job: Succès : Retourne un "Vault Token" temporaire
end
%% Phase 3 : Utilisation du secret
rect rgb(235, 255, 235)
note right of Job: Phase 3 : Accès aux secrets
Job->>Vault: Demande de secret (via le Vault Token)
Vault-->>Job: Fournit la valeur du secret déchiffré
end
- Token JWT
Le JSON Web Token (JWT, à prononcer JOT) est un format compact et sécurisé utilisé pour transmettre des informations entre parties sous la forme d’un objet JSON. Vous pouvez trouver plus d’informations sur le site jwt.io
Un JWT est toujours composé de trois parties distinctes, séparées par des points (.), selon la structure suivante :
• Header
• Payload
• Signature
De fait, un token JWT ressemble à ceci header.payload.signature.
- Header
Le Header est la première partie du JWT. Il s’agit d’un objet JSON encodé en Base64url qui sert principalement à spécifier la nature du jeton, et l’algorithme de cryptographie utilisé pour le signer.
| Champ (Claim) | Description | Exemple |
| typ (Type) | Indique le type de média. Pour un JWT, la valeur est presque toujours JWT. | « typ »: « JWT » |
| alg (Algorithm) | Spécifie l’algorithme de signature utilisé pour sécuriser le jeton, souvent HS256 (HMAC avec SHA-256) ou RS256 (RSA avec SHA-256). | « alg »: « RS256 » |
Un header ressemble donc à ceci :
{
"alg": "RS256",
"typ": "JWT"
}
- Payload
Le Payload contient des claims (revendications, ou assertions) sur l’entité (l’utilisateur, ou l’application) et les métadonnées. C’est un objet JSON encodé en Base64.
Attention ! :
Bien que le token soit signé et qu’on puisse donc vérifier qui l’a émis, il n’est pas chiffré pour autant.
Il faut donc absolument éviter de mettre des données sensibles dans le contenu du payload, puisqu’un simple « base64_decode » permet de lire le contenu des claims !
Les claims peuvent être de 3 types :
- Registered Claims
- Ce sont des claims standardisés mais optionnels, recommandés pour l’interopérabilité.
- iss (Issuer) : L’entité qui a émis le jeton (ex: https://gitlab.com).
- sub (Subject) : L’entité faisant l’objet du jeton (l’identifiant unique de l’utilisateur). Crucial pour l’OIDC dans GitLab CI.
- aud (Audience) : Les destinataires auxquels le JWT est destiné (ex: l’URL du fournisseur de cloud ou du Vault).
- exp (Expiration Time) : L’heure d’expiration du jeton (pour des raisons de sécurité, les JWT ont une durée de vie très courte).
- iat (Issued At) : L’heure à laquelle le JWT a été émis.
- Note : Liste des claims standards définis par la spécification OIDC
- Public Claims : Ce sont des claims qui sont définis de manière libre par l’utilisateur qui demande la génération du token. Pour éviter les collisions, ils doivent être namespacés.
- Private Claims : Ce sont des claims spécifiques créés pour être utilisés entre les parties (l’émetteur et le consommateur) qui ont convenu de leur signification. Dans GitLab-CI, c’est là que l’on trouve les informations spécifiques au pipeline (project_path, ref, user_login, etc…).
Exemple de payload :
Voici un exemple de Payload, inspiré de GitLab OIDC :
{
"iss": "https://gitlab.com",
"sub": "project_path:mon-org/mon-projet:ref_type:branch:ref:main",
"aud": "https://sts.amazonaws.com",
"exp": 1672531200,
"iat": 1672530600,
"project_id": "42",
"user_login": "gitlab-ci-job"
}
- Signature
La signature est utilisée par le destinataire (« Audience ») pour vérifier que le jeton est authentique et qu’il n’a pas été altéré en cours de route.
Le processus de création de la signature est le suivant :
- Prendre le Header encodé en Base64url.
- Prendre le Payload encodé en Base64url.
- Concaténer les deux avec un point (.) : Base64url(Header) + « . » + Base64url(Payload).
- Appliquer l’algorithme de hachage spécifié dans le Header (alg) à cette chaîne, en utilisant une clé secrète (clé privée de l’IdP).
Si le destinataire peut régénérer la même signature en utilisant la clé publique correspondante de l’IdP, alors le jeton est valide. C’est ce mécanisme qui garantit que le jeton a bien été émis par l’issuer (GitLab dans notre exemple) et qu’il n’a pas été falsifié.
Vous pouvez vérifier ce comportement en copiant votre token JWT sur le site de jwt.io. Vous pourrez alors apercevoir le contenu du payload (les claims) directement.
Le site vous informera cependant que l’authenticité du jeton n’a pas pu être vérifiée, puisque vous n’avez pas fourni la clé publique correspondant à la clé privée qui a été utilisée pour signer le token.
Si vous copiez la clé publique dans le champ JWT Signature Verification (Optional), le site pourra dans ce cas vérifier si la signature est valide.
Utilisation dans un pipeline Gitlab-CI
Pour obtenir un token JWT, il faut utiliser le mot-clé id_token :
job_with_id_tokens:
id_tokens:
JWT_TOKEN_FOR_VAULT:
aud: https://url-of-vault-service
JWT_TOKEN_FOR_AZURE:
aud: api://AzureADTokenExchange
script:
- get-secrets.sh $JWT_TOKEN_FOR_VAULT
- do-something-on-azure.sh $JWT_TOKEN_FOR_AZURE
Il peut ensuite être utilisé pour s’authentifier sur le service cible.
- Mise en place avec Hashicorp Vault
Pour pouvoir faire communiquer communiquer Gitlab et Vault avec cette méthode, il est nécessaire d’activer l’authentification JWT dans Vault.
vault auth enable jwt
vault write auth/jwt/config \
oidc_discovery_url="https://gitlab.service" \
bound_issuer="gitlab.service"
Ensuite, il faut créer une "policy", "demo-dev" dans notre exemple, qui donne le droit ( = capabilities) de lecture sur le(s) secret(s) stockés dans le chemin demo/dev/* :
vault policy write demo-dev - <<EOF
# Policy name: demo-dev
#
# Read-only permission on 'secret/demo/dev/*' path
path "secret/data/demo/dev/*" {
capabilities = [ "read" ]
}
EOF
Il ne nous reste qu’à créer un rôle de type JWT. Nous allons aussi renforcer la sécurité en vérifiant certains claism du token JWT présenté. Dans l’exemple, l’authentification ne sera valide que si :
- l’ID du projet Gitlab est 1234
- la branche sur laquelle le job est exécuté est develop
vault write auth/jwt/role/demo-dev - <<EOF
{
"role_type": "jwt",
"policies": ["demo-dev"],
"token_explicit_max_ttl": 60,
"user_claim": "user_email",
"bound_claims": {
"project_id": "1234",
"ref": "develop",
"ref_type": "branch"
}
}
EOF
Nous allons maintenant pouvoir configurer le job Gitlab-CI pour aller chercher ce secret. La propriété id_tokens nous permet de demander à Gitlab de nous fournir un token JWT. Noter que nous définissons l’audience ( = aud) avec l’URL du service Vault.
get-secrets:
stage: init
environment:
name: dev
variables:
VAULT_ROLE: demo-{CI_ENVIRONMENT_SLUG}
id_tokens:
VAULT_OIDC_TOKEN:
aud: https://vault.service
script:
- export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=${VAULT_ROLE} jwt=${VAULT_OIDC_TOKEN})"
- export DB_PASS="$(vault kv get -mount=secret -field=pass demo/${CI_ENVIRONMENT_SLUG}/db)"
rules:
- if: '$CI_DEFAULT_BRANCH == "develop"'
Nous avons réussi à nous authentifier à Vault, et à récupérer la valeur de la clé db contenue dans le chemin demo/dev.
- Mise en place avec Azure
La mise en place de l’OIDC entre GitLab et Azure repose sur une fonctionnalité appelée « Workload Identity Federation ».
Cela permet à Azure de faire confiance aux jetons (JWT) émis par GitLab pour autoriser des accès sans stocker de secrets d’un Service Principal.
La première étape consiste à préparer l’entité qui recevra les droits dans votre abonnement Azure :
- Créer une App Registration (ou utiliser un Service Principal existant).
- Aller dans Certificates & Secrets > Federated credentials.
- Ajouter un nouveau credential avec les paramètres suivants :
- Federated credential scenario : Other issuer.
- Issuer : https://gitlab.service (l’URL de votre instance Gitlab).
- Subject identifier : C’est ici que vous définissez la sécurité. Vous pouvez restreindre l’accès à un projet ou une branche spécifique.
- Exemple pour un projet : project_path:mon-groupe/mon-projet:ref_type:branch:ref:main
- Audience : api://AzureADTokenExchange (doit correspondre à la valeur envoyée par GitLab).
Une fois l’identité créée, vous devez lui donner les permissions nécessaires sur vos ressources, comme par exemple :
- VMs (pour exécuter des process)
- KeyVault (pour stocker des secrets qui ont été générés par la chaine CI/CD)
- Storage Account (pour stocker des fichiers, comme par exemple des TFState dans le cas de l’utilisation de Terraform)
- etc…
- Mise en place avec Gitlab-CI
do-something-job:
stage: deploy
variables:
AZURE_CLIENT_ID: "00000000-0000-0000-0000-000000000000" # Application (client) ID
AZURE_TENANT_ID: "00000000-0000-0000-0000-000000000000" # Directory (tenant) ID
AZURE_KV_NAME: "my-kv"
id_tokens:
JWT_TOKEN_FOR_AZURE:
aud: api://AzureADTokenExchange
image: mcr.microsoft.com/azure-cli
script:
# Connexion à Azure sans mot de passe
- az login --service-principal \
--client-id $AZURE_CLIENT_ID \
--tenant $AZURE_TENANT_ID \
--federated-token $JWT_TOKEN_FOR_AZURE
# Vous pouvez maintenant interagir avec les ressources Azure sur lesquelles vous avez des permissions
- az keyvault secret set --vault-name $AZURE_KV_NAME \
--name "ExamplePassword" \
--value "hVFkk965BuUv"
Conclusion
En définitive, l’adoption du protocole OIDC pour interconnecter GitLab-CI avec des infrastructures critiques comme Azure ou HashiCorp Vault marque la fin de l’ère des secrets statiques.
Cette modernisation offre un double bénéfice immédiat :
- elle renforce drastiquement la sécurité en s’appuyant sur des jetons éphémères et audités
- En conditionnant l’accès aux ressources à l’identité cryptographique vérifiée du pipeline plutôt qu’à un mot de passe stocké, vous ancrez vos processus CI/CD dans une approche Zero Trust, où la confiance est établie dynamiquement et uniquement pour la durée de l’exécution.
Les trois grands Cloud Providers (AWS, Azure & GCP) supportent déjà cette authentification OIDC, ainsi que plusieurs outils couramment utilisés dans des chaînes CI/CD :
- Hashicorp Vault
- SonarQube
- JFrog Artifactory
- Snyk
- etc…
Vous n’avez plus d’excuses pour mettre en place ce mécanisme dans vos pipelines de CI/CD, et ainsi vous libérer de la charge de la gestion des secrets !
