Restrictions Réseau et Système de Fichiers pour les Environnements de Build CI/CD

Les pipelines CI/CD comptent parmi les charges de travail les plus privilégiées d’une organisation. Ils récupèrent le code source, téléchargent les dépendances, accèdent aux secrets et poussent des artefacts vers les registres de production. Pourtant, dans de nombreux environnements, les processus de build derrière ces pipelines s’exécutent avec un accès réseau illimité et des permissions complètes sur le système de fichiers — une combinaison qui représente l’une des failles les plus exploitables de la livraison logicielle moderne.

Lorsqu’un environnement de build peut joindre n’importe quelle adresse IP et écrire sur n’importe quel chemin du disque, une seule dépendance compromise ou une pull request malveillante peut exfiltrer des secrets, altérer des artefacts ou installer des portes dérobées persistantes. Ce guide présente des techniques concrètes pour verrouiller l’accès réseau et l’accès au système de fichiers dans les environnements de build CI/CD, des NetworkPolicies Kubernetes aux systèmes de build hermétiques.

Pourquoi les environnements de build non restreints sont dangereux

Avant de plonger dans les solutions, il convient de comprendre les menaces spécifiques que créent les environnements de build non restreints. Ces risques ne sont pas théoriques — ils ont été exploités dans de véritables attaques de la chaîne d’approvisionnement logicielle.

Exfiltration de données

Les environnements de build ont fréquemment accès à des secrets : clés API, identifiants de registre, clés de signature et tokens de déploiement. Si un processus de build dispose d’un accès réseau sortant illimité, une dépendance compromise peut envoyer ces secrets vers un serveur contrôlé par un attaquant. Cela peut se produire via un script postinstall malveillant dans un paquet npm, une dépendance PyPI compromise ou même une cible Makefile spécialement conçue. Sans restrictions réseau, il n’existe aucune barrière entre le secret et le point de terminaison de l’attaquant.

Attaques de la chaîne d’approvisionnement

Un attaquant capable d’exécuter du code arbitraire pendant un build peut modifier les artefacts produits. Si le système de fichiers est accessible en écriture sans restriction, les binaires compilés, les images de conteneurs ou les manifestes de déploiement peuvent être altérés après l’étape de build légitime mais avant que l’artefact ne soit poussé. C’est l’essence de nombreuses attaques de la chaîne d’approvisionnement — le code source paraît sain, mais l’artefact livré est empoisonné.

Mouvement latéral

Les environnements de build qui partagent un réseau avec d’autres infrastructures (bases de données, API internes, services de métadonnées cloud) offrent à un attaquant un point de pivot. Un job de build compromis peut scanner les réseaux internes, accéder aux points de terminaison de métadonnées d’instance cloud (comme 169.254.169.254) et passer d’un contexte CI/CD à un accès plus large à l’infrastructure.

Restrictions réseau

Le contrôle le plus efficace que vous puissiez mettre en place est de restreindre l’accès réseau sortant des environnements de build. Les builds doivent récupérer des dépendances et pousser des artefacts — mais ils ont rarement besoin d’un accès Internet illimité.

NetworkPolicy Kubernetes pour les pods runner

Si vous exécutez vos runners CI/CD sur Kubernetes (par exemple avec Actions Runner Controller ou l’exécuteur Kubernetes de GitLab), les ressources NetworkPolicy vous offrent un contrôle fin de l’accès réseau au niveau du pod. Une politique bien conçue refuse tout le trafic sortant par défaut, puis n’autorise que les points de terminaison spécifiques dont le build a besoin.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: ci-runner-netpol
  namespace: ci-runners
spec:
  podSelector:
    matchLabels:
      app: ci-runner
  policyTypes:
    - Egress
  egress:
    # Allow DNS resolution
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
    # Allow access to container registry
    - to:
        - ipBlock:
            cidr: 10.0.50.0/24
      ports:
        - protocol: TCP
          port: 443
    # Allow access to artifact storage
    - to:
        - ipBlock:
            cidr: 10.0.60.0/24
      ports:
        - protocol: TCP
          port: 443
    # Deny everything else by omission

Cette politique autorise les pods runner à résoudre le DNS, à joindre le registre de conteneurs et à accéder au stockage d’artefacts — rien d’autre. Toute autre connexion sortante est rejetée. Si vous utilisez un plugin CNI qui prend en charge NetworkPolicy (Calico, Cilium ou Weave Net), elle prend effet immédiatement dès son application.

Pour un contrôle plus granulaire, la ressource CiliumNetworkPolicy de Cilium prend en charge des règles basées sur le DNS, vous permettant de spécifier des noms de domaine plutôt que des blocs d’adresses IP :

apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: ci-runner-cilium-policy
  namespace: ci-runners
spec:
  endpointSelector:
    matchLabels:
      app: ci-runner
  egress:
    - toEndpoints:
        - matchLabels:
            io.kubernetes.pod.namespace: kube-system
            k8s-app: kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: ANY
    - toFQDNs:
        - matchName: "ghcr.io"
        - matchName: "registry.npmjs.org"
        - matchName: "pypi.org"
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP

Docker –network=none

Pour les étapes de build basées sur Docker qui ne devraient nécessiter aucun accès réseau (compilation, analyse statique, tests unitaires), vous pouvez supprimer entièrement l’accès réseau en exécutant le conteneur avec --network=none :

docker run --network=none \
  --rm \
  -v "$(pwd)/src:/workspace:ro" \
  -v "$(pwd)/output:/output" \
  my-build-image:latest \
  make build

Avec --network=none, le conteneur ne dispose d’aucune interface réseau — pas même la boucle locale dans certaines configurations. C’est l’isolation réseau la plus forte que vous puissiez obtenir pour une étape de build. L’essentiel est de structurer votre pipeline de sorte que la récupération des dépendances se fasse dans une étape (avec un accès réseau limité) et que le build proprement dit se déroule dans une étape distincte, sans réseau.

Règles de pare-feu pour les runners auto-hébergés

Si vous utilisez des runners auto-hébergés sur des VM plutôt que dans des conteneurs, des règles de pare-feu au niveau de l’hôte offrent une protection équivalente. Sous Linux, des règles iptables ou nftables peuvent restreindre le trafic sortant du compte utilisateur qui exécute les jobs CI :

# Allow DNS
iptables -A OUTPUT -m owner --uid-owner ci-runner -p udp --dport 53 -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 53 -j ACCEPT

# Allow HTTPS to specific registries
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 443 \
  -d registry.example.com -j ACCEPT
iptables -A OUTPUT -m owner --uid-owner ci-runner -p tcp --dport 443 \
  -d ghcr.io -j ACCEPT

# Deny all other outbound traffic from the CI runner
iptables -A OUTPUT -m owner --uid-owner ci-runner -j DROP

Cette approche fonctionne bien lorsque vous exécutez l’agent CI sous un compte utilisateur dédié et que vous devez laisser le système hôte lui-même conserver une connectivité plus large pour l’administration et les mises à jour.

Mise en liste d’autorisation des registres et des API

Quel que soit le mécanisme d’application, le principe est le même : tout refuser en sortie par défaut, puis n’autoriser que ce dont le build a réellement besoin. Une liste d’autorisation typique inclut le registre de paquets (npm, PyPI, Maven Central), le registre de conteneurs (Docker Hub, GHCR, ECR), l’API de la plateforme CI/CD (pour les mises à jour de statut et les téléversements d’artefacts) et éventuellement un proxy ou un miroir que vous contrôlez. Tout le reste devrait être bloqué. Utilisez autant que possible un proxy ou un miroir interne pour les dépendances — cela réduit la liste d’autorisation à un seul point de terminaison et vous offre gratuitement la mise en cache et la journalisation d’audit.

Restrictions du système de fichiers

Les restrictions réseau empêchent les données de quitter l’environnement de build. Les restrictions du système de fichiers empêchent les modifications non autorisées en son sein. Ensemble, elles constituent une solide posture de défense en profondeur.

Système de fichiers racine en lecture seule

Exécuter les conteneurs de build avec un système de fichiers racine en lecture seule empêche tout processus de modifier l’image de base. Cela bloque une catégorie d’attaques où du code malveillant modifie les binaires système, installe des portes dérobées ou altère la configuration des outils de build au niveau du système.

Dans Docker, utilisez l’option --read-only :

docker run --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=512m \
  --tmpfs /workspace/build:rw,size=2g \
  -v "$(pwd)/src:/workspace/src:ro" \
  my-build-image:latest \
  make build

Dans Kubernetes, définissez le security context dans la spécification du pod :

apiVersion: v1
kind: Pod
metadata:
  name: ci-build-pod
spec:
  containers:
    - name: build
      image: my-build-image:latest
      securityContext:
        readOnlyRootFilesystem: true
        runAsNonRoot: true
        allowPrivilegeEscalation: false
      volumeMounts:
        - name: build-tmp
          mountPath: /tmp
        - name: build-output
          mountPath: /workspace/build
        - name: source
          mountPath: /workspace/src
          readOnly: true
  volumes:
    - name: build-tmp
      emptyDir:
        medium: Memory
        sizeLimit: 512Mi
    - name: build-output
      emptyDir:
        sizeLimit: 2Gi
    - name: source
      configMap:
        name: source-code

tmpfs pour les artefacts de build

Lorsque le système de fichiers racine est en lecture seule, les builds ont besoin d’un espace inscriptible pour les fichiers temporaires, les caches et les artefacts produits. Utilisez des montages tmpfs (adossés à la RAM) ou des volumes emptyDir (dans Kubernetes) pour ces chemins. Cela présente l’avantage supplémentaire que tous les artefacts de build sont automatiquement nettoyés à la sortie du conteneur — aucune donnée obsolète ne persiste d’un build à l’autre.

Montez tmpfs avec des options restrictives autant que possible : noexec empêche l’exécution de binaires écrits dans les répertoires temporaires (ce qui bloque un vecteur d’attaque courant), nosuid empêche les attaques par bit SUID, et les limites size empêchent un build incontrôlé d’épuiser la mémoire de l’hôte.

Empêcher les écritures sur les chemins sensibles

Au-delà du système de fichiers racine, certains chemins méritent une protection supplémentaire. Montez le code source en lecture seule pour empêcher le build de modifier ses propres entrées. Assurez-vous que /etc, /usr et /var ne sont pas inscriptibles. Si le build doit écrire dans un répertoire personnel (pour la configuration des outils), fournissez un montage inscriptible dédié plutôt que de rendre tout le répertoire personnel inscriptible. Bloquez l’accès aux sockets Docker, aux tokens de compte de service Kubernetes et aux fichiers d’identifiants cloud en ne les montant tout simplement pas dans les conteneurs de build.

Builds hermétiques

La référence absolue en matière de sécurité des environnements de build est le build hermétique : un build qui n’a aucun accès réseau et n’utilise que des entrées explicitement déclarées et pré-récupérées. Les builds hermétiques éliminent des catégories entières d’attaques de la chaîne d’approvisionnement, car le processus de build ne peut pas télécharger de code qui n’a pas été explicitement spécifié et vérifié.

Le schéma du build hermétique

Un pipeline de build hermétique comporte généralement deux phases. Dans la première phase (la phase de résolution/récupération), les dépendances sont téléchargées depuis des sources approuvées, leurs sommes de contrôle sont vérifiées par rapport à un lockfile, puis elles sont stockées dans un cache local ou un répertoire vendored. Cette phase nécessite un accès réseau limité. Dans la seconde phase (la phase de build), la compilation ou l’assemblage proprement dit se déroule sans aucun accès réseau. Toutes les entrées proviennent du système de fichiers local — le code source et les dépendances pré-récupérées.

# Phase 1: Fetch dependencies (limited network)
docker run --network=ci-restricted \
  -v "$(pwd):/workspace" \
  my-build-image:latest \
  sh -c "cd /workspace && npm ci --ignore-scripts"

# Phase 2: Build (no network)
docker run --network=none \
  --read-only \
  --tmpfs /tmp:rw,noexec,size=512m \
  -v "$(pwd):/workspace:ro" \
  -v "$(pwd)/dist:/dist" \
  my-build-image:latest \
  sh -c "cd /workspace && npm run build && cp -r build/* /dist/"

Bazel et les builds hermétiques

Bazel est conçu autour de l’hermétisme. Avec --sandbox_default_allow_network=false, Bazel bloque l’accès réseau pendant les actions de build par défaut. Les dépendances sont déclarées dans les fichiers WORKSPACE ou MODULE.bazel avec des hachages SHA-256 explicites, et Bazel les récupère dans une phase distincte avant le début du build. Si une dépendance ne correspond pas au hachage déclaré, le build échoue.

# In .bazelrc
build --sandbox_default_allow_network=false
build --incompatible_strict_action_env
fetch --repository_cache=/shared/bazel-cache/repos

Cela rend les builds Bazel reproductibles et résistants aux attaques par confusion de dépendances. Chaque entrée est adressée par son contenu et vérifiée.

Nix et les builds reproductibles

Nix adopte une approche similaire. Chaque dérivation de build spécifie ses entrées par hachage de contenu, et le sandbox de build de Nix bloque l’accès réseau par défaut. La commande nix-build récupère toutes les sources dans le store Nix (en vérifiant les hachages), puis exécute le build dans un environnement isolé sans réseau et avec un système de fichiers minimal. Cela garantit que les builds sont reproductibles — les mêmes entrées produisent toujours la même sortie.

Mise en œuvre pratique

Voyons comment mettre en œuvre ces restrictions dans des plateformes CI/CD spécifiques.

GitHub Actions avec Actions Runner Controller (ARC) + NetworkPolicy

Si vous utilisez Actions Runner Controller pour exécuter GitHub Actions sur Kubernetes, vous pouvez appliquer des NetworkPolicies directement aux pods runner. ARC crée des pods avec des labels prévisibles, ce qui les rend faciles à cibler avec des politiques.

apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: secure-runner
  namespace: ci-runners
spec:
  replicas: 3
  template:
    metadata:
      labels:
        app: ci-runner
        security-tier: restricted
    spec:
      containers:
        - name: runner
          securityContext:
            readOnlyRootFilesystem: true
            runAsNonRoot: true
            allowPrivilegeEscalation: false
            capabilities:
              drop:
                - ALL
          volumeMounts:
            - name: work
              mountPath: /runner/_work
            - name: tmp
              mountPath: /tmp
      volumes:
        - name: work
          emptyDir:
            sizeLimit: 10Gi
        - name: tmp
          emptyDir:
            medium: Memory
            sizeLimit: 1Gi
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: secure-runner-netpol
  namespace: ci-runners
spec:
  podSelector:
    matchLabels:
      app: ci-runner
  policyTypes:
    - Egress
    - Ingress
  ingress: []
  egress:
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
      ports:
        - protocol: TCP
          port: 443

Cette configuration refuse tout le trafic entrant (les runners ne devraient pas accepter de connexions entrantes) et limite le trafic sortant au DNS et au HTTPS. En production, remplacez le CIDR 0.0.0.0/0 par des plages d’adresses IP spécifiques pour l’API de GitHub, votre registre de conteneurs et votre stockage d’artefacts.

GitLab CI avec configuration du runner

L’exécuteur Kubernetes de GitLab prend en charge la configuration du security context dans le fichier config.toml du runner. Vous pouvez définir directement le système de fichiers en lecture seule et d’autres restrictions :

# config.toml for GitLab Runner (Kubernetes executor)
[[runners]]
  name = "secure-k8s-runner"
  executor = "kubernetes"
  [runners.kubernetes]
    namespace = "ci-runners"
    image = "alpine:latest"
    privileged = false
    allow_privilege_escalation = false
    [runners.kubernetes.pod_security_context]
      run_as_non_root = true
      run_as_user = 1000
    [runners.kubernetes.build_container_security_context]
      read_only_root_filesystem = true
      allow_privilege_escalation = false
      [runners.kubernetes.build_container_security_context.capabilities]
        drop = ["ALL"]
    [runners.kubernetes.volumes]
      [[runners.kubernetes.volumes.empty_dir]]
        name = "build-tmp"
        mount_path = "/tmp"
        medium = "Memory"
        size_limit = "512Mi"
      [[runners.kubernetes.volumes.empty_dir]]
        name = "build-workspace"
        mount_path = "/builds"
        size_limit = "5Gi"

Combinez cela avec une NetworkPolicy appliquée au namespace ci-runners et vous disposez à la fois de restrictions sur le système de fichiers et sur le réseau.

Restrictions Docker-in-Docker

Docker-in-Docker (DinD) est couramment utilisé pour construire des images de conteneurs en CI. C’est aussi l’un des schémas les plus risqués, car il nécessite généralement le mode privilégié. Si vous devez utiliser DinD, appliquez ces restrictions :

# Use rootless DinD instead of privileged mode
services:
  dind:
    image: docker:24-dind-rootless
    environment:
      - DOCKER_TLS_CERTDIR=/certs
    volumes:
      - dind-certs:/certs/client
      - dind-data:/var/lib/docker

# When running builds inside DinD, pass network and filesystem restrictions
docker --host tcp://dind:2376 --tlsverify \
  run --network=none --read-only \
  --tmpfs /tmp:rw,noexec,size=256m \
  --security-opt=no-new-privileges \
  my-build-image:latest make build

Mieux encore, remplacez DinD par des outils qui n’ont pas du tout besoin d’un démon Docker. kaniko, buildah et ko peuvent construire des images de conteneurs sans accès privilégié, et ils fonctionnent bien avec des systèmes de fichiers en lecture seule et des réseaux restreints.

Surveillance et audit

Les restrictions ne sont utiles que si vous savez quand elles sont testées ou contournées. La surveillance complète le tableau de la sécurité.

Détecter les connexions réseau inattendues

Utilisez Hubble de Cilium, les flow logs de Calico ou Falco pour détecter les connexions réseau que votre politique aurait dû bloquer (ou les connexions vers des destinations inhabituelles sur des ports autorisés). Configurez des alertes pour toute requête DNS vers des domaines absents de votre liste d’autorisation, les connexions sortantes vers des ports non standard, les connexions vers des plages d’adresses IP connues comme malveillantes, et tout trafic sortant provenant de pods censés être en --network=none.

# Falco rule: detect unexpected outbound connections from CI runners
- rule: CI Runner Unexpected Outbound Connection
  desc: Detect network connections from CI runner pods to non-approved destinations
  condition: >
    evt.type in (connect, sendto) and
    container and
    k8s.ns.name = "ci-runners" and
    not (fd.sip in (approved_registry_ips) or fd.sport = 53)
  output: >
    Unexpected outbound connection from CI runner
    (command=%proc.cmdline connection=%fd.name container=%container.name
    pod=%k8s.pod.name namespace=%k8s.ns.name)
  priority: WARNING
  tags: [network, ci-cd, supply-chain]

Auditer l’accès au système de fichiers

Surveillez les écritures sur le système de fichiers dans les conteneurs de build pour détecter les modifications inattendues. L’outil auditd de Linux peut surveiller des chemins spécifiques, et Falco peut détecter les écritures vers des emplacements sensibles. Les chemins clés à surveiller incluent /etc et /usr (qui ne devraient jamais être écrits pendant un build), le chemin du socket Docker, les chemins des tokens de compte de service Kubernetes, et tout chemin contenant des identifiants ou des clés de signature.

Si vous utilisez des systèmes de fichiers racine en lecture seule, toute tentative d’écriture vers un chemin protégé génère une erreur — journalisez ces erreurs et déclenchez des alertes dessus. Elles indiquent soit un build mal configuré, soit une attaque potentielle.

Compromis et expérience développeur

Des restrictions réseau et système de fichiers strictes créent inévitablement des frictions. Comprendre et gérer ces compromis est essentiel à une adoption réussie.

Vitesse de build

Les builds hermétiques exigent que toutes les dépendances soient pré-récupérées, ce qui ajoute une étape au pipeline. Cependant, cela signifie aussi que les dépendances peuvent être mises en cache de manière agressive. En pratique, de nombreuses équipes constatent que les builds hermétiques sont en réalité plus rapides, car le taux de succès du cache est bien plus élevé lorsque la résolution des dépendances est déterministe. Utilisez un cache partagé (un cache distant Bazel, un cache binaire Nix ou un simple cache HTTP pour les dépendances vendored) pour amortir le coût sur l’ensemble des builds.

Expérience développeur

Les développeurs rencontreront des échecs lorsque les builds tenteront d’accéder à des points de terminaison réseau bloqués ou d’écrire vers des chemins en lecture seule. De bons messages d’erreur sont essentiels. Encapsulez vos étapes de build dans des scripts qui interceptent les erreurs de permission et les échecs réseau, puis affichent des messages exploitables expliquant pourquoi l’accès a été bloqué et comment résoudre le problème (généralement en ajoutant une dépendance au lockfile ou en changeant le chemin de sortie).

Envisagez un déploiement progressif : commencez par un mode surveillance (journaliser les violations sans bloquer), puis passez à l’application effective. Cela laisse aux équipes le temps de mettre à jour leurs configurations de build sans casser tous les pipelines d’un coup.

Débogage

Déboguer des échecs de build dans un environnement restreint est plus difficile lorsque vous ne pouvez pas installer d’outils supplémentaires ni joindre des services externes. Proposez un « mode débogage » qui assouplit les restrictions pour une exécution de pipeline spécifique, déclenchée manuellement (jamais pour les exécutions automatisées sur la branche main). Journalisez le fait que le mode débogage a été utilisé et qui l’a déclenché. N’autorisez jamais le mode débogage à contourner les restrictions sur les builds d’artefacts de production.

Tout assembler

Voici un résumé de l’approche en couches pour sécuriser les environnements de build CI/CD :

Couche 1 — Restrictions réseau : Refus par défaut du trafic sortant avec des listes d’autorisation pour les registres et les API. Utilisez NetworkPolicy Kubernetes, --network=none de Docker ou des règles de pare-feu au niveau de l’hôte selon votre infrastructure de runners.

Couche 2 — Restrictions du système de fichiers : Système de fichiers racine en lecture seule, tmpfs pour les chemins inscriptibles avec limites de taille et noexec, code source monté en lecture seule.

Couche 3 — Builds hermétiques : Séparez la résolution des dépendances de la construction. Exécutez la phase de build sans aucun accès réseau et uniquement avec des entrées pré-récupérées et vérifiées par hachage.

Couche 4 — Surveillance : Détectez les violations de politique, les connexions inattendues et les tentatives de modification du système de fichiers, et déclenchez des alertes.

Aucune couche ne suffit à elle seule. Des restrictions réseau sans contrôles sur le système de fichiers laissent toujours possible l’altération des artefacts. Des restrictions du système de fichiers sans contrôles réseau laissent toujours possible l’exfiltration. Des builds hermétiques sans surveillance vous rendent aveugle aux tentatives d’attaque. Les couches se renforcent mutuellement.

Guides connexes

Pour approfondir la sécurisation de votre pipeline CI/CD, consultez ces guides connexes :

Commencez par les restrictions réseau — elles offrent le plus fort impact pour le moindre effort de mise en œuvre. Ajoutez ensuite les restrictions du système de fichiers et progressez vers des builds hermétiques à mesure que la maturité de votre pipeline augmente. Chaque couche que vous ajoutez rend les attaques de la chaîne d’approvisionnement nettement plus difficiles à exécuter.