Aller au contenu
CI/CD & Automatisation medium

GitLab CI : durcir vos pipelines en 10 mesures

14 min de lecture

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.

  • Restreindre les permissions du CI_JOB_TOKEN au strict nécessaire
  • Isoler les runners par niveau de confiance (shared, group, project)
  • Désactiver le mode privileged et 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 plumber et des scripts API

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é.

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.

.gitlab-ci.yml — Mouvement latéral via CI_JOB_TOKEN
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ès
  1. 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.

  2. 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}'
  3. 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.

config.toml — AVANT (dangereux)
[[runners]]
name = "shared-runner"
[runners.docker]
privileged = true
config.toml — APRÈS (sécurisé)
[[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.

NiveauRunnerTagsUsage
Public/ForkRunner dédié, non protégépublic, untrustedMR externes, CI de contribution
InterneRunner de groupeinternalBuild, tests, qualité
DéploiementRunner de projet, protégédeploy, protectedDéploiement staging/production
config.toml — Runner de déploiement isolé
[[runners]]
name = "deploy-runner"
limit = 1
[runners.docker]
privileged = false
allowed_images = ["registry.example.com/deploy/*"]
allowed_services = []
  1. 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
  2. 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.

  3. Forcer les tags dans les jobs de déploiement

    .gitlab-ci.yml — Job restreint au runner protégé
    deploy:production:
    tags:
    - deploy
    - protected
    rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    script:
    - 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.

.gitlab-ci.yml — AVANT (tag mutable)
test:
image: python:3.12-slim
.gitlab-ci.yml — APRÈS (digest immuable)
test:
image: python:3.12-slim@sha256:a3e58f8c7a032e5e4e5f3a37075e4b8f6a1b9c2d4e6f8a0b2c4d6e8f0a2b4c6d
Obtenir le digest d'une image
docker manifest inspect python:3.12-slim | jq -r '.manifests[0].digest'

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.

.gitlab-ci.yml — AVANT (include non vérifié)
include:
- remote: https://example.com/templates/deploy.yml
.gitlab-ci.yml — APRÈS (include depuis un projet versionné)
include:
- 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 :

config.toml — Whitelist d'images
[[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.

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.

  1. 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.

  2. Ou exiger une approbation manuelle

    Settings > CI/CD > General pipelines → cocher Require approval for all fork pipelines.

  3. 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.

Protéger la branche main via l'API
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èsValeurQui peut agir
No access0Personne
Developer30Développeurs et au-dessus
Maintainer40Maintainers et au-dessus
Admin60Administrateurs 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.

.gitlab-ci.yml — Deploy uniquement via MR mergée
deploy:production:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
script:
- ansible-playbook deploy-prod.yml

Combinez 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.

Scanner un projet avec plumber
plumber scan gitlab \
--token "$GITLAB_API_TOKEN" \
--url "https://gitlab.example.com" \
--project "my-org/my-project"
Résultat type
[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 warnings

Intégrez plumber dans votre pipeline CI pour un audit continu :

.gitlab-ci.yml — Audit continu avec plumber
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"
#MesureVérifiable par
1CI_JOB_TOKEN scope restreintSettings > CI/CD > Token Access
2Mode privileged désactivéconfig.toml du runner
3Runners séparés par confianceAdministration > Runners (tags + protected)
4Images épinglées par digestGrep @sha256: dans .gitlab-ci.yml
5Includes verrouillés par refGrep include: project: avec ref:
6Images autorisées restreintesconfig.tomlallowed_images
7Pipelines de fork bloqués ou approuvésSettings > CI/CD > General pipelines
8Branches critiques protégéesSettings > Repository > Protected branches
9MR avec approbation obligatoireSettings > Merge requests > Approvals
10Audit plumber régulierPipeline schedule hebdomadaire
SymptômeCause probableSolution
Job échoue avec image is not allowedImage non dans allowed_images du runnerAjouter l’image à la whitelist dans config.toml
Job de déploiement ne démarre pasRunner protégé + branche non protégéeVérifier que la branche est dans la liste protégée
CI_JOB_TOKEN ne peut plus cloner un projetScope du token restreintAjouter le projet source dans Token Access du projet cible
Build Docker échoue après désactivation de privilegedDinD nécessite privileged: trueMigrer vers kaniko ou buildah
plumber ne se connecte pasToken insuffisantUtiliser un PAT avec scope api et read_repository
  • Le durcissement couvre toute la chaîne : token, runner, images, includes, branches, approbations
  • Le CI_JOB_TOKEN est trop permissif par défaut — restreignez son scope projet par projet
  • Le mode privileged est 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 des include: 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

Contrôle de connaissances

Validez vos connaissances avec ce quiz interactif

10 questions
5 min.
70% requis

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

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn