Sécuriser un pipeline GitLab CI ne se réduit pas à masquer les variables. Le durcissement couvre toute la chaîne : les permissions du token de job, l’isolation des runners, l’épinglage des dépendances, la restriction des includes et la configuration au niveau instance. Ce guide détaille 10 mesures concrètes, classées de la plus simple à la plus structurante, avec les commandes pour vérifier chaque point sur votre instance KVM.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Restreindre les permissions du
CI_JOB_TOKENau strict nécessaire - Isoler les runners par niveau de confiance (shared, group, project)
- Désactiver le mode
privilegedet configurer les executors de manière sécurisée - Épingler les images Docker et les includes par hash ou version exacte
- Restreindre les Actions autorisées au niveau instance et groupe
- Auditer la configuration avec
plumberet des scripts API
Dans quel contexte ?
Section intitulée « Dans quel contexte ? »Vous avez une instance GitLab CE auto-hébergée sur KVM avec des runners enregistrés. Vos pipelines fonctionnent, les secrets sont protégés (guide précédent), mais vous n’avez pas encore restreint les permissions par défaut. Un développeur peut encore déclencher un pipeline qui fait du Docker-in-Docker en mode privileged, référencer n’importe quelle image sans vérification, ou inclure un fichier YAML distant non audité.
Mesure 1 — Restreindre le scope du CI_JOB_TOKEN
Section intitulée « Mesure 1 — Restreindre le scope du CI_JOB_TOKEN »Le CI_JOB_TOKEN est un token éphémère créé automatiquement pour chaque job. Par défaut, il peut accéder en lecture à tous les projets du groupe.
Le problème
Section intitulée « Le problème »steal-code: script: - git clone https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.example.com/org/autre-projet.git # Fonctionne si "autre-projet" n'a pas restreint l'accèsLa correction
Section intitulée « La correction »-
Restreindre les projets autorisés dans les settings du projet cible
Dans le projet cible (celui qu’on veut protéger) : Settings > CI/CD > Token Access → décocher Allow access to this project with a CI_JOB_TOKEN ou limiter à une liste de projets autorisés.
-
Vérifier via l’API
Vérifier la configuration du token access curl --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \"https://gitlab.example.com/api/v4/projects/$PROJECT_ID" \| jq '{ci_inbound_job_token_scope_enabled: .ci_restrict_pipeline_cancellation_role}' -
Limiter les permissions du token dans le pipeline
.gitlab-ci.yml — Permissions minimales default:id_tokens:VAULT_TOKEN:aud: https://vault.example.com
Mesure 2 — Désactiver le mode privileged sur les runners
Section intitulée « Mesure 2 — Désactiver le mode privileged sur les runners »Un runner Docker en mode privileged donne un accès root complet à l’hôte. C’est le vecteur d’évasion de conteneur le plus direct.
[[runners]] name = "shared-runner" [runners.docker] privileged = true[[runners]] name = "shared-runner" [runners.docker] privileged = false cap_drop = ["ALL"] security_opt = ["no-new-privileges:true"]Mesure 3 — Séparer les runners par niveau de confiance
Section intitulée « Mesure 3 — Séparer les runners par niveau de confiance »Ne partagez pas les mêmes runners entre des projets publics/fork et des projets internes qui déploient en production.
| Niveau | Runner | Tags | Usage |
|---|---|---|---|
| Public/Fork | Runner dédié, non protégé | public, untrusted | MR externes, CI de contribution |
| Interne | Runner de groupe | internal | Build, tests, qualité |
| Déploiement | Runner de projet, protégé | deploy, protected | Déploiement staging/production |
[[runners]] name = "deploy-runner" limit = 1 [runners.docker] privileged = false allowed_images = ["registry.example.com/deploy/*"] allowed_services = []-
Enregistrer un runner protégé dédié au déploiement
Enregistrer un runner protégé gitlab-runner register \--non-interactive \--url "https://gitlab.example.com" \--token "glrt-xxxxxxxxxxxx" \--executor docker \--docker-image "alpine:3.21" \--tag-list "deploy,protected" \--run-untagged=false -
Marquer le runner comme protégé dans l’UI
Administration > Runners → sélectionner le runner → cocher Protected. Un runner protégé n’exécute que les jobs sur des branches/tags protégés.
-
Forcer les tags dans les jobs de déploiement
.gitlab-ci.yml — Job restreint au runner protégé deploy:production:tags:- deploy- protectedrules:- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHscript:- ansible-playbook deploy-prod.yml
Mesure 4 — Épingler les images Docker par digest
Section intitulée « Mesure 4 — Épingler les images Docker par digest »Référencer une image par tag (python:3.12) expose au tag poisoning — le même vecteur qui a compromis Trivy et KICS sur GitHub Actions.
test: image: python:3.12-slimtest: image: python:3.12-slim@sha256:a3e58f8c7a032e5e4e5f3a37075e4b8f6a1b9c2d4e6f8a0b2c4d6e8f0a2b4c6ddocker manifest inspect python:3.12-slim | jq -r '.manifests[0].digest'Mesure 5 — Verrouiller les includes distants
Section intitulée « Mesure 5 — Verrouiller les includes distants »Un include: remote: charge un fichier YAML depuis une URL externe à chaque exécution du pipeline. Si cette URL est compromise, tous vos pipelines le sont.
include: - remote: https://example.com/templates/deploy.ymlinclude: - project: 'my-org/ci-templates' ref: 'v2.1.0' file: '/templates/deploy.yml'Préférez include: project: avec un tag versionné ou un hash de commit. Si vous devez utiliser include: remote:, auditez le contenu régulièrement et épinglez le hash du fichier.
Mesure 6 — Restreindre les images autorisées au niveau runner
Section intitulée « Mesure 6 — Restreindre les images autorisées au niveau runner »Le config.toml du runner permet de limiter les images que les pipelines peuvent utiliser :
[[runners]] [runners.docker] allowed_images = [ "registry.example.com/*", "python:3.12-*", "node:20-*", "alpine:3.*" ] allowed_services = [ "postgres:16-*", "redis:7-*" ]Toute image non listée provoquera un échec du job avec image is not allowed. C’est la défense la plus efficace contre l’utilisation d’images compromises.
Mesure 7 — Limiter les pipelines de fork
Section intitulée « Mesure 7 — Limiter les pipelines de fork »Les Merge Requests depuis des forks peuvent exécuter du code arbitraire sur vos runners. Par défaut, GitLab exécute les pipelines de fork dans le projet parent.
-
Désactiver les pipelines de fork sur les projets sensibles
Settings > CI/CD > General pipelines → décocher Run pipelines in the parent project for merge requests from forks.
-
Ou exiger une approbation manuelle
Settings > CI/CD > General pipelines → cocher Require approval for all fork pipelines.
-
Vérifier via l’API
Vérifier la configuration des pipelines de fork curl --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \"https://gitlab.example.com/api/v4/projects/$PROJECT_ID" \| jq '{ci_allow_fork_pipelines_to_run_in_parent_project}'
Mesure 8 — Activer la protection des branches par défaut
Section intitulée « Mesure 8 — Activer la protection des branches par défaut »Les branches protégées sont le mécanisme central de GitLab pour contrôler qui peut pousser, merger et déclencher des pipelines sur les branches critiques.
curl --request POST \ --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \ --data "name=main&push_access_level=40&merge_access_level=30&allow_force_push=false" \ "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/protected_branches"| Niveau d’accès | Valeur | Qui peut agir |
|---|---|---|
| No access | 0 | Personne |
| Developer | 30 | Développeurs et au-dessus |
| Maintainer | 40 | Maintainers et au-dessus |
| Admin | 60 | Administrateurs uniquement |
Mesure 9 — Forcer les merge requests avec approbation
Section intitulée « Mesure 9 — Forcer les merge requests avec approbation »Un pipeline de déploiement ne doit jamais se déclencher par un push direct. Exigez une MR avec au moins une approbation.
deploy:production: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: never - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - ansible-playbook deploy-prod.ymlCombinez avec les Merge Request Approvals dans Settings > Merge requests :
- Approvals required : au moins 1 (2 recommandé pour la production)
- Prevent approval by author : activé
- Prevent approval by committers : activé
Mesure 10 — Auditer la configuration avec plumber
Section intitulée « Mesure 10 — Auditer la configuration avec plumber »plumber est un outil open source de Checkmarx qui analyse la configuration de sécurité de vos projets et pipelines GitLab.
plumber scan gitlab \ --token "$GITLAB_API_TOKEN" \ --url "https://gitlab.example.com" \ --project "my-org/my-project"[WARN] Unprotected branch: main (push allowed for Developers)[WARN] Fork pipelines enabled without approval[WARN] CI_JOB_TOKEN scope not restricted[WARN] Runner using privileged mode[INFO] 4 findings, 0 critical, 4 warningsIntégrez plumber dans votre pipeline CI pour un audit continu :
security-audit: stage: test image: checkmarx/plumber:latest script: - plumber scan gitlab --token $CI_JOB_TOKEN --url $CI_SERVER_URL --project $CI_PROJECT_PATH rules: - if: $CI_PIPELINE_SOURCE == "schedule"Checklist de durcissement
Section intitulée « Checklist de durcissement »| # | Mesure | Vérifiable par |
|---|---|---|
| 1 | CI_JOB_TOKEN scope restreint | Settings > CI/CD > Token Access |
| 2 | Mode privileged désactivé | config.toml du runner |
| 3 | Runners séparés par confiance | Administration > Runners (tags + protected) |
| 4 | Images épinglées par digest | Grep @sha256: dans .gitlab-ci.yml |
| 5 | Includes verrouillés par ref | Grep include: project: avec ref: |
| 6 | Images autorisées restreintes | config.toml → allowed_images |
| 7 | Pipelines de fork bloqués ou approuvés | Settings > CI/CD > General pipelines |
| 8 | Branches critiques protégées | Settings > Repository > Protected branches |
| 9 | MR avec approbation obligatoire | Settings > Merge requests > Approvals |
| 10 | Audit plumber régulier | Pipeline schedule hebdomadaire |
Dépannage
Section intitulée « Dépannage »| Symptôme | Cause probable | Solution |
|---|---|---|
Job échoue avec image is not allowed | Image non dans allowed_images du runner | Ajouter l’image à la whitelist dans config.toml |
| Job de déploiement ne démarre pas | Runner protégé + branche non protégée | Vérifier que la branche est dans la liste protégée |
CI_JOB_TOKEN ne peut plus cloner un projet | Scope du token restreint | Ajouter le projet source dans Token Access du projet cible |
Build Docker échoue après désactivation de privileged | DinD nécessite privileged: true | Migrer vers kaniko ou buildah |
| plumber ne se connecte pas | Token insuffisant | Utiliser un PAT avec scope api et read_repository |
À retenir
Section intitulée « À retenir »- Le durcissement couvre toute la chaîne : token, runner, images, includes, branches, approbations
- Le
CI_JOB_TOKENest trop permissif par défaut — restreignez son scope projet par projet - Le mode
privilegedest l’évasion de conteneur la plus simple — désactivez-le et migrez vers kaniko ou buildah - L’épinglage par digest (pas par tag) est la seule protection contre le tag poisoning sur les images Docker
- Les
include: remote:doivent être remplacés par desinclude: project:avec ref versionné - Les branches protégées + MR avec approbation = la porte d’entrée contrôlée du déploiement
- plumber en pipeline schedule détecte les régressions de configuration
- CI/CD job token — GitLab Docs
- GitLab Runner advanced configuration — GitLab Docs
- Protected branches — GitLab Docs
- Merge request approvals — GitLab Docs
- plumber — CI/CD security scanner — Checkmarx
- kaniko — Build container images without Docker — Google
- OWASP Top 10 CI/CD — CICD-SEC-07: Insecure System Configuration — OWASP
Testez vos connaissances
Section intitulée « Testez vos connaissances »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)Profil de compétences
Quoi faire maintenant
Ressources pour progresser
Des indices pour retenter votre chance ?
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications