Séparation des Responsabilités et Moindre Privilège dans les Pipelines CI/CD

Introduction

La plupart des pipelines CI/CD démarrent avec un objectif simple : acheminer le code depuis la machine d’un développeur vers la production le plus rapidement possible. En cours de route, quelqu’un crée un compte de service, lui accorde des permissions larges, stocke les identifiants comme secret du pipeline, et passe à autre chose. Cela fonctionne. Les builds passent, les déploiements réussissent, et personne n’y repense — jusqu’à ce qu’un attaquant compromette ce pipeline et découvre qu’il possède les clés de l’ensemble du royaume.

Le problème n’est pas que les équipes sont négligentes. Le problème est que les outils CI/CD rendent facile l’octroi de privilèges excessifs et difficile l’application de contrôles granulaires. Le chemin par défaut dans la plupart des plateformes est une identité unique avec un accès large exécutant chaque étape du pipeline. Les principes de sécurité comme la séparation des responsabilités (SoD) et le moindre privilège ressemblent à des contraintes bureaucratiques quand on essaie de livrer des fonctionnalités.

Ils ne le sont pas. Ce sont des contrôles d’ingénierie qui limitent le rayon d’impact quand — et non si — quelque chose tourne mal. Ce guide explique comment appliquer ces deux principes aux pipelines CI/CD d’une manière qui renforce votre posture de sécurité sans détruire la vélocité de livraison.

Pourquoi les pipelines accumulent les privilèges

Avant de plonger dans les solutions, il convient de comprendre comment les pipelines se retrouvent avec des permissions excessives. Le schéma est remarquablement constant d’une organisation à l’autre.

Le problème du compte de service unique

Tout commence avec un seul compte de service — souvent nommé quelque chose comme ci-deployer ou pipeline-bot — créé lors de la mise en place initiale du CI/CD. Ce compte a besoin de récupérer le code, donc il obtient l’accès en lecture au dépôt. Puis il a besoin de pousser des images Docker, donc il obtient l’accès en écriture au registre. Puis il doit déployer en staging, puis en production, puis gérer l’infrastructure. En quelques semaines, cette seule identité peut construire, tester, déployer et accéder aux données de production. Elle est devenue un passe-partout.

Les tokens omnipotents

Étroitement lié est le « token omnipotent » — un jeton d’accès personnel ou une clé API avec un accès en lecture/écriture à tout. Ces tokens sont souvent créés par un administrateur lors de la mise en place, stockés comme variable CI/CD, et jamais renouvelés. Ils survivent généralement à la personne qui les a créés, et personne ne se souvient exactement des permissions qu’ils portent.

La commodité avant la sécurité

Les organisations utilisent fréquemment un pool unique de runners pour tous les environnements. La même machine qui exécute les tests unitaires sur des pull requests non fiables provenant de forks a également accès réseau à l’infrastructure de production. Le raisonnement est simple : maintenir des pools de runners séparés est opérationnellement coûteux. Mais cette commodité signifie qu’une pull request malveillante pourrait potentiellement accéder aux identifiants de production.

Absence de frontières d’identité entre les étapes

La plupart des pipelines exécutent toutes les étapes sous la même identité. L’étape de build, l’étape de test, l’étape de déploiement — elles partagent toutes le même compte de service, les mêmes secrets et le même accès réseau. Il n’y a pas de frontière entre « compiler du code » et « pousser en production ». Du point de vue de la sécurité, ce sont des niveaux de confiance fondamentalement différents qui ne devraient jamais partager une identité.

Séparation des responsabilités dans le CI/CD

La séparation des responsabilités est un principe de conception des contrôles qui garantit qu’aucune entité unique ne dispose d’un accès suffisant pour accomplir seule un processus critique. En CI/CD, cela signifie diviser délibérément les opérations du pipeline entre différentes identités, approbations et frontières de confiance.

