Aperçu
GitLab CI est la deuxième plateforme CI/CD la plus utilisée dans l’industrie, alimentant des millions de pipelines au sein d’organisations de toutes tailles. Son intégration étroite avec le contrôle de version la rend exceptionnellement pratique — mais cette même intégration crée une surface d’attaque étendue si les pipelines ne sont pas délibérément renforcés.
Dans ce lab pratique, vous parcourrez six exercices qui sécurisent progressivement un pipeline GitLab CI. Vous commencerez avec une configuration intentionnellement non sécurisée où chaque variable est visible depuis toutes les branches, les runners partagés gèrent tous les jobs, et il n’y a aucune porte d’approbation sur les environnements. À la fin, vous disposerez d’un pipeline qui applique un accès aux variables selon le principe du moindre privilège, des runners à portée limitée, des environnements protégés avec approbations de déploiement, un accès restreint au CI_JOB_TOKEN, des pipelines de merge request sécurisés, ainsi que des contrôles de renforcement supplémentaires incluant la détection de secrets.
Chaque commande, extrait YAML et chemin d’interface dans ce lab est basé sur GitLab 16.x / 17.x et fonctionne sur le plan gratuit de GitLab.com.
Prérequis
- Un compte GitLab — le plan gratuit sur gitlab.com est suffisant pour chaque exercice.
- Un projet de test contenant une application simple (même un simple fichier
index.htmlsuffit) et un fichier.gitlab-ci.ymlà la racine du dépôt. - Une familiarité de base avec la syntaxe GitLab CI : stages, jobs, scripts et rules.
- (Optionnel) Une machine Linux ou macOS si vous prévoyez d’enregistrer votre propre GitLab Runner dans l’Exercice 2.
Mise en Place de l’Environnement
Étape 1 — Créer un Nouveau Projet GitLab
- Naviguez vers GitLab > New Project > Create blank project.
- Nommez-le
secure-pipeline-lab, définissez la visibilité sur Private et initialisez avec un README. - Sous Settings > Repository > Protected branches, confirmez que
mainest listée comme branche protégée (c’est le paramètre par défaut).
Étape 2 — Ajouter une Application Simple
Créez index.html à la racine du dépôt :
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Secure Pipeline Lab</title></head>
<body><h1>Hello, GitLab CI!</h1></body>
</html>
Étape 3 — Créer le Pipeline Initial (Non Sécurisé)
Ajoutez le fichier .gitlab-ci.yml suivant. Il est délibérément non sécurisé — c’est le point de départ que nous renforcerons tout au long du lab :
# .gitlab-ci.yml — Point de départ NON SÉCURISÉ
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- echo "Building the application..."
- echo "DB_PASSWORD is $DB_PASSWORD" # Variable affichée dans les logs !
test-job:
stage: test
script:
- echo "Running tests..."
- echo "API_KEY is $API_KEY" # Variable affichée dans les logs !
deploy-job:
stage: deploy
script:
- echo "Deploying to production..."
- echo "DEPLOY_TOKEN is $DEPLOY_TOKEN" # Variable affichée dans les logs !
Ce pipeline présente plusieurs problèmes :
- Toutes les variables CI/CD sont accessibles depuis toutes les branches, y compris celles créées par des contributeurs externes.
- Les variables sont affichées directement dans les logs des jobs — toute personne ayant accès aux logs peut les lire.
- Les jobs s’exécutent sur des runners partagés sans garantie d’isolation.
- Il n’y a aucune porte d’approbation sur les environnements — le job de déploiement s’exécute automatiquement à chaque push.
Committez ce fichier sur main et vérifiez que le pipeline s’exécute. Maintenant, corrigeons chacun de ces problèmes.
Exercice 1 : Variables Protégées et Masquées
Les variables CI/CD de GitLab prennent en charge trois indicateurs de protection qui réduisent considérablement le rayon d’impact d’une branche ou d’un fork compromis.
Comprendre les Trois Indicateurs
| Indicateur | Effet |
|---|---|
| Protected | La variable est uniquement injectée dans les pipelines s’exécutant sur des branches ou tags protégés. Un pipeline déclenché depuis une branche de fonctionnalité ou un fork ne verra jamais la valeur. |
| Masked | GitLab masque la valeur de la variable dans les logs des jobs. Si un script affiche accidentellement la valeur, le log affiche [MASKED] à la place. |
| Hidden (GitLab 17+) | La valeur de la variable ne peut pas être révélée dans l’interface après sa création — même par les mainteneurs du projet. Utile pour les secrets gérés par une équipe plateforme que les développeurs ne devraient jamais voir en texte clair. |
Étape 1 — Créer les Variables
- Allez dans Settings > CI/CD > Variables > Expand > Add variable.
- Créez les variables suivantes :
| Clé | Valeur (exemple) | Protected | Masked | Hidden |
|---|---|---|---|---|
DEPLOY_TOKEN |
glpat-xxxxxxxxxxxxxxxxxxxx |
Oui | Oui | Non |
DB_PASSWORD |
S3cur3P@ssw0rd!2024 |
Oui | Oui | Oui |
API_KEY |
sk-test-abc123def456 |
Non | Oui | Non |
Étape 2 — Mettre à Jour le Pipeline
# .gitlab-ci.yml — Exercice 1
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- echo "Building the application..."
- echo "API_KEY value length = ${#API_KEY}" # Sûr : affiche la longueur, pas la valeur
test-job:
stage: test
script:
- echo "Running tests..."
# Tentative d'affichage d'une variable masquée :
- echo "DB_PASSWORD is $DB_PASSWORD"
# La sortie affichera : DB_PASSWORD is [MASKED]
deploy-job:
stage: deploy
script:
- echo "Deploying with DEPLOY_TOKEN..."
- echo "Token is $DEPLOY_TOKEN"
# Sur main (protégée) : Token is [MASKED]
# Sur une branche de fonctionnalité : Token is <vide — variable non injectée>
rules:
- if: $CI_COMMIT_BRANCH == "main"
Étape 3 — Vérifier le Comportement de Protection
- Push sur
main— le job de déploiement s’exécute etDEPLOY_TOKENest injecté (le log affiche[MASKED]). - Créez une branche
feature/test-vars, poussez un commit — le job de déploiement ne s’exécute pas (les rules le restreignent àmain). Même si vous modifiez les rules pour le laisser s’exécuter,DEPLOY_TOKENetDB_PASSWORDsont vides car la branche n’est pas protégée. API_KEY, qui est masquée mais non protégée, est disponible sur les deux branches — sa valeur est masquée dans les logs.
Leçon clé : Marquez toujours les identifiants de déploiement comme Protected et Masked. Utilisez Hidden pour les secrets que les développeurs ne devraient jamais récupérer depuis l’interface.
Exercice 2 : Sécurité et Portée des Runners
Les runners sont les moteurs de calcul qui exécutent vos jobs CI. Choisir le bon type de runner — et définir correctement sa portée — est l’une des décisions de sécurité les plus impactantes que vous puissiez prendre.
Types de Runners
| Type | Portée | Posture de Sécurité |
|---|---|---|
| Instance (partagé) | Disponible pour chaque projet sur l’instance GitLab | Multi-tenant. Les jobs d’autres projets peuvent s’exécuter sur la même machine. Risque de fuite de données via le système de fichiers partagé, le socket Docker ou les couches en cache. |
| Group | Disponible pour chaque projet d’un groupe spécifique | Meilleure isolation que les runners d’instance, mais toujours partagé entre les projets du groupe. |
| Project | Disponible pour un seul projet uniquement | Meilleure isolation. Vous contrôlez la machine, la configuration Docker et l’accès réseau. |
Étape 1 — Enregistrer un Runner Spécifique au Projet
Sur une machine que vous contrôlez (une VM, un serveur disponible ou même un hôte Docker local), installez GitLab Runner et enregistrez-le :
# Installer GitLab Runner (Linux amd64)
sudo curl -L --output /usr/local/bin/gitlab-runner \
https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64
sudo chmod +x /usr/local/bin/gitlab-runner
sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner
sudo gitlab-runner start
# Enregistrer le runner
# Trouvez votre token d'enregistrement : Settings > CI/CD > Runners > Expand > New project runner
sudo gitlab-runner register \
--non-interactive \
--url https://gitlab.com/ \
--token "$RUNNER_TOKEN" \
--executor docker \
--docker-image alpine:latest \
--description "secure-deploy-runner" \
--tag-list "secure-deploy" \
--access-level ref_protected
L’indicateur critique est --access-level ref_protected. Il indique à GitLab que le runner n’acceptera que les jobs provenant de branches ou tags protégés. Un pipeline déclenché par une branche de fonctionnalité ou une merge request de fork ne sera jamais planifié sur ce runner.
Étape 2 — Désactiver les Runners Partagés pour les Jobs Sensibles
Allez dans Settings > CI/CD > Runners et basculez Enable shared runners for this project sur désactivé — ou laissez-les activés pour les étapes non sensibles et utilisez des tags pour diriger les jobs sensibles vers votre runner de projet.
Étape 3 — Mettre à Jour le Pipeline avec une Sélection de Runner par Tags
# .gitlab-ci.yml — Exercice 2
stages:
- build
- test
- deploy
build-job:
stage: build
# S'exécute sur n'importe quel runner disponible (partagé convient pour les builds)
script:
- echo "Building the application..."
test-job:
stage: test
script:
- echo "Running tests..."
deploy-job:
stage: deploy
tags:
- secure-deploy # S'exécute uniquement sur le(s) runner(s) avec ce tag
script:
- echo "Deploying with DEPLOY_TOKEN..."
- |
curl --fail --silent --header "PRIVATE-TOKEN: $DEPLOY_TOKEN" \
https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/releases
rules:
- if: $CI_COMMIT_BRANCH == "main"
Parce que le runner secure-deploy est enregistré avec l’accès ref_protected, ce job de déploiement ne s’exécutera que sur le runner spécifique au projet et uniquement lorsque le pipeline provient d’une référence protégée.
Exercice 3 : Environnements Protégés et Approbations de Déploiement
Même avec des variables protégées et des runners à portée limitée, vous pouvez souhaiter une validation humaine avant que le code n’atteigne la production. Les environnements protégés de GitLab fournissent exactement cela.
Étape 1 — Créer les Environnements
- Naviguez vers Operate > Environments > New environment.
- Créez deux environnements :
stagingetproduction.
Étape 2 — Protéger l’Environnement de Production
- Allez dans Settings > CI/CD > Protected environments (disponible sur Premium/Ultimate, ou sur le plan gratuit en auto-hébergé).
- Sélectionnez
production. - Sous Allowed to deploy, restreignez aux
Maintainers(ou à un utilisateur spécifique). - Sous Required approvals, définissez à 1 (ou plus, selon votre politique).
- Ajoutez le(s) approbateur(s) désigné(s).
Étape 3 — Mettre à Jour le Pipeline avec les Définitions d’Environnements
# .gitlab-ci.yml — Exercice 3
stages:
- build
- test
- deploy
build-job:
stage: build
script:
- echo "Building the application..."
test-job:
stage: test
script:
- echo "Running tests..."
deploy-staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
script:
- echo "Deploying to staging..."
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-production:
stage: deploy
tags:
- secure-deploy
environment:
name: production
url: https://prod.example.com
script:
- echo "Deploying to production..."
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual # Nécessite un clic humain
allow_failure: false # Le pipeline reste bloqué jusqu'à approbation
Fonctionnement de l’Approbation
- Un push sur
maindéclenche le pipeline. deploy-stagings’exécute automatiquement.deploy-productionaffiche un bouton Play dans l’interface du pipeline.- Cliquer sur Play ne lance pas immédiatement le job — GitLab vérifie les règles de protection de l’environnement et présente une boîte de dialogue d’approbation au(x) approbateur(s) désigné(s).
- Le job ne démarre qu’après avoir reçu le nombre d’approbations requis.
Cette double porte — when: manual plus approbation d’environnement — garantit qu’aucune personne seule ne peut pousser du code directement en production sans revue.
Exercice 4 : Portée du CI_JOB_TOKEN
Chaque job GitLab CI reçoit un token automatique dans la variable CI_JOB_TOKEN. Ce token authentifie les requêtes API et Git en tant que projet du pipeline. Par défaut, sa portée est dangereusement large.
Le Risque
Sans restrictions, un job dans le Projet A peut utiliser CI_JOB_TOKEN pour cloner ou appeler l’API de n’importe quel autre projet dans le même groupe (ou instance, selon les paramètres). Si un contributeur malveillant injecte un script dans un job CI, il peut exfiltrer du code depuis des dépôts non liés.
Étape 1 — Restreindre la Portée du Token
- Allez dans Settings > CI/CD > Token Access.
- Basculez Limit access to this project sur Enabled.
- Sous Allow CI job tokens from the following projects to access this project, ajoutez uniquement les projets qui ont réellement besoin d’accès (modèle de liste d’autorisation).
- Sous Limit CI_JOB_TOKEN access to the following projects (sortant), ajoutez uniquement les projets que votre pipeline doit atteindre.
Étape 2 — Tester l’Accès
# .gitlab-ci.yml — Exercice 4
stages:
- test
test-token-allowed:
stage: test
script:
- echo "Cloning an allowed project..."
- git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/my-group/allowed-project.git
- echo "Success — access permitted"
test-token-denied:
stage: test
script:
- echo "Cloning a non-allowed project..."
- git clone https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com/my-group/restricted-project.git
# Sortie attendue : remote: HTTP Basic: Access denied
# fatal: Authentication failed — 403 Forbidden
allow_failure: true
Étape 3 — Vérifier
- Exécutez le pipeline.
test-token-allowedréussit et clone le projet autorisé. test-token-deniedéchoue avec 403 Forbidden carrestricted-projectn’est pas dans la liste d’autorisation.
Leçon clé : Restreignez toujours le CI_JOB_TOKEN à l’ensemble minimal de projets dont votre pipeline a réellement besoin. Considérez la portée par défaut « ouverte » comme une mauvaise configuration.
Exercice 5 : Sécurité des Pipelines de Merge Request
Les pipelines de merge request (MR) s’exécutent lorsqu’un contributeur ouvre ou met à jour une merge request. Ils sont essentiels pour la qualité du code — mais ils peuvent aussi constituer un vecteur d’attaque s’ils ne sont pas configurés avec soin.
Le Risque
Lorsqu’un contributeur externe fork votre projet et ouvre une MR, GitLab peut exécuter un pipeline sur cette MR. Si le pipeline a accès à des variables protégées ou à des runners privilégiés, le code du contributeur pourrait exfiltrer des secrets.
Étape 1 — Configurer les Règles de Pipeline MR
# .gitlab-ci.yml — Exercice 5
stages:
- validate
- build
- deploy
# --- Jobs sûrs pour les pipelines MR (aucun secret nécessaire) ---
lint:
stage: validate
script:
- echo "Linting code..."
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
unit-tests:
stage: validate
script:
- echo "Running unit tests..."
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
# --- Jobs nécessitant des secrets — ne jamais exécuter sur les pipelines MR ---
build-image:
stage: build
script:
- echo "Building and pushing Docker image..."
- echo "Using REGISTRY_TOKEN = $REGISTRY_TOKEN" # Protected + Masked
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-production:
stage: deploy
tags:
- secure-deploy
environment:
name: production
url: https://prod.example.com
script:
- echo "Deploying to production..."
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual
Gestion des Pipelines MR de Forks par GitLab
- Les pipelines déclenchés par
merge_request_eventdepuis un fork s’exécutent automatiquement avec des permissions limitées. - Les variables protégées ne sont jamais injectées dans les pipelines MR de forks.
- Le
CI_JOB_TOKENdans les pipelines de forks a une portée réduite — il ne peut accéder qu’au projet source (fork), pas au projet cible.
En séparant vos jobs en « sûrs pour les MR » (lint, test) et « nécessitant des secrets » (build, deploy), vous garantissez que les contributeurs peuvent valider leur code sans exposer les identifiants.
Bonnes Pratiques pour les Pipelines MR
- N’utilisez jamais
only/except— préférezrules:pour plus de clarté et de fiabilité. - Conditionnez les jobs dépendant de secrets avec
if: $CI_COMMIT_BRANCH == "main"(ou une autre référence protégée). - Envisagez d’activer Pipelines must succeed sous Settings > Merge requests pour exiger que le pipeline MR réussisse avant la fusion.
- Activez les Merged results pipelines pour tester le résultat de la fusion plutôt que la seule branche source — cela détecte les problèmes d’intégration plus tôt.
Exercice 6 : Renforcement Supplémentaire
Avec les variables, runners, environnements, tokens et pipelines MR sécurisés, plusieurs contrôles supplémentaires amènent votre pipeline à un niveau de sécurité prêt pour la production.
Délais d’Expiration des Jobs
Les jobs sans limite de temps peuvent être exploités pour le crypto-mining ou utilisés pour maintenir un accès persistant. Définissez des délais explicites :
deploy-production:
stage: deploy
timeout: 10 minutes
script:
- echo "Deploying..."
Pipelines Interruptibles
Évitez le gaspillage de ressources et limitez la fenêtre d’exploitation des jobs malveillants de longue durée en marquant les jobs non critiques comme interruptibles :
lint:
stage: validate
interruptible: true # Annulé automatiquement si un pipeline plus récent démarre
script:
- echo "Linting..."
Règles de Push (Restreindre la Création de Pipelines)
Sous Settings > Repository > Push rules, vous pouvez :
- Rejeter les commits non signés — garantit que chaque commit est signé GPG.
- Restreindre les noms de branches — imposer une convention de nommage (ex.
feature/*,bugfix/*). - Empêcher le push de secrets — la règle de push intégrée de GitLab peut bloquer les fichiers correspondant aux patterns de secrets courants.
Détection de Secrets avec GitLab SAST
Ajoutez le template intégré de Détection de Secrets de GitLab pour détecter les identifiants accidentellement committés :
include:
- template: Security/Secret-Detection.gitlab-ci.yml
Cela ajoute un job secret_detection qui analyse chaque commit à la recherche de clés API, tokens, mots de passe et autres patterns de secrets. Les résultats apparaissent dans l’onglet Security des merge requests.
Le Pipeline Renforcé Final
Voici le fichier .gitlab-ci.yml complet combinant tous les contrôles de sécurité de ce lab. Chaque ligne liée à la sécurité est commentée.
# .gitlab-ci.yml — Pipeline GitLab CI Entièrement Renforcé
# Inclure le scanner de détection de secrets intégré de GitLab
include:
- template: Security/Secret-Detection.gitlab-ci.yml # Analyse les secrets divulgués
stages:
- validate
- build
- deploy
# --- Paramètres par défaut appliqués à tous les jobs ---
default:
timeout: 10 minutes # Empêcher les jobs incontrôlés/exploités
# --- Sûr pour les pipelines de merge request (aucun secret nécessaire) ---
lint:
stage: validate
interruptible: true # Annulé si un pipeline plus récent démarre
script:
- echo "Linting source code..."
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # Exécuter sur les MR
- if: $CI_COMMIT_BRANCH == "main" # Exécuter sur main
unit-tests:
stage: validate
interruptible: true
script:
- echo "Running unit tests..."
- echo "API_KEY length = ${#API_KEY}" # Sûr : affiche uniquement la longueur
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"
# --- Nécessite des secrets — s'exécute uniquement sur la branche protégée ---
build-image:
stage: build
script:
- echo "Building Docker image..."
- echo "Authenticating to registry..." # Utilise REGISTRY_TOKEN (Protected + Masked)
rules:
- if: $CI_COMMIT_BRANCH == "main" # Uniquement sur la branche protégée
# --- Déploiement staging — automatique sur main ---
deploy-staging:
stage: deploy
environment:
name: staging # Environnement suivi
url: https://staging.example.com
script:
- echo "Deploying to staging..."
rules:
- if: $CI_COMMIT_BRANCH == "main"
# --- Déploiement production — manuel + approbation requise ---
deploy-production:
stage: deploy
tags:
- secure-deploy # S'exécute uniquement sur le runner spécifique au projet
environment:
name: production # Environnement protégé avec approbations
url: https://prod.example.com
script:
- echo "Deploying to production..."
- |
curl --fail --silent \
--header "PRIVATE-TOKEN: $DEPLOY_TOKEN" \ # Variable Protected + Masked
--request POST \
"https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/deployments"
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual # Nécessite un déclenchement humain
allow_failure: false # Le pipeline bloque jusqu'à approbation
timeout: 5 minutes # Délai plus serré pour les déploiements
Nettoyage
Après avoir terminé le lab, nettoyez les ressources de test :
- Supprimer ou archiver le projet de test : Allez dans Settings > General > Advanced > Delete project.
- Supprimer les variables CI/CD : Si vous prévoyez de conserver le projet, allez dans Settings > CI/CD > Variables et supprimez les variables de test (
DEPLOY_TOKEN,DB_PASSWORD,API_KEY). - Désenregistrer le runner de test :
# Lister les runners enregistrés
sudo gitlab-runner list
# Désenregistrer le runner de test
sudo gitlab-runner unregister --name "secure-deploy-runner"
# Optionnellement, supprimer complètement GitLab Runner
sudo gitlab-runner stop
sudo gitlab-runner uninstall
sudo rm /usr/local/bin/gitlab-runner
Points Clés à Retenir
- Protégez et masquez chaque variable secrète. Les variables protégées ne sont injectées que sur les branches protégées, et le masquage empêche l’exposition accidentelle dans les logs. Utilisez l’indicateur Hidden pour les secrets qui ne devraient jamais être lisibles dans l’interface.
- Limitez la portée des runners au niveau de confiance minimum requis. Utilisez des runners spécifiques au projet avec l’accès
ref_protectedpour les jobs de déploiement. Réservez les runners partagés aux étapes non sensibles de build et de test. - Conditionnez les déploiements en production avec la protection d’environnement et les approbations. Combiner
when: manualavec un environnement protégé et des approbateurs requis garantit qu’aucune personne seule ne peut pousser en production sans contrôle. - Restreignez le CI_JOB_TOKEN à une liste d’autorisation explicite. La portée par défaut est trop large. Limitez l’accès entrant et sortant aux seuls projets dont votre pipeline a réellement besoin.
- Séparez les jobs de pipeline MR des jobs de déploiement. Les jobs de lint et de test sont sûrs pour les pipelines de merge request ; les jobs de build et de déploiement nécessitant des secrets ne doivent s’exécuter que sur les branches protégées.
- Superposez les contrôles supplémentaires : délais d’expiration, jobs interruptibles, règles de push et détection de secrets. Chaque couche adresse un vecteur d’attaque différent et ensemble, elles créent une défense en profondeur.
Prochaines Étapes
Continuez à développer vos connaissances en sécurité CI/CD avec ces guides connexes :
- Modèles d’Exécution CI/CD et Hypothèses de Confiance — Comprenez les implications de sécurité des différentes architectures CI/CD et où se situent les frontières de confiance.
- Séparation des Responsabilités et Moindre Privilège dans les Pipelines CI/CD — Apprenez à concevoir des pipelines où aucun rôle ou token n’a plus d’accès que nécessaire.