Sécurité du socket Docker : un montage équivalant à root (et alternatives plus sûres)

Cet article vous a aidé ?

Quelque part dans votre parc, un conteneur a -v /var/run/docker.sock:/var/run/docker.sock parce que « il doit construire des images »
ou « il doit inspecter des conteneurs ». Ça marche. Ça part en production. Plus personne n’y pense.

Jusqu’au jour où un compte de service anodin à l’intérieur de ce conteneur découvre qu’il peut démarrer des conteneurs privilégiés, monter le système de fichiers de l’hôte,
et réécrire votre réalité. Un seul montage de volume. Contrôle au niveau de l’hôte. Ce n’est pas un « peut-être » théorique. C’est une voie d’escalade pratique et reproductible.

Le problème du « seul montage » : pourquoi docker.sock équivaut essentiellement à root

Le démon Docker (dockerd) est un processus privilégié de longue durée sur l’hôte. Il peut :
créer des namespaces, configurer des cgroups, mettre en place le réseau, monter des systèmes de fichiers, et démarrer des conteneurs avec de larges privilèges. C’est son rôle.
Le CLI Docker n’est qu’un client qui envoie des appels API au démon.

/var/run/docker.sock est le socket Unix où cette API vit par défaut. Si un processus peut parler au socket avec
les permissions suffisantes, il peut demander au démon d’effectuer des actions au niveau de l’hôte en son nom. Le démon ne sait pas (ou ne se préoccupe pas) si la requête
vient « d’un administrateur de confiance au terminal » ou « d’une application Node.js à l’intérieur d’un conteneur ». Il reçoit simplement des appels API.

Voilà l’essentiel : quand vous montez le socket Docker de l’hôte dans un conteneur, vous donnez effectivement à ce conteneur la capacité de contrôler
le runtime des conteneurs de l’hôte. C’est plus de pouvoir que ce qu’un conteneur a normalement.

Concrètement, cela signifie qu’un conteneur compromis peut :

  • Démarrer un nouveau conteneur en mode --privileged.
  • Monter le système de fichiers de l’hôte dans ce conteneur (par ex. /:/host).
  • Écrire dans des chemins de l’hôte, modifier des configs, déposer des clés SSH, lire des secrets ou altérer des unités systemd.
  • Configurer le réseau pour renifler ou rediriger le trafic.
  • Tirer et exécuter des images arbitraires comme vecteur d’exécution.

Si cela ressemble à « root », c’est parce que ça l’est, juste avec une interface utilisateur un peu différente.
Le socket n’est pas « une chose Docker ». C’est une capacité équivalente à root.

Première plaisanterie (et on limitera les plaisanteries) : Monter docker.sock dans un conteneur, c’est comme distribuer des clés maîtres—sauf que les clés peuvent aussi construire un nouveau bâtiment.

« Mais on l’utilise seulement pour les builds » n’est pas une atténuation

L’API Docker n’est pas granulare par défaut. Si vous pouvez créer des conteneurs, vous pouvez créer des conteneurs qui montent l’hôte.
Si vous pouvez monter l’hôte, vous pouvez tout faire. L’API ne s’arrête pas poliment à « seulement construire, pas de vilaineries ».

La frontière de sécurité ici n’est pas le conteneur ; c’est le démon. Et le démon est sur l’hôte avec des privilèges d’hôte.
La question devient donc : « Qui est autorisé à demander au démon d’exécuter des actions ? » Si la réponse est « n’importe quel processus dans ce conteneur », vous avez
élargi votre périmètre d’impact pour inclure chaque bogue de l’arbre de dépendances de ce conteneur.

Quelques faits et un peu d’histoire qui expliquent comment on en est arrivé là

Ce ne sont pas des anecdotes pour le plaisir. Ils expliquent pourquoi les schémas de socket Docker sont si courants, pourquoi ils persistent, et pourquoi des alternatives modernes existent.

  1. Docker a commencé comme un outil de commodité pour les développeurs. L’adoption initiale privilégiait « ça marche sur ma machine » plus que la rigueur du contrôle d’accès.
  2. La séparation démon/client a toujours existé. Même le classique docker CLI a toujours été un client distant ; la configuration par défaut est simplement un socket local.
  3. Le groupe docker équivalait historiquement à root. Sur de nombreux systèmes Linux, l’accès à /var/run/docker.sock est accordé par l’appartenance au groupe docker, ce qui permet des actions au niveau root.
  4. Les sockets Unix ont été choisis pour l’ergonomie locale. Ils sont rapides, simples, et évitent d’exposer un port TCP par défaut—mais ils ne résolvent pas l’autorisation.
  5. Docker distant sur TCP a existé tôt, et il a souvent été mal configuré. « Ouvrir le port 2375 sur le monde » est devenu un schéma d’incident récurrent dans les années 2010.
  6. Les outils de build ont évolué parce que le pattern du socket était pénible. BuildKit, les builds rootless et les builders « sans démon » ont gagné en adoption en partie pour éviter de donner aux runners CI le contrôle de l’hôte.
  7. Docker-in-Docker (DinD) est devenu un contournement avec ses propres dangers. Il réduisait le partage du socket hôte mais introduisait des besoins en privilèges, une complexité de stockage et des limites d’isolation imbriquées.
  8. Kubernetes a fait passer le problème à l’échelle. Une fois que vous montez un socket runtime dans des Pods (Docker, containerd, sockets CRI), « un Pod compromis » peut devenir « un nœud compromis ».

La leçon historique : la plupart des expositions de socket ne sont pas malveillantes ; ce sont des commodités accidentelles qui se sont durcies en « pratique standard ».
La production est l’endroit où la « pratique standard » reçoit un audit.

Modèle de menace : ce qu’un attaquant peut faire avec le socket

Supposez que l’attaquant ait une exécution de code dans un conteneur qui a monté le socket docker de l’hôte. Cela peut arriver via :
une RCE dans votre application, une dépendance malveillante, un job CI compromis, ou un endpoint d’administration qui fuit. Et ensuite ?

Chemin d’escalade en étapes simples

L’attaquant peut exécuter le CLI Docker s’il existe dans le conteneur, ou utiliser du HTTP brut sur le socket Unix.
Dans les deux cas, il peut demander :

  • Créer un conteneur avec --privileged (ou un ensemble ciblé de capacités).
  • Monter / de l’hôte dans le conteneur.
  • Chrooter dans le système de fichiers de l’hôte et le modifier.
  • Persister : unit systemd, cron, clés SSH, authorized_keys, ou remplacer des binaires.

Ce n’est pas seulement « root » ; c’est aussi « plan de contrôle »

Même sans monter /, contrôler Docker peut :

  • Arrêter ou redémarrer des services critiques.
  • Lire des variables d’environnement d’autres conteneurs (souvent incluant des tokens).
  • S’attacher à des conteneurs en cours d’exécution et exfiltrer des secrets en mémoire.
  • Créer des pivots réseau en rejoignant des réseaux de conteneurs.
  • Tirer des images depuis des registries en utilisant les identifiants de l’hôte.

« Mais le conteneur n’est pas privilégié » est une mauvaise compréhension

Le conteneur lui-même peut être non privilégié. Peu importe. Le démon est privilégié. Vous demandez à un processus hôte privilégié d’exécuter un travail d’hôte privilégié.
La frontière n’est pas appliquée par le démon à moins que vous ne la mettiez en place.

Voici un cadrage utile en opérationnel : docker.sock est une interface d’administration. Montez-la uniquement là où vous accorderiez aussi un shell root sur l’hôte.
Si cette phrase vous met mal à l’aise, tant mieux—maintenant on peut corriger cela.

Playbook de diagnostic rapide

Lorsque vous suspectez une exposition du socket docker (ou que vous répondez à un moment « pourquoi ce conteneur peut faire ça ? »), la rapidité compte. Vérifiez dans cet ordre :

Première étape : le socket est-il monté ou joignable ?

  • Inspectez les montages des conteneurs pour /var/run/docker.sock.
  • Vérifiez le système de fichiers pour le chemin du socket et ses permissions.
  • Confirmez si le processus peut lui parler (un appel API qui échoue est toujours une donnée).

Deuxième étape : qui peut y accéder ?

  • Propriété et groupe du socket ; vérifiez qui est dans le groupe docker.
  • À l’intérieur du conteneur : le processus tourne-t-il en root ? Est-il dans un groupe mappé au socket ?
  • Y a-t-il des sidecars ou agents avec des permissions plus larges ?

Troisième étape : que peut-il faire maintenant ?

  • Essayez un appel en lecture seule (docker ps) et un appel en écriture (docker run) de manière contrôlée.
  • Vérifiez la configuration du démon : TLS ? plugins d’autorisation ? démon rootless ? remappage des namespaces utilisateur ?
  • Cherchez les runners CI, outils de déploiement, ou conteneurs « monitoring » qui portent discrètement le socket.

Quatrième étape : rayon d’impact et contrôles de persistance

  • Recherchez des conteneurs démarrés avec --privileged, des montages d’hôte, ou des namespaces host PID/network.
  • Auditez les événements récents de démarrage de conteneurs ; vérifiez les images inconnues.
  • Vérifiez l’hôte pour de nouvelles unités systemd, cron jobs, clés SSH, ou binaires modifiés.

Ce playbook est volontairement direct. L’objectif est d’identifier si vous avez affaire à « normal mais risqué » ou à « exploitation active ».
Vous affinerez ensuite. Pour l’instant, vous voulez des certitudes.

Tâches pratiques : commandes, sorties et décisions

Voici des tâches réelles que vous pouvez exécuter sur un hôte Linux ou dans un conteneur (là où c’est approprié). Chacune inclut :
la commande, une sortie d’exemple, ce que la sortie signifie, et la décision que vous en tirez.

Tâche 1 : Vérifier si le socket Docker existe et comment il est protégé

cr0x@server:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jan  3 09:12 /var/run/docker.sock

Sens : C’est un socket Unix (s) possédé par root et groupe docker, écrivable par le groupe.

Décision : Considérez l’appartenance au groupe docker comme un accès privilégié. Si des conteneurs peuvent se mapper dans ce groupe, vous avez un problème de frontière de privilèges.

Tâche 2 : Lister les membres du groupe docker (côté hôte)

cr0x@server:~$ getent group docker
docker:x:998:jenkins,deploy,alice

Sens : Ces utilisateurs peuvent probablement contrôler Docker sur l’hôte.

Décision : Réduisez cette liste agressivement. Si « deploy » est un compte partagé, vous partagez essentiellement root.

Tâche 3 : Confirmer avec quel démon votre CLI dialogue

cr0x@server:~$ docker context ls
NAME        DESCRIPTION                               DOCKER ENDPOINT               ERROR
default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock

Sens : Votre contexte par défaut est le socket local possédé par root.

Décision : Si vous attendiez un builder distant ou un démon rootless, vous ne l’utilisez pas. Corrigez le workflow, pas le récit.

Tâche 4 : Trouver les conteneurs qui montent le socket docker

cr0x@server:~$ docker ps --format '{{.ID}} {{.Names}}' | while read id name; do docker inspect -f '{{.Name}} {{range .Mounts}}{{println .Source "->" .Destination}}{{end}}' "$id"; done | grep -F '/var/run/docker.sock'
/ci-runner /var/run/docker.sock -> /var/run/docker.sock
/portainer /var/run/docker.sock -> /var/run/docker.sock

Sens : Deux conteneurs ont le contrôle Docker de l’hôte. Le runner CI est attendu ; Portainer peut être acceptable, mais les deux sont des cibles à haute valeur.

Décision : Pour chaque conteneur : justifiez, scopez, ou supprimez. « On l’a toujours fait » n’est pas une justification.

Tâche 5 : À l’intérieur d’un conteneur suspect, tester si le socket est utilisable

cr0x@server:~$ docker exec -it ci-runner sh -lc 'id && ls -l /var/run/docker.sock'
uid=1000 gid=1000 groups=1000
srw-rw---- 1 root docker 0 Jan  3 09:12 /var/run/docker.sock

Sens : Le processus n’est pas dans le groupe docker. Cela peut bloquer l’accès—sauf si le conteneur s’exécute parfois en root, ou si l’ID de groupe est mappé différemment.

Décision : Essayez ensuite un appel API inoffensif. Ne supposez pas que vous êtes en sécurité parce que id semble non privilégié.

Tâche 6 : Tenter un appel Docker plutôt en lecture seule depuis l’intérieur

cr0x@server:~$ docker exec -it ci-runner sh -lc 'docker ps'
Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Get "http://%2Fvar%2Frun%2Fdocker.sock/v1.45/containers/json": dial unix /var/run/docker.sock: connect: permission denied

Sens : Le socket est monté, mais l’utilisateur courant ne peut pas y accéder.

Décision : C’est toujours un risque : si le conteneur peut devenir root (mauvaise config, SUID, exploit), la partie est gagnée. Vérifiez aussi si le conteneur tourne parfois en root durant des jobs.

Tâche 7 : Vérifier si le conteneur tourne en root (côté hôte)

cr0x@server:~$ docker inspect -f '{{.Name}} user={{.Config.User}}' ci-runner
/ci-runner user=

Sens : L’utilisateur vide signifie souvent root par défaut à l’intérieur du conteneur.

Décision : Si ce conteneur a aussi le socket, considérez-le comme « root sur l’hôte, en attente ». Corrigez maintenant : exécutez-le en non-root et retirez le socket, ou isolez le démon.

Tâche 8 : Prouver l’escalade (dans un laboratoire contrôlé), en montant la racine de l’hôte

cr0x@server:~$ docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock alpine sh -lc 'apk add --no-cache docker-cli >/dev/null && docker run --rm -it --privileged -v /:/host alpine sh -lc "ls -l /host/etc/shadow | head -n 1"'
-rw-r-----    1 root     shadow        1251 Jan  3 08:59 /host/etc/shadow

Sens : Un conteneur ayant accès au socket a créé un conteneur privilégié pouvant lire des fichiers sensibles de l’hôte.

Décision : Si vous pouvez faire cela, un attaquant le peut aussi. Éliminez les montages de socket dans les charges de travail générales.

Tâche 9 : Identifier les conteneurs privilégiés et les montages d’hôte

cr0x@server:~$ docker ps -q | xargs -r docker inspect -f '{{.Name}} privileged={{.HostConfig.Privileged}} mounts={{range .Mounts}}{{.Source}}:{{.Destination}},{{end}}'
/ci-runner privileged=false mounts=/var/run/docker.sock:/var/run/docker.sock,
/node-exporter privileged=false mounts=/proc:/host/proc,/sys:/host/sys,
/debug-shell privileged=true mounts=/:/host,

Sens : /debug-shell est privilégié et monte la racine de l’hôte. C’est un levier d’urgence—acceptable si contrôlé, catastrophique s’il est oublié.

Décision : Supprimez ou verrouillez les conteneurs « debug ». Implementez des politiques empêchant les combinaisons privilégiées + montages d’hôte hors des workflows de secours.

Tâche 10 : Vérifier la configuration d’écoute du démon (éviter l’exposition TCP accidentelle)

cr0x@server:~$ ps aux | grep -E 'dockerd|docker daemon' | grep -v grep
root      1321  0.3  1.4 1332456 118320 ?      Ssl  08:58   0:06 /usr/bin/dockerd -H fd://

Sens : Il utilise l’activation de socket systemd (-H fd://), pas l’écoute explicite sur TCP.

Décision : Bien. Si vous voyez -H tcp://0.0.0.0:2375 sans TLS, considérez cela comme un incident.

Tâche 11 : Vérifier si l’API Docker est joignable sur le réseau

cr0x@server:~$ ss -lntp | grep -E '(:2375|:2376)\b' || true

Sens : Pas d’écouteurs sur les ports TCP Docker courants.

Décision : Gardez-le ainsi sauf si vous avez TLS et une stratégie d’autorisation. « On le firewallera plus tard » est la voie vers un appel du lundi matin.

Tâche 12 : Interroger Docker via le socket brut (utile quand le CLI docker n’est pas disponible)

cr0x@server:~$ curl --unix-socket /var/run/docker.sock http://localhost/version
{"Platform":{"Name":"Docker Engine - Community"},"Components":[{"Name":"Engine","Version":"26.0.0","Details":{"ApiVersion":"1.45","GitCommit":"...","GoVersion":"...","Os":"linux","Arch":"amd64","KernelVersion":"6.5.0"}}],"Version":"26.0.0","ApiVersion":"1.45","MinAPIVersion":"1.24","GitCommit":"...","GoVersion":"...","Os":"linux","Arch":"amd64","KernelVersion":"6.5.0","BuildTime":"..."}

Sens : Si cela réussit à l’intérieur d’un conteneur, ce conteneur a un accès admin Docker même s’il ne contient pas le CLI docker.

Décision : Ne vous laissez pas tromper par « on n’a pas installé docker ». L’accès dépend du socket, pas du binaire.

Tâche 13 : Auditer l’unité systemd de Docker pour les flags (côté hôte)

cr0x@server:~$ systemctl cat docker | sed -n '1,120p'
# /lib/systemd/system/docker.service
[Service]
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Sens : Pas d’écouteur TCP non sécurisé explicitement configuré ici.

Décision : Si vous devez exposer TCP, faites-le explicitement avec TLS et restreignez les clients ; sinon gardez-le local.

Tâche 14 : Identifier quelles images et quels conteneurs sont les plus susceptibles d’être exploités

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}' | sed -n '1,15p'
NAMES         IMAGE                 STATUS
ci-runner     company/runner:latest Up 3 hours
portainer     portainer/portainer   Up 7 days
api           company/api:2026.01   Up 2 days

Sens : Les conteneurs « Runner » et « UI admin » sont de haute valeur, fréquemment exposés à Internet, et souvent complexes.

Décision : Priorisez la suppression des montages de socket pour tout ce qui analyse des entrées non fiables ou exécute des jobs tiers.

Tâche 15 : Si vous utilisez Docker rootless, confirmez-le

cr0x@server:~$ docker info --format 'rootless={{.SecurityOptions}}'
rootless=[name=seccomp, name=rootless]

Sens : Le démon tourne en mode rootless (ou au moins rapporte l’option de sécurité rootless).

Décision : Rootless réduit le risque de prise d’hôte, mais ne l’efface pas. Évaluez ce que le démon rootless peut accéder (chemins de stockage, identifiants, réseau).

Tâche 16 : Repérer « docker.sock sous un autre nom » (sockets containerd / CRI)

cr0x@server:~$ ls -l /run/containerd/containerd.sock 2>/dev/null || true
srw-rw---- 1 root root 0 Jan  3 08:58 /run/containerd/containerd.sock

Sens : Le socket containerd existe. Le monter (ou monter des sockets CRI) dans un conteneur peut de même exposer le contrôle au niveau du nœud, selon ce qui écoute et comment c’est protégé.

Décision : Ne jouez pas au whack-a-mole avec les noms de fichiers. Traitez les sockets de contrôle du runtime comme des interfaces privilégiées de manière générale.

Trois mini-récits d’entreprise issus du terrain

Mini-récit 1 : L’incident causé par une mauvaise hypothèse

Une entreprise de taille moyenne avait un conteneur « utilitaire » tournant sur chaque hôte de build. Il collectait les logs de build, téléversait des artefacts, et—par commodité—
avait le socket Docker monté pour pouvoir étiqueter les conteneurs et nettoyer les vieilles images.

Quelqu’un a supposé que le conteneur était à faible risque parce qu’il « ne parlait qu’à des systèmes internes ». Le conteneur tournait aussi un petit endpoint HTTP pour les checks de santé
et les métriques. L’endpoint acceptait quelques paramètres et les écrivait dans des logs. Un petit bug d’injection s’est glissé lors d’un refactor pressé.

Un scanner s’ennuyant a trouvé l’endpoint. En quelques minutes, l’attaquant avait une exécution de code à distance dans le conteneur utilitaire. L’attaquant n’a pas eu besoin d’un exploit noyau.
Il a simplement utilisé le socket pour lancer un conteneur privilégié avec le système de fichiers de l’hôte monté, puis a déposé un binaire backdoor dans un chemin utilisé par un cron.

La détection fut embarrassante : pics CPU, redémarrages de conteneurs, et une vague soudaine de « pourquoi cet hôte fait des connexions sortantes ? » L’équipe a d’abord
cherché des problèmes réseau et des timeouts de registry. La cause racine s’est avérée être le truc le plus ancien du livre Docker : un conteneur contrôlant Docker.

La correction n’a pas été « patcher l’injection ». Ils ont retiré les montages de socket de tout ce qui n’était pas un composant de build strictement contrôlé, et ont déplacé le nettoyage vers
un timer systemd côté hôte sous propriété admin explicite. L’équipe de sécurité n’était pas ravie, mais au moins ils avaient un périmètre d’impact connu.

Mini-récit 2 : L’optimisation qui s’est retournée contre eux

Une autre organisation avait une forte charge CI. Les builds étaient lents, surtout parce que tirer les images de base et les couches surchargeait le registry et le cache était froid.
Quelqu’un a eu l’idée d’optimisation suivante : exécuter un conteneur « service de cache de build » partagé sur chaque nœud, monter docker.sock, et lui faire pré-tirer des images et réchauffer le cache.
Ça a réduit le temps de build. Les gens ont applaudi. La demande de changement est passée parce que c’était « juste des performances ».

Le revers est arrivé discrètement. Le service de cache avait besoin de larges permissions pour gérer des images entre projets. Les jobs CI ont commencé à dépendre indirectement de lui, et bientôt
le service de cache est devenu un plan de contrôle de facto pour les hôtes de build. C’est devenu aussi une dépendance. Quand il a planté, les builds se sont bloqués.

Puis la revue sécurité est arrivée. Les reviewers ont posé la question ennuyeuse : « Si un job CI est compromis, peut-il atteindre le conteneur de cache ? »
La réponse était oui—même réseau, même nœud, variables d’environnement partagées, et de multiples opportunités de mouvement latéral.

Le montage du socket signifiait que toute compromission du service de cache était une compromission de l’hôte. Mais c’était pire : le service de cache tirait des images avec les identifiants
registry de l’hôte. Si vous pouvez tirer avec ces identifiants, vous pouvez aussi les exfiltrer de plusieurs façons. L’optimisation n’a pas seulement augmenté la surface d’attaque ;
elle l’a centralisée.

Ils l’ont annulée et l’ont remplacée par un builder BuildKit distant avec des credentials à portée limitée, plus un caching côté registry. Les builds sont devenus légèrement plus lents que le
pic de la « solution maligne », mais le risque s’est effondré. La performance est une fonctionnalité ; la survivabilité est le produit.

Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise

Une grande entreprise avait une politique simple : pas de montages docker.sock dans les charges applicatives. Les exceptions nécessitaient un court formulaire et un plan de contrôles compensatoires.
Les gens râlaient. Bien sûr. La politique était appliquée par des lignes directrices de revue d’images et des audits périodiques des conteneurs en cours d’exécution.

Un jour, une nouvelle équipe a déployé un conteneur agent fournisseur « pour aider à l’observabilité ». Le quickstart du fournisseur utilisait un montage de socket pour découvrir les conteneurs.
L’équipe a demandé une exception, comme prévu. La sécurité a demandé une alternative en lecture seule et une justification argumentée. Le fournisseur a dit « c’est sûr ».
La sécurité a dit « montrez-nous ».

Lors des tests, un ingénieur a démontré que l’agent pouvait créer des conteneurs et monter l’hôte. Cela a mis fin à la discussion « c’est sûr ».
L’équipe a plutôt déployé l’agent avec une source de données réduite (métriques cgroup et procfs) et un collecteur côté hôte séparé pour les métadonnées des conteneurs.

Un mois plus tard, une vulnérabilité dans cet agent fournisseur a fait la une. Ils l’ont patchée selon le calendrier, mais la vulnérabilité ne s’est jamais transformée en prise d’hôte
chez eux parce que l’agent n’avait jamais eu le contrôle de l’hôte. Le résultat n’a pas été spectaculaire. C’est le point. Les contrôles ennuyeux sont souvent ceux qui gardent votre rapport d’incident court.

Alternatives plus sûres (avec compromis, pas de contes de fées)

Le bon remplacement dépend de la raison pour laquelle vous avez monté le socket à l’origine. « Nous avons besoin de Docker dans un conteneur » n’est pas une exigence ; c’est un symptôme.
Ci-dessous des schémas qui fonctionnent dans des systèmes réels, y compris ce qu’ils vous coûtent.

1) Ne le faites pas : déplacer les opérations Docker vers l’hôte (timers systemd, scripts contrôlés)

Si la seule raison d’accéder au socket est le nettoyage, le pruning d’images, la rotation des logs, ou la collecte de métadonnées—faites-le sur l’hôte.
Utilisez un timer systemd ou un cron avec une propriété explicite et un audit.

Compromis : Un peu plus de gestion d’hôte. Mais vous retrouvez des frontières de privilèges claires et réduisez le nombre de processus pouvant piloter le démon.

2) Démon Docker rootless (ou BuildKit rootless) pour les workloads non fiables

Docker rootless exécute le démon sans privilèges root. Cela change le rayon d’impact : l’accès à l’API Docker n’est plus automatiquement équivalent à root de l’hôte.
C’est toujours puissant—juste contraint par les permissions de l’utilisateur.

Compromis : Certaines fonctionnalités réseau, conteneurs privilégiés et capacités noyau ne fonctionneront pas. La performance et la compatibilité peuvent différer.
Opérationnellement, vous gérez maintenant des démons par utilisateur, des chemins de stockage et des emplacements de socket distincts.

3) Builders distants : BuildKit comme service (domaine de sécurité séparé)

Un cas d’usage fréquent « j’ai besoin du docker.sock » est la construction d’images en CI. Le schéma plus sûr : le CI parle à un builder distant isolé, durci et scoppé.
Vos workloads applicatifs ne voient jamais un socket runtime. Vos runners CI n’obtiennent pas de droits admin sur l’hôte.

Compromis : Nécessite connectivité réseau et gestion des credentials. Le débogage des builds peut passer de « ssh sur l’hôte » à « inspecter les logs du builder ».
Ça vaut le coup.

4) Builds sans démon : approches type Kaniko

Les builders sans démon peuvent construire des images de conteneurs sans nécessiter les privilèges du démon Docker. Cela supprime l’incitation à monter docker.sock dans les jobs CI.

Compromis : Toutes les fonctionnalités Dockerfile ne se comportent pas identiquement. Le caching des couches et la performance peuvent varier. Vous échangez le privilège du démon contre des particularités de l’outil de build.

5) Docker-in-Docker (DinD) : mieux que le montage du socket, mais toujours tranchant

DinD exécute un démon Docker à l’intérieur d’un conteneur. Les jobs CI parlent à ce démon interne, pas au démon hôte. Cela peut réduire le risque de prise d’hôte par du code de job CI.
Mais en pratique DinD nécessite souvent --privileged pour fonctionner correctement, et l’isolation du stockage devient rapidement compliquée.

Compromis : Risque de conteneur privilégié, complexité des cgroups imbriqués, coûts de performance, et tendance à devenir une infrastructure « temporaire » qui ne disparaît jamais.

6) Plugins d’autorisation et enforcement de politiques (quand vous devez vraiment exposer le socket)

S’il existe un besoin légitime d’accès contrôlé à l’API Docker, vous pouvez ajouter des couches de politique :
plugins d’autorisation, certificats client TLS pour l’accès distant, et contrôles stricts sur les endpoints API autorisés.

Compromis : Vous devez maintenant faire tourner et maintenir une couche d’authentification sur votre runtime de conteneurs. Si elle échoue ouverte, vous perdez la protection. Si elle échoue fermée, vous êtes réveillé à 3h du matin.
C’est quand même mieux que d’espérer.

7) Remplacer « introspection Docker » par des signaux en lecture seule

Les agents de monitoring demandent souvent le socket pour énumérer conteneurs et labels. Alternatives :
lire /proc, cgroups, node exporters, ou drivers de logs ; accepter moins de métadonnées ; ou exécuter un collecteur côté hôte de confiance qui exporte des métriques assainies.

Compromis : Fidélité réduite. Mais pour la plupart des usages de monitoring, « moins de métadonnées » vaut mieux que « root par accident ».

8) Si vous êtes sur Kubernetes : faites appliquer via admission controls et remplaçants PSP

Si vous exécutez Kubernetes, ne comptez plus sur les revues manuelles. Ajoutez des politiques pour refuser les montages de sockets runtime et les hostPath sauf autorisation explicite.
Bloquez aussi les pods privilégiés et hostPID/hostNetwork sauf pour des composants système connus.

Compromis : Vous allez casser le « pod debug rapide » de quelqu’un. Très bien. Fournissez un namespace break-glass avec un RBAC strict et des logs d’audit.

Une citation pour rester honnête

L’espoir n’est pas une stratégie. — General Gordon R. Sullivan

Vous n’avez pas à être paranoïaque. Vous devez simplement être réaliste quant à ce que vous avez monté.

Deuxième plaisanterie (la dernière, promis) : Le socket Docker est comme une règle de firewall « temporaire »—tout le monde s’en souvient juste après l’incident.

Erreurs courantes : symptômes → cause racine → correctif

1) Symptom : « Notre conteneur d’application peut démarrer d’autres conteneurs »

Cause racine : Le socket docker de l’hôte est monté dans le conteneur d’application, ou le conteneur a accès aux permissions du groupe docker.

Correctif : Retirez le montage du socket ; repensez le workflow (builder distant, automatisation côté hôte). Assurez-vous que le conteneur tourne en non-root et n’a aucun accès au groupe docker.

2) Symptom : « Nous avons supprimé le CLI docker mais le conteneur contrôle toujours Docker »

Cause racine : L’API est accessible via le socket ; des outils comme curl peuvent y parler directement.

Correctif : Retirez l’accès au socket. La sécurité n’est pas « l’absence d’un binaire client ».

3) Symptom : « Seul le CI a docker.sock, donc tout va bien »

Cause racine : Le CI exécute du code non fiable (PRs, dépendances, scripts de build). C’est précisément l’endroit où vous ne voulez pas d’admin hôte.

Correctif : Utilisez des builders distants ou des builds sans démon. Si vous devez utiliser un démon, isolez-le par job et restreignez les credentials.

4) Symptom : « Des conteneurs tournent en privilégié et personne ne sait pourquoi »

Cause racine : Un conteneur avec accès au socket les a démarrés, ou un workflow « debug » a été normalisé en production.

Correctif : Auditez les sources de démarrage des conteneurs ; retirez les montages de socket ; appliquez des politiques interdisant les conteneurs privilégiés en dehors des namespaces de secours contrôlés.

5) Symptom : « Nous avons exposé Docker sur TCP par commodité »

Cause racine : dockerd écoute sur tcp://0.0.0.0:2375 (souvent sans TLS), ou les règles de firewall sont trop permissives.

Correctif : Désactivez l’écoute TCP ; si un accès distant est requis, utilisez TLS sur 2376 avec certificats clients et ACL réseau strictes, plus des contrôles d’autorisation.

6) Symptom : « L’agent de monitoring exige docker.sock, le fournisseur dit que c’est requis »

Cause racine : Par défaut, commodité du fournisseur. Ils veulent des métadonnées complètes ; ils demandent l’interface la plus simple.

Correctif : Utilisez un collecteur côté hôte ; fournissez des sources métriques en lecture seule ; négociez un périmètre réduit ; refusez le montage de socket dans les agents à usage général.

7) Symptom : « Nous avons essayé rootless, mais les builds ont cassé »

Cause racine : Certaines fonctionnalités Dockerfile et opérations privilégiées attendent un comportement rootful, ou votre pipeline suppose un réseau hôte direct.

Correctif : Utilisez un builder BuildKit distant avec privilèges contrôlés ; séparez « build » et « run » ; ajustez les Dockerfiles ; acceptez que certaines hypothèses héritées doivent disparaître.

8) Symptom : « Nous avons utilisé DinD et maintenant l’utilisation disque est énorme »

Cause racine : Stockage du démon imbriqué overlay2 dans le système de fichiers du conteneur ; les caches s’accumulent par runner ; le pruning n’impacte pas l’hôte comme prévu.

Correctif : Utilisez des volumes externes pour le cache du builder avec gestion du cycle de vie ; ou migrez vers des builders distants/cache coté registry ; implémentez des politiques de prune explicites par environnement.

Listes de contrôle / plan étape par étape

Étapes : éliminer les montages docker.sock non sûrs sans casser la production

  1. Inventoriez l’utilisation du socket.

    • Lister les conteneurs en cours qui montent /var/run/docker.sock.
    • Lister les manifests/compose qui l’incluent.
    • Classer par usage : builds CI, monitoring, UI admin, nettoyage, « divers ».
  2. Décidez lesquels sont des interdictions immédiates.

    • Charges applicatives traitant des entrées non fiables : interdiction.
    • Agents tiers : interdiction par défaut.
    • Runners CI : forte attention ; probablement refonte.
  3. Remplacez d’abord les cas d’accès aux métadonnées.

    • Remplacez l’introspection du socket Docker par des métriques cgroup/procfs quand possible.
    • Utilisez des collecteurs côté hôte pour le reste.
  4. Corrigez ensuite les builds CI (risque le plus élevé).

    • Choisissez un pattern de builder : BuildKit distant, builds sans démon, ou DinD isolé par job.
    • Limitez les credentials de registry aux dépôts nécessaires.
    • Séparez les secrets de build des secrets runtime.
  5. Verrouillez les exceptions restantes.

    • Exécutez en non-root.
    • Utilisez l’isolation réseau et des règles entrantes strictes.
    • Ajoutez de l’audit des appels API Docker si faisable.
    • Documentez la justification et une date de retrait.
  6. Faites appliquer, ne plaidez pas.

    • Ajoutez des contrôles dans le CI (rejeter les manifests compose/k8s avec montages de socket).
    • Ajoutez des audits runtime et des alertes (scan périodique des conteneurs en cours).

Checklist opérationnelle : si vous devez autoriser l’accès au socket (rare)

  • Le conteneur avec accès au socket est traité comme un agent hôte privilégié, pas comme une application ordinaire.
  • L’image du conteneur est minimale, figée et patchée agressivement.
  • Tourne en non-root quand possible ; pas de shell, pas de gestionnaire de paquets dans les images de production.
  • L’exposition réseau est minimisée ; pas d’accès entrant depuis des réseaux non fiables.
  • Pas de secrets partagés permettant le mouvement latéral (tokens scoppés ; credentials courte durée).
  • Logs forts : événements de démarrage de conteneurs, pulls d’images, et événements du démon sont surveillés.
  • Propriété claire : qui est alerté en cas de problème et qui approuve les changements.

FAQ

1) Monter /var/run/docker.sock équivaut-il toujours à root ?

Dans les configurations Docker rootful, pratiquement oui. Si un processus peut émettre des appels API Docker créant des conteneurs, il peut généralement escalader en contrôle total de l’hôte
en démarrant un conteneur privilégié et en montant le système de fichiers de l’hôte. Il existe des cas particuliers (autorisation personnalisée, démons contraints), mais ne comptez pas votre parc sur des cas rares.

2) Et si le socket est monté en lecture seule ?

Un montage en lecture seule concerne les opérations d’écriture sur le nœud de socket, pas la capacité à envoyer des requêtes API dessus. Si vous pouvez ouvrir le socket, vous pouvez lui parler.
Le « montage docker.sock en lecture seule » est surtout de la mise en scène sécuritaire.

3) Et si le conteneur tourne en non-root ?

C’est mieux, mais insuffisant. Si l’utilisateur non-root peut accéder au socket (via le mapping de groupe ou des permissions permissives), vous êtes toujours exposé.
Même s’il ne peut pas aujourd’hui, les sorties de conteneur et escalades de privilèges internes deviennent beaucoup plus précieuses quand le socket est présent.

4) Docker n’est-il pas déjà isolé par les namespaces ?

Les conteneurs sont isolés de l’hôte par namespaces et cgroups. Le démon Docker n’est pas un conteneur ; c’est un processus hôte avec des privilèges d’hôte.
Donner du code conteneurisé l’accès au démon, c’est lui fournir une API d’administration de votre couche d’isolation.

5) Notre fournisseur exige docker.sock pour le monitoring. Quelle alternative pragmatique ?

Exécutez un collecteur côté hôte (comme service system) pour rassembler les métadonnées des conteneurs et exporter des métriques assainies.
Ou acceptez des métadonnées réduites via cgroup/procfs. Si le fournisseur insiste pour dire que le socket est impératif, traitez leur agent comme un composant privilégié et isolez-le en conséquence.

6) Exposer Docker sur TCP avec TLS est-il plus sûr que monter le socket ?

Cela peut l’être, si vous appliquez réellement l’authentification client (mutual TLS), restreignez l’accès réseau, et idéalement appliquez une politique d’autorisation.
C’est aussi plus facile à mal configurer par accident. Un socket local n’est au moins pas directement accessible depuis Internet par défaut.

7) Docker rootless résout-il complètement le problème ?

Rootless réduit l’aspect « root de l’hôte » parce que le démon tourne en tant qu’utilisateur non privilégié. Mais cela ne rend pas l’exposition de l’API Docker inoffensive.
Un attaquant peut toujours contrôler des builds, tirer des images, exfiltrer des credentials disponibles pour cet utilisateur, et perturber le service. C’est une atténuation, pas un laissez-passer.

8) Quelle est l’approche la plus propre pour les builds CI aujourd’hui ?

Un builder BuildKit distant ou un builder sans démon est généralement la plus propre : les jobs CI soumettent des builds, reçoivent des artefacts, et n’obtiennent jamais de contrôle runtime au niveau hôte.
Le choix exact dépend de vos besoins de cache, des fonctionnalités Dockerfile et de la gestion des secrets.

9) Comment convaincre des parties prenantes qui ne veulent que livrer ?

Présentez cela comme un enjeu de sécurité en production : un montage de socket transforme toute RCE applicative en compromission d’hôte. Cela modifie la gravité de l’incident, le temps de rétablissement,
et l’exposition réglementaire. Proposez un plan de migration avec étapes mesurables (inventaire, remplacement du monitoring, remplacement des builds CI, enforcement de politique).

10) Si nous retirons le socket, comment faisons-nous le nettoyage et le pruning des conteneurs ?

Exécutez le nettoyage sur l’hôte via des timers systemd ou la gestion du cycle de vie native de l’orchestrateur. Si vous devez le faire dans un conteneur, exécutez un agent hôte dédié et durci avec une exception documentée et une isolation réseau stricte. Mais préférez la gestion côté hôte.

Conclusion : prochaines étapes pratiques

Le socket Docker est une interface d’administration. Traitez-le comme telle. Si votre posture par défaut est « le monter partout », vous avez construit un chemin d’escalade de privilèges dans votre plateforme.
La correction n’est pas un simple flag ; c’est une décision : séparer les « workloads » des « plans de contrôle ».

Prochaines étapes que vous pouvez entreprendre cette semaine :

  1. Inventoriez chaque montage de socket et chaque utilisateur dans le groupe docker.
  2. Retirez les montages de socket des charges applicatives en premier. Pas de débat, pas d’exceptions par défaut.
  3. Refactorez les builds CI pour utiliser des builders distants ou des builds sans démon.
  4. Remplacez l’utilisation du socket pour le monitoring par des collecteurs hôte ou des signaux en lecture seule.
  5. Faites appliquer avec des contrôles politiques et des audits runtime pour éviter que cela ne réapparaisse dans six mois.

Les systèmes de production ne tombent pas en panne parce que quelqu’un ne savait pas. Ils tombent parce qu’une commodité risquée est devenue invisible.
Rendez-la visible. Puis supprimez-la.

← Précédent
Docker : règles de routage Traefik qui échouent silencieusement — corriger les labels correctement
Suivant →
Ubuntu 24.04 : Swap sur SSD — le faire en sécurité (et quand il ne faut pas) (cas n°50)

Laisser un commentaire