Principes fondamentaux pour la SoD des pipelines

  • Aucune identité unique ne devrait construire ET déployer en production. L’identité qui compile le code et produit les artefacts ne devrait pas être la même que celle qui pousse ces artefacts vers l’infrastructure de production.
  • Les auteurs du code ne devraient pas approuver leurs propres déploiements. La personne qui écrit le code ne devrait pas être la seule approbatrice de la mise en production de ce code. Au minimum, un second humain doit être impliqué.
  • Les définitions de pipeline devraient être protégées du code qu’elles traitent. Les fichiers de workflow qui définissent comment le code est construit et déployé ne devraient pas être modifiables par le même processus qui exécute le code applicatif.
  • Les artefacts de build devraient être immuables une fois produits. Une fois qu’une étape de build produit un artefact, aucune étape ultérieure ne devrait pouvoir le modifier. L’artefact qui a été testé est l’artefact qui est déployé.

Correspondance entre SoD et étapes du pipeline

Un pipeline bien conçu associe la séparation des responsabilités à des étapes distinctes, chacune avec sa propre identité et ses propres permissions :

  • Build — Compile le code, résout les dépendances, produit les artefacts. Nécessite un accès en lecture au code source et aux registres de dépendances. Aucun accès aux cibles de déploiement.
  • Test — Exécute les tests unitaires, les tests d’intégration, les analyses de sécurité. Nécessite un accès en lecture aux artefacts et à l’infrastructure de test. Aucun accès aux secrets de production.
  • Signature — Signe cryptographiquement les artefacts qui passent toutes les vérifications. Nécessite l’accès aux clés de signature mais rien d’autre. Cette étape agit comme une porte de contrôle.
  • Staging — Déploie dans un environnement de staging pour validation finale. Nécessite un accès en écriture au staging uniquement. Aucun identifiant de production disponible.
  • Déploiement — Promeut l’artefact signé et testé en production. Nécessite un accès en écriture à la production, protégé par une approbation manuelle. Identité différente de celle du build.

Chaque frontière d’étape est une frontière de confiance. Les identifiants ne circulent pas entre les étapes sauf s’ils sont explicitement accordés.

Modèles de moindre privilège

Le moindre privilège signifie n’accorder que les permissions minimales requises pour une tâche spécifique, pour la durée la plus courte nécessaire. En CI/CD, cela se traduit par des modèles concrets qui varient selon la plateforme.

GitHub Actions : permissions par job

GitHub Actions fournit un bloc permissions qui contrôle les portées du GITHUB_TOKEN généré automatiquement. Par défaut, ce token dispose d’un accès large en lecture/écriture. Vous devriez toujours le restreindre.

Définissez des valeurs par défaut restrictives au niveau du workflow et n’accordez des permissions supplémentaires qu’aux jobs spécifiques qui en ont besoin :

# .github/workflows/deploy.yml
name: Build and Deploy

# Restrict default permissions for ALL jobs in this workflow
permissions:
  contents: read

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    # This job inherits the workflow-level permissions: contents read only
    steps:
      - uses: actions/checkout@v4
      - run: make build
      - uses: actions/upload-artifact@v4
        with:
          name: app-binary
          path: dist/

  security-scan:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      security-events: write  # Only this job can write security findings
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/analyze@v3

  deploy-staging:
    needs: [build, security-scan]
    runs-on: ubuntu-latest
    environment: staging  # Scoped to staging secrets only
    permissions:
      contents: read
      id-token: write  # OIDC token for cloud auth — no static credentials
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/staging-deployer
          aws-region: us-east-1
      - run: ./scripts/deploy.sh staging

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment: production  # Requires manual approval + different secrets
    permissions:
      contents: read
      id-token: write
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/production-deployer
          aws-region: us-east-1
      - run: ./scripts/deploy.sh production

Points clés de cette configuration : chaque job déclare uniquement les permissions dont il a besoin, les jobs de déploiement utilisent OIDC pour des identifiants éphémères au lieu de secrets statiques, et le staging et la production utilisent des rôles IAM différents avec des niveaux d’accès différents.

GitLab CI : variables protégées et runners

GitLab CI offre un ensemble de contrôles différent mais tout aussi puissant pour implémenter le moindre privilège :

# .gitlab-ci.yml
stages:
  - build
  - test
  - deploy-staging
  - deploy-production

build:
  stage: build
  tags:
    - shared-runners  # Non-privileged runners for build
  script:
    - make build
  artifacts:
    paths:
      - dist/

test:
  stage: test
  tags:
    - shared-runners
  script:
    - make test
  dependencies:
    - build

deploy-staging:
  stage: deploy-staging
  tags:
    - deploy-runners  # Dedicated runners with network access to staging
  environment:
    name: staging
  script:
    - ./scripts/deploy.sh staging
  # CI_JOB_TOKEN is automatically scoped to this project
  # Staging secrets are only available on protected branches
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

deploy-production:
  stage: deploy-production
  tags:
    - production-runners  # Isolated runners with production network access
  environment:
    name: production
  script:
    - ./scripts/deploy.sh production
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
      when: manual  # Requires manual approval
  allow_failure: false  # Block the pipeline if not approved

Dans GitLab, utilisez les variables protégées afin que les identifiants de production ne soient disponibles que pour les branches et tags protégés. Utilisez les runners protégés pour garantir que les jobs de déploiement en production ne s’exécutent que sur une infrastructure durcie et isolée. Marquez les secrets de production comme masqués pour prévenir toute exposition accidentelle dans les logs.

Comptes de service par étape avec rôles IAM scopés

Au-delà de la plateforme CI elle-même, appliquez le moindre privilège au niveau du fournisseur cloud. Chaque étape du pipeline devrait s’authentifier avec un compte de service différent disposant de permissions strictement délimitées :

  • Étape de build : Accès en lecture seule aux dépôts source et aux registres de dépendances. Accès en écriture au stockage d’artefacts (par exemple, un bucket S3, un registre de conteneurs) mais rien d’autre.
  • Étape de test : Accès en lecture seule aux artefacts. Permission de créer et détruire une infrastructure de test éphémère, mais aucun accès au staging ou à la production.
  • Étape de déploiement : Accès en écriture à la cible de déploiement spécifique (par exemple, un namespace Kubernetes unique, un service ECS spécifique). Aucun accès aux autres environnements ou services.

Identifiants éphémères via OIDC

Les identifiants statiques stockés comme secrets CI/CD sont une dette de sécurité. Ils n’expirent pas, sont difficiles à renouveler et constituent des cibles attractives pour les attaquants. L’approche moderne consiste à utiliser la fédération OIDC pour que votre plateforme CI/CD échange un token éphémère, signé par la plateforme, contre des identifiants cloud temporaires :

  • GitHub Actions peut assumer des rôles AWS IAM, des comptes de service GCP ou des identités managées Azure en utilisant la permission id-token: write — aucun secret stocké n’est nécessaire.
  • GitLab CI supporte OIDC nativement via son CI_JOB_JWT ou le mot-clé id_tokens, permettant le même modèle.
  • Ces identifiants durent généralement de 15 à 60 minutes et sont limités au job spécifique qui les a demandés.

Si vous utilisez encore des clés d’accès statiques dans les secrets de votre pipeline, la migration vers OIDC devrait être une initiative hautement prioritaire.

Protéger les définitions de pipeline

Le pipeline-as-code est puissant, mais il introduit un problème de confiance subtil : si un attaquant peut modifier la définition du pipeline, il contrôle comment le code est construit, testé et déployé. Protéger les fichiers de configuration du pipeline est tout aussi important que protéger le code applicatif lui-même.

CODEOWNERS pour les fichiers de workflow

Utilisez un fichier CODEOWNERS pour exiger que des équipes spécifiques examinent les modifications apportées à la configuration CI/CD :

# .github/CODEOWNERS
/.github/workflows/   @your-org/platform-security
/.gitlab-ci.yml       @your-org/platform-security
/Jenkinsfile          @your-org/platform-security
/terraform/           @your-org/infrastructure

Cela garantit que personne ne peut modifier les définitions de pipeline sans l’approbation de l’équipe responsable de la sécurité CI/CD. Combinez cela avec des règles de protection de branches qui exigent l’approbation des CODEOWNERS.

Protection de branche sur les répertoires de configuration du pipeline

Appliquez des règles de protection de branche qui empêchent les pushs directs vers les branches contenant les définitions de pipeline. Au minimum :

  • Exigez des revues de pull request avant de fusionner les modifications des fichiers de workflow.
  • Exigez que les vérifications de statut passent (y compris les analyses de sécurité des modifications de workflow elles-mêmes).
  • Désactivez les force pushs vers les branches protégées.
  • Exigez des commits signés pour les modifications de la configuration du pipeline.

Templates de pipeline immuables

GitHub Actions et GitLab CI supportent tous deux le référencement de définitions de pipeline depuis des dépôts externes gérés de manière centralisée :

Workflows réutilisables GitHub Actions :

# In your repository's workflow, reference a centrally managed template
jobs:
  deploy:
    uses: your-org/shared-workflows/.github/workflows/secure-deploy.yml@v2.1.0
    with:
      environment: production
      artifact-name: app-binary
    secrets: inherit

Inclusions GitLab CI depuis des dépôts protégés :

# .gitlab-ci.yml
include:
  - project: 'platform-team/ci-templates'
    ref: 'v3.0.0'
    file: '/templates/secure-deploy.yml'

# Local jobs can use templates but cannot override protected stages
deploy-production:
  extends: .secure-deploy-template
  variables:
    TARGET_ENV: production

En épinglant des versions spécifiques (tags ou SHA de commits) et en restreignant qui peut modifier le dépôt de templates, vous garantissez que les équipes individuelles ne peuvent pas altérer les contrôles de sécurité intégrés au processus de déploiement.

Empêcher les pipelines auto-modifiants

Un pipeline ne devrait jamais pouvoir modifier sa propre définition. Surveillez ces schémas :

  • Des étapes de pipeline qui écrivent dans .github/workflows/ ou .gitlab-ci.yml et commitent les modifications.
  • La génération dynamique de pipeline qui récupère la configuration depuis des sources non fiables.
  • Des variables de pipeline qui peuvent outrepasser des paramètres critiques pour la sécurité, comme le runner à utiliser ou l’environnement dans lequel déployer.

Si vous avez besoin d’un comportement dynamique de pipeline, utilisez des templates paramétrés avec un ensemble fixe d’entrées autorisées plutôt que de permettre la modification arbitraire de la logique du pipeline.

Contrôles de déploiement

Les contrôles de déploiement sont les portes entre les étapes du pipeline qui imposent la supervision humaine et la conformité aux politiques. C’est là que la séparation des responsabilités devient tangible.

Réviseurs requis et approbations manuelles

Les déploiements en production devraient exiger une approbation explicite de quelqu’un d’autre que l’auteur du code. Les deux principales plateformes supportent cela nativement :

  • GitHub Environments permettent de configurer des réviseurs requis. Lorsqu’un job de workflow référence un environnement avec des règles de protection, le pipeline se met en pause jusqu’à ce qu’un réviseur autorisé approuve.
  • GitLab Protected Environments restreignent quels utilisateurs ou groupes peuvent déclencher des déploiements vers des environnements spécifiques. Combiné avec when: manual, cela crée une porte d’approbation.

GitHub Environments avec règles de protection

Les GitHub Environments sont un mécanisme puissant pour les contrôles de déploiement. Configurez-les avec :

  • Réviseurs requis : Spécifiez les individus ou équipes qui doivent approuver avant l’exécution du job. Utilisez au moins deux réviseurs pour la production.
  • Minuterie d’attente : Ajoutez un délai entre l’approbation et l’exécution, laissant le temps de détecter des erreurs ou de se coordonner avec les fenêtres de changement.
  • Branches de déploiement : Restreignez les branches pouvant déployer vers l’environnement. La production ne devrait accepter que les déploiements depuis main ou release/*.
  • Secrets d’environnement : Stockez les identifiants au niveau de l’environnement, pas du dépôt. Cela garantit que les secrets de staging ne sont pas disponibles pour les jobs de production et vice versa.

Gels de déploiement et fenêtres de changement

Implémentez des gels de déploiement pendant les périodes commerciales critiques (par exemple, Black Friday, fin de trimestre) en :

  • Utilisant des règles de protection d’environnement planifiées qui bloquent automatiquement les déploiements pendant les fenêtres définies.
  • Exigeant des approbations supplémentaires pendant les périodes de gel plutôt que de bloquer entièrement — les correctifs d’urgence devraient toujours être possibles avec une supervision renforcée.
  • Enregistrant toutes les dérogations au gel à des fins d’audit.

Canary et déploiement progressif comme mécanisme de contrôle

Les stratégies de déploiement progressif ne sont pas seulement une préoccupation de disponibilité — elles constituent un contrôle de sécurité. Si un artefact compromis passe tous les autres contrôles, un déploiement canary limite le rayon d’impact :

  • Déployez d’abord vers 1 à 5 % du trafic avec des vérifications de santé automatisées.
  • Exigez une seconde approbation manuelle pour poursuivre au-delà de l’étape canary.
  • Automatisez le rollback si les taux d’erreur ou la latence dépassent les seuils.
  • Traitez l’étape canary comme un environnement distinct avec ses propres exigences d’approbation.

Audit et traçabilité

La séparation des responsabilités et le moindre privilège ne sont efficaces que si vous pouvez vérifier qu’ils sont respectés. L’audit et la traçabilité bouclent la boucle en fournissant la preuve de qui a fait quoi, quand et pourquoi.

Logs d’exécution de pipeline comme pistes d’audit

Les plateformes CI/CD maintiennent des logs détaillés de chaque exécution de pipeline. Traitez ces logs comme des données d’audit pertinentes pour la sécurité :

  • Conservez les logs de pipeline pendant au moins 90 jours (plus longtemps si votre cadre de conformité l’exige).
  • Exportez les logs vers un stockage centralisé et inviolable (par exemple, SIEM, CloudWatch Logs, ou un bucket S3 dédié avec verrouillage d’objet).
  • Assurez-vous que les logs capturent quelle identité a déclenché le pipeline, quels secrets ont été accédés (mais pas leurs valeurs) et quelles approbations ont été accordées.

Lier les déploiements aux commits et aux approbateurs

Chaque déploiement en production devrait être traçable jusqu’à :

  • Le SHA de commit Git spécifique qui a été déployé.
  • La pull request qui a introduit le changement, incluant tous les réviseurs et approbateurs.
  • L’identifiant d’exécution du pipeline et la personne qui a approuvé la porte de déploiement.
  • Le digest de l’artefact (SHA d’image conteneur, hash de binaire) qui a été déployé.

Cette chaîne de traçabilité devrait être automatisée. Si quelqu’un demande « qu’est-ce qui tourne en production et qui l’a approuvé », la réponse devrait être disponible en secondes, pas en heures.

Logs d’audit cloud pour les changements initiés par le pipeline

Les actions du pipeline laissent des traces dans les logs d’audit du fournisseur cloud. Corrélez-les avec vos logs CI/CD :

  • AWS CloudTrail enregistre chaque appel API effectué par le rôle IAM assumé par votre pipeline. Croisez l’identifiant d’exécution du pipeline avec le userIdentity.sessionContext de CloudTrail pour lier les changements cloud à des exécutions de pipeline spécifiques.
  • GCP Cloud Audit Logs fournissent une traçabilité similaire pour les actions des comptes de service.
  • Azure Activity Logs capturent les modifications de ressources effectuées par les principaux de service du pipeline.

Alertes sur l’escalade de privilèges ou le contournement des politiques

Configurez des alertes pour les événements qui indiquent que vos contrôles sont contournés :

  • Un pipeline accédant à des secrets dont il ne devrait pas avoir besoin (par exemple, l’étape de build accédant aux identifiants de base de données de production).
  • Des modifications aux règles de protection de branche ou aux fichiers CODEOWNERS.
  • Des modifications aux politiques IAM attachées aux comptes de service du pipeline.
  • Des exécutions de pipeline déclenchées depuis des branches non protégées déployant vers des environnements protégés.
  • Des approbations de déploiement accordées par la même personne qui a écrit le code.

Intégrez ces alertes dans votre workflow d’opérations de sécurité et traitez-les avec la même urgence que les incidents de production.

Anti-patterns courants

Savoir quoi éviter est tout aussi important que savoir quoi implémenter. Voici les anti-patterns que nous rencontrons le plus fréquemment lors des évaluations de sécurité CI/CD.

Un seul token admin pour toutes les opérations CI/CD

Un jeton d’accès personnel avec une portée admin, créé par un ingénieur senior, stocké comme secret de dépôt, et utilisé par chaque job dans chaque pipeline. Quand cet ingénieur quitte l’organisation, personne ne révoque le token parce que personne ne sait ce qui cessera de fonctionner. C’est l’anti-pattern le plus courant et le plus dangereux.

Correction : Remplacez par des comptes de service par étape utilisant la fédération OIDC. Aucun token statique, aucune identité partagée.

Désactiver la protection de branche « temporairement »

Un déploiement est bloqué parce qu’une vérification requise échoue. Quelqu’un désactive la protection de branche pour pousser directement sur main, avec l’intention de la réactiver plus tard. Il oublie, ou il la réactive mais manque un paramètre. Entre-temps, la fenêtre non protégée a permis un push direct qui a contourné la revue de code.

Correction : Ne désactivez jamais la protection de branche. Si une vérification requise échoue, corrigez la vérification ou utilisez un processus d’urgence qui exige plusieurs approbations et crée une piste d’audit.

Runners partagés entre production et charges de travail de PR

Les pipelines de pull request provenant de forks s’exécutent sur la même infrastructure qui a accès réseau aux systèmes de production. Une PR malveillante peut exfiltrer des secrets, accéder à des services internes ou pivoter vers l’infrastructure de production.

Correction : Utilisez des pools de runners séparés. Les charges de travail non fiables (PR, surtout celles provenant de forks) s’exécutent sur des runners éphémères et isolés sans accès aux environnements sensibles. Les runners de déploiement en production sont restreints aux branches protégées uniquement.

Accès SSH manuel quand les pipelines échouent

Lorsqu’un pipeline de déploiement échoue, un ingénieur se connecte en SSH directement à un serveur de production et déploie manuellement. Cela contourne chaque contrôle du pipeline : revue de code, tests automatisés, signature d’artefact, approbation de déploiement et journalisation d’audit.

Correction : Investissez dans la fiabilité du pipeline pour que l’intervention manuelle soit rarement nécessaire. Quand elle l’est, utilisez une procédure de bris de glace qui exige plusieurs approbations, crée une piste d’audit et déclenche une revue post-incident.

Conclusion

La séparation des responsabilités et le moindre privilège ne sont pas des contraintes bureaucratiques imposées par une équipe de conformité. Ce sont des contrôles d’ingénierie qui réduisent directement le rayon d’impact des incidents de sécurité dans votre pipeline de livraison logicielle.

Une étape de build compromise avec le moindre privilège peut produire un artefact malveillant — mais elle ne peut pas déployer cet artefact en production. Un identifiant de déploiement compromis limité au staging ne peut pas atteindre la production. Une pull request malveillante traitée par un runner isolé ne peut pas exfiltrer les secrets de production. Chaque contrôle limite ce qu’un attaquant peut accomplir à chaque étape.

Commencez par auditer les permissions actuelles de votre pipeline. Identifiez chaque compte de service, chaque identifiant stocké et chaque secret auquel votre pipeline peut accéder. Associez chacun à l’étape et à la tâche spécifique qui en a besoin. Puis commencez à réduire la portée : remplacez les identifiants statiques par OIDC, divisez les comptes de service uniques en identités par étape, ajoutez des portes d’approbation pour les déploiements en production et protégez vos définitions de pipeline avec CODEOWNERS et la protection de branche.

Vous n’avez pas besoin de tout faire en une fois. Chaque amélioration incrémentale réduit le risque. Mais commencez — car votre pipeline CI/CD est probablement le système le plus privilégié et le moins scruté de toute votre infrastructure.