Si vous avez déjà tapé docker ps sur un serveur et vous êtes senti responsable, voici un retournement inattendu :
ce même pouvoir peut être accessible depuis Internet ouvert avec un seul mauvais paramètre de configuration.
Pas de chaîne d’exploitation. Pas de payload sophistiqué.
Juste « bonjour, daemon » et votre hôte devient le calcul de quelqu’un d’autre.
L’API distante de Docker n’est pas « juste un autre port de service ». C’est un plan de contrôle proche du root qui peut lancer des conteneurs privilégiés,
monter le système de fichiers de l’hôte et persister discrètement. Traitez-la comme SSH avec authentification par mot de passe et une IP publique — car fonctionnellement c’est pire.
Pourquoi l’API distante Docker équivaut pratiquement à root
Le daemon de Docker (dockerd) est l’autorité qui crée les espaces de noms, configure les cgroups, monte les systèmes de fichiers,
gère le réseau des conteneurs et lance des processus en votre nom. Quand vous contrôlez le daemon, vous contrôlez l’hôte.
L’API distante (sur un socket Unix ou TCP) n’est pas une « interface de gestion » en lecture seule. C’est le volant.
Voici le problème central : le daemon exécute des actions privilégiées, et il fait confiance aux requêtes provenant de son point d’accès API.
Si ce point d’accès est joignable et pas fortement authentifié, toute personne qui peut lui parler peut :
- Lancer un conteneur avec
--privileged - Monter par liaison
/de l’hôte dans un conteneur - Écrire des clés SSH dans
/root/.ssh/authorized_keys - Installer une persistance via des unités systemd, cron ou des services drop-in
- Exfiltrer des secrets depuis les variables d’environnement, les volumes et les couches d’image
- Pivotter dans votre réseau en utilisant le routage et les identifiants de l’hôte
C’est pourquoi les incidents « API Docker exposée » sautent souvent la phase d’exploitation et vont directement à la monétisation : cryptominage, botnets,
vol d’identifiants ou mouvement latéral. Ils n’ont pas besoin de forcer l’entrée. Vous avez ouvert la porte et inscrit « admin à l’intérieur ».
Blague #1 : Exposer 2375 sur Internet, c’est comme cacher la clé de votre maison sous le paillasson — puis diffuser le paillasson en direct.
Faits et contexte historique à connaître
Ce sont des éléments courts et concrets qui importent car ils expliquent pourquoi ce problème revient sans cesse.
Certains sont historiques, d’autres expliquent « comment on en est arrivé là », et tous apparaissent dans des post-mortems.
- Le daemon Docker écoute par défaut sur un socket Unix (
/var/run/docker.sock), pas sur TCP, précisément pour éviter une exposition distante par défaut. - Le port 2375 est conventionnellement « Docker sur TCP sans TLS » ; le port 2376 est « Docker sur TCP avec TLS ». Beaucoup de scanners cherchent d’abord le 2375.
- Les premiers outils Docker ont normalisé les daemons distants (surtout en CI et dans les flux « Docker-in-Docker »), et les habitudes sont restées même si les modèles de menace ont changé.
- Docker Machine a popularisé les points de terminaison Docker distants pour le provisioning ; beaucoup d’anciens billets de blog montrent encore des pratiques non sécurisées copiées dans des flottes modernes.
- L’API distante est basée sur HTTP. Si vous pouvez l’atteindre, vous pouvez lui parler avec
curl. C’est pratique pour l’automatisation et catastrophique pour les réseaux exposés. - Le groupe « docker » équivaut à root sur la plupart des systèmes parce qu’il donne accès au socket du daemon. Ce n’est pas subtil ; c’est fréquemment ignoré.
- Les attaquants ont industrialisé le scannage des Docker exposés il y a des années. Ce n’est pas une menace de niche ; c’est un bruit de fond automatisé comme les tentatives SSH.
- Les règles de sécurité cloud et les paramètres de pare-feu ont évolué dans le temps. Une règle entrante « temporaire » devient parfois permanente parce que personne n’en assume la suppression.
Modèle de menace : comment les attaquants exploitent un daemon exposé
La chaîne d’attaque commune est embarrassante de simplicité
Quand Docker écoute sur tcp://0.0.0.0:2375 (ou une interface publique) sans authentification TLS client,
le flux de travail de l’attaquant est essentiellement :
- Trouver une IP avec le port 2375 ouvert.
- Appeler l’API pour lister containers/images.
- Lancer un conteneur avec un montage par liaison du système de fichiers racine de l’hôte.
- Modifier les fichiers de l’hôte pour persister l’accès.
- Optionnellement, lancer un mineur dans un conteneur et partir.
Pourquoi les conteneurs ne vous protègent pas d’un contrôle du daemon
Les conteneurs sont des primitives d’isolation, pas une sécurité magique. La frontière d’isolation est appliquée par le noyau, mais l’entité qui configure
cette frontière est le daemon. Si vous pouvez dire au daemon « monte la racine de l’hôte dans ce conteneur », le noyau se conforme parce que
le daemon est autorisé à le demander.
Qu’en est-il de « Docker rootless » ?
Docker rootless réduit le rayon d’impact, mais cela ne rend pas une API exposée inoffensive. Elle peut toujours donner à un attaquant la capacité d’exécuter des
charges de travail arbitraires en tant que cet utilisateur, d’accéder à ses secrets et de pivoter. Rootless aide ; ce n’est pas une carte « sortie de prison ».
Une citation à garder en tête
« L’espoir n’est pas une stratégie. » (idée paraphrasée, souvent attribuée à des ingénieurs et planificateurs militaires)
Traitez « nous pensons que ce n’est pas exposé » comme de l’espoir.
Mode opératoire de diagnostic rapide
Vous êtes de garde. Quelqu’un dit : « Pourquoi cet hôte surchauffe ? » Ou pire : « Pourquoi avons-nous du trafic sortant vers des destinations bizarres ? »
Ne commencez pas par débattre de la philosophie des conteneurs. Commencez par localiser l’exposition du plan de contrôle et la charge réelle.
Premier point : le daemon est-il exposé sur TCP ?
- Vérifiez les ports à l’écoute (
ss/lsof) et les flags du service Docker. - Vérifiez l’état du pare-feu/groupes de sécurité, pas seulement la configuration locale.
- Si vous voyez
0.0.0.0:2375ou:::2375, traitez-le comme un incident actif jusqu’à preuve du contraire.
Deuxième point : y a-t-il des preuves de conteneurs ou d’images non autorisés ?
- Listez les conteneurs en cours ; recherchez des mineurs, des noms d’image aléatoires, des conteneurs avec
--privileged, des montages d’hôte ou des réglages réseau étranges. - Inspectez les timestamps de création récents des conteneurs.
- Recherchez de nouveaux utilisateurs/clefs SSH/unités systemd créés par des processus conteneurisés écrivant sur l’hôte.
Troisième point : contenir, puis enquêter
- Bloquez immédiatement l’accès entrant au daemon au niveau du périmètre réseau.
- Capturez des preuves (listes de processus, métadonnées des conteneurs, logs) avant de tout effacer.
- Faites tourner les identifiants qui auraient pu être accessibles : rôles d’instance cloud, identifiants de registre, secrets d’application sur disque.
Tâches pratiques : détecter, confirmer et décider (commandes incluses)
Voici des tâches pratiques que vous pouvez exécuter sur un hôte. Chacune inclut ce que la sortie signifie et quelle décision en découle.
Pas de magie, pas de « vérifiez juste votre pare-feu ». Vous êtes le pare-feu maintenant.
Tâche 1 : Vérifier si Docker écoute sur TCP
cr0x@server:~$ sudo ss -lntp | grep -E ':(2375|2376)\s'
LISTEN 0 4096 0.0.0.0:2375 0.0.0.0:* users:(("dockerd",pid=1024,fd=6))
Ce que cela signifie : dockerd écoute sur toutes les interfaces IPv4 sur le port 2375, en clair.
Décision : traiter comme une exposition critique. Procéder à la contention : bloquer au pare-feu et reconfigurer le daemon.
Tâche 2 : Confirmer la configuration du daemon via systemd
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 -H tcp://0.0.0.0:2375
Ce que cela signifie : Le daemon a été démarré explicitement avec un écouteur TCP. Ce n’est pas un comportement accidentel du noyau ; c’est une configuration.
Décision : supprimez l’hôte TCP à moins que vous n’implémentiez TLS mutuel et des contrôles réseau stricts.
Tâche 3 : Vérifier docker info pour hosts configurés et options de sécurité
cr0x@server:~$ docker info | sed -n '1,80p'
Client:
Context: default
Debug Mode: false
Server:
Containers: 12
Running: 3
Paused: 0
Stopped: 9
Server Version: 26.1.0
Storage Driver: overlay2
Security Options:
apparmor
seccomp
Profile: builtin
Ce que cela signifie : Cela ne vous dit pas directement les adresses de liaison du daemon, mais montre la base du profil de sécurité.
Décision : si vous trouvez ensuite des conteneurs non autorisés, vous voudrez savoir si AppArmor/seccomp était en place (et si --privileged l’a contourné).
Tâche 4 : Identifier comment le daemon est lié (daemon.json)
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"]
}
Ce que cela signifie : L’API distante sur TCP en clair est configurée de manière persistante.
Décision : supprimez l’entrée d’hôte TCP ou migrez vers TLS sur le port 2376 avec authentification par certificat client.
Tâche 5 : Vérifier l’état du pare-feu sur l’hôte (exemple UFW)
cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
22/tcp ALLOW IN 203.0.113.0/24
2375/tcp ALLOW IN Anywhere
Ce que cela signifie : Vous avez une stratégie « deny par défaut », puis vous avez autorisé le seul port qui peut livrer les clés.
Décision : supprimez cette règle immédiatement sauf si elle est strictement limitée à un réseau de gestion avec TLS.
Tâche 6 : Valider l’exposition depuis l’extérieur (en utilisant une seconde machine)
cr0x@server:~$ curl -s http://198.51.100.10:2375/version
{"Platform":{"Name":"Docker Engine - Community"},"Components":[{"Name":"Engine","Version":"26.1.0","Details":{"ApiVersion":"1.45"}}],"ApiVersion":"1.45","MinAPIVersion":"1.24","GitCommit":"...","GoVersion":"go1.22.2","Os":"linux","Arch":"amd64","KernelVersion":"6.8.0-41-generic","BuildTime":"..."}
Ce que cela signifie : Si vous pouvez récupérer /version sans authentification, n’importe qui le peut.
Décision : c’est un incident, pas un simple ticket. Contenez maintenant ; enquêtez ensuite.
Tâche 7 : Lister les conteneurs via l’API distante (cela ne devrait pas fonctionner anonymement)
cr0x@server:~$ curl -s http://198.51.100.10:2375/containers/json?all=1 | head
[{"Id":"b1c...","Names":["/web-1"],"Image":"nginx:alpine","State":"running","Status":"Up 3 days"},
{"Id":"f9a...","Names":["/xmrig"],"Image":"unknown:latest","State":"running","Status":"Up 2 hours"}]
Ce que cela signifie : Énumération distante non authentifiée. La présence d’un nom/image de conteneur suspect est un signal d’alerte.
Décision : commencez la collecte de preuves et la contention. Ne « supprimez pas simplement » avant d’avoir capturé les métadonnées et les logs.
Tâche 8 : Inspecter un conteneur suspect pour les montages d’hôte et le niveau de privilège
cr0x@server:~$ docker inspect xmrig --format '{{.HostConfig.Privileged}} {{json .Mounts}}'
true [{"Type":"bind","Source":"/","Destination":"/host","Mode":"rw","RW":true,"Propagation":"rprivate"}]
Ce que cela signifie : Ce conteneur est privilégié et a la racine de l’hôte montée en lecture-écriture sur /host.
Décision : présumez une compromission de l’hôte. Prévoyez reconstruction/restauration, rotation des identifiants et audit complet.
Tâche 9 : Vérifier les conteneurs créés récemment (indice de chronologie)
cr0x@server:~$ docker ps -a --format 'table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.CreatedAt}}\t{{.Status}}' | head -n 10
CONTAINER ID IMAGE NAMES CREATED AT STATUS
f9a2d1c3ab11 unknown:latest xmrig 2026-01-03 01:12:44 +0000 Up 2 hours
b1c9aa88d220 nginx:alpine web-1 2025-12-30 09:01:02 +0000 Up 3 days
Ce que cela signifie : Un nouveau conteneur est apparu récemment. Si personne ne peut l’expliquer, il est non autorisé jusqu’à preuve du contraire.
Décision : corrélez avec les logs (dockerd, auditd, logs de flux cloud) et contenez.
Tâche 10 : Examiner les logs du daemon Docker pour accès via l’API distante
cr0x@server:~$ sudo journalctl -u docker --since "6 hours ago" | tail -n 20
time="2026-01-03T01:12:43.991Z" level=info msg="API listen on [::]:2375"
time="2026-01-03T01:12:44.120Z" level=info msg="POST /v1.45/containers/create"
time="2026-01-03T01:12:44.348Z" level=info msg="POST /v1.45/containers/f9a2d1c3ab11/start"
Ce que cela signifie : Vous avez un enregistrement horodaté de création de conteneur via l’API.
Décision : préservez les logs. Si vous avez une journalisation centralisée, assurez-vous de la rétention et de l’export pour l’analyse d’incident.
Tâche 11 : Vérifier qui a accès au socket Docker localement
cr0x@server:~$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 Jan 3 00:01 /var/run/docker.sock
Ce que cela signifie : Les membres du groupe docker peuvent contrôler le daemon.
Décision : gardez l’appartenance au groupe docker minimale et auditée ; traitez-la comme un accès sudo.
Tâche 12 : Énumérer les membres du groupe docker
cr0x@server:~$ getent group docker
docker:x:999:ci-runner,alice
Ce que cela signifie : Deux utilisateurs ont effectivement un contrôle équivalent à root via Docker.
Décision : si vous n’avez pas de raison forte, retirez les humains de ce groupe et forcez les opérations privilégiées via une automatisation contrôlée.
Tâche 13 : Contention immédiate — bloquer le TCP 2375 localement (iptables)
cr0x@server:~$ sudo iptables -I INPUT -p tcp --dport 2375 -j DROP
cr0x@server:~$ sudo iptables -L INPUT -n --line-numbers | head
Chain INPUT (policy ACCEPT)
num target prot opt source destination
1 DROP tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:2375
Ce que cela signifie : Vous avez stoppé l’hémorragie au niveau de l’hôte (ceci n’est pas un substitut aux corrections au niveau du périmètre).
Décision : conservez cette règle jusqu’à ce que vous ayez corrigé la configuration du daemon et vérifié que l’exposition extérieure a disparu.
Tâche 14 : Corriger le daemon pour retirer l’écouteur TCP (override systemd)
cr0x@server:~$ sudo systemctl edit docker
# Creates /etc/systemd/system/docker.service.d/override.conf
cr0x@server:~$ sudo cat /etc/systemd/system/docker.service.d/override.conf
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
Ce que cela signifie : Vous avez remplacé l’unité pour supprimer l’argument d’hôte TCP.
Décision : rechargez systemd et redémarrez Docker ; puis revérifiez les sockets à l’écoute et la possibilité d’atteinte externe.
Tâche 15 : Redémarrer Docker en toute sécurité et confirmer les sockets
cr0x@server:~$ sudo systemctl daemon-reload
cr0x@server:~$ sudo systemctl restart docker
cr0x@server:~$ sudo ss -lntp | grep -E ':(2375|2376)\s' || echo "no docker tcp listener"
no docker tcp listener
Ce que cela signifie : Le daemon n’écoute plus sur TCP.
Décision : vérifiez maintenant depuis un réseau externe. Ne vous fiez pas uniquement aux vérifications locales.
Tâche 16 : Si vous avez vraiment besoin d’accès distant, imposez TLS mutuel sur 2376
Si votre réponse est « mais notre automatisation en a besoin », très bien. Vous n’êtes toujours pas autorisé à utiliser du clair.
Avec Docker, « TLS activé » n’a pas de sens si vous n’exigez pas l’authentification par certificat client.
cr0x@server:~$ sudo cat /etc/docker/daemon.json
{
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"],
"tlsverify": true,
"tlscacert": "/etc/docker/pki/ca.pem",
"tlscert": "/etc/docker/pki/server-cert.pem",
"tlskey": "/etc/docker/pki/server-key.pem"
}
Ce que cela signifie : Le daemon exigera un certificat client signé par votre CA.
Décision : n’autorisez le 2376 que depuis un réseau de gestion, et distribuez les clés client comme vous distribuez les clés SSH : de manière minimale, avec rotation et journalisation.
Tâche 17 : Tester TLS depuis un client avec certificat (devrait réussir)
cr0x@server:~$ docker --host tcp://198.51.100.10:2376 \
--tlsverify \
--tlscacert ./ca.pem \
--tlscert ./client-cert.pem \
--tlskey ./client-key.pem \
version
Client: Docker Engine - Community
Version: 26.1.0
API version: 1.45
Server: Docker Engine - Community
Engine:
Version: 26.1.0
API version: 1.45 (minimum version 1.24)
Ce que cela signifie : Votre client peut s’authentifier et parler au daemon.
Décision : procédez uniquement si vous pouvez aussi prouver que l’accès non authentifié échoue.
Tâche 18 : Tester l’accès non authentifié à 2376 (devrait échouer)
cr0x@server:~$ curl -s https://198.51.100.10:2376/version | head
Client sent an HTTP request to an HTTPS server.
Ce que cela signifie : Le HTTP en clair est rejeté. Maintenant essayez HTTPS sans certificat client.
Décision : si HTTPS sans certificat client fonctionne, vous êtes encore exposé.
cr0x@server:~$ curl -sk https://198.51.100.10:2376/version | head
TLS handshake error: remote error: tls: bad certificate
Ce que cela signifie : Le daemon exige un certificat client valide.
Décision : c’est le seuil minimal pour que « API Docker distante » ne soit pas un portail root ouvert.
Tâche 19 : Rechercher une persistance suspecte sur l’hôte (systemd)
cr0x@server:~$ systemctl list-unit-files --type=service | grep -E 'docker|container|update|agent' | tail
docker.service enabled
containerd.service enabled
system-update.service disabled
Ce que cela signifie : C’est un contrôle léger. Il ne détectera pas tout, mais il attrape souvent les persistances bâclées.
Décision : si vous suspectez une compromission, suivez par des vérifications d’intégrité des fichiers et un plan de reconstruction.
Patrons de durcissement qui tiennent en production
1) Le bon défaut : pas de socket TCP, seulement socket Unix
Le contrôle le plus propre est de ne pas avoir de plan de contrôle distant du tout. Utilisez SSH pour atteindre l’hôte, puis parlez au socket Unix localement.
Oui, c’est moins « cloud-native ». C’est aussi moins « convivial pour les criminels ».
Si vous avez besoin d’orchestration distante, envisagez des outils qui n’exigent pas d’exposer massivement le daemon. Beaucoup d’équipes utilisent des tunnels SSH
pour les rares cas où l’accès API distant est requis temporairement.
2) Si vous devez exposer Docker : TLS mutuel, étroite délimitation réseau et identifiants de courte durée
Le TLS mutuel est non négociable. Pas « TLS avec un certificat serveur ». Pas « authentification basique derrière un proxy ». Certificats clients, signés par une CA que vous contrôlez,
et rotatifs comme tout autre identifiant.
Puis restreignez l’accès. « Seulement nos IP de bureau » n’est pas une restriction ; c’est de l’optimisme avec une touche de fragilité VPN. Vous voulez :
- Entrées entrantes autorisées uniquement depuis un sous-réseau de gestion dédié
- Deny explicite depuis partout ailleurs
- Règles de security group revues comme du code (parce que c’est un comportement de production)
3) N’utilisez pas un reverse proxy générique devant l’API Docker sauf si vous maîtrisez
Mettre l’API HTTP de Docker derrière un reverse proxy semble propre jusqu’à ce que quelqu’un ajoute une route permissive, désactive l’authentification client « juste pour les tests »,
ou loggue des en-têtes sensibles. Aussi : l’API Docker n’est pas conçue comme une application web publique. C’est un plan de contrôle.
Si vous insistez pour un proxy, il doit assurer un TLS mutuel de bout en bout, des listes d’autorisation strictes d’endpoints et une journalisation utile pour la réponse aux incidents
sans divulguer de secrets. La plupart des proxies en production finissent par être une façon compliquée d’être non sécurisé.
4) Traitez l’appartenance au groupe docker comme un accès privilégié
C’est là que la réalité d’entreprise fait mal. Les gens ajoutent des runners CI, des développeurs et des « contractuels temporaires » au groupe docker parce que c’est plus rapide que
définir des frontières de privilèges correctes. C’est ainsi que vous obtenez un chemin de mouvement latéral interne contournant l’audit sudo.
Votre objectif : garder l’accès au daemon derrière une automatisation qui a des validations et des logs, ou derrière des shells root avec MFA.
Tout le reste n’est que root distribué avec des étapes en plus.
5) Intégrez la détection dans votre baseline
Vous pouvez prévenir l’exposition et vouloir tout de même de la détection parce que la réalité est désordonnée. Vérifications de baseline qui attrapent les pires erreurs :
- Alerter si
dockerdse lie sur0.0.0.0:2375ou:::2375 - Alerter sur des règles de pare-feu autorisant 2375/2376 depuis des CIDR non gestion
- Alerter sur de nouveaux conteneurs privilégiés ou conteneurs montant
/ - Suivre centralement les événements create/start des conteneurs
Trois mini-récits d’entreprise issus du terrain
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
Une entreprise SaaS de taille moyenne avait un environnement de staging qui semblait « interne » parce qu’il tournait dans un VPC cloud. L’équipe a supposé que VPC voulait dire privé.
Ce n’était pas le cas. Un des nœuds avait une IP publique pour la commodité, et une règle de security group qui autorisait le 2375 entrant depuis « Anywhere »
parce qu’un contractuel devait exécuter une migration ponctuelle.
Personne n’a retiré la règle. Le contractuel est parti. Le ticket a été enterré par une réorganisation trimestrielle. Un mois plus tard, le nœud a commencé à chauffer,
puis a été limité par le fournisseur cloud. L’hypothèse initiale était « mauvais déploiement » car c’est généralement le cas.
Ils ont rollbacké. Le CPU est resté saturé.
L’ingénieur de garde a finalement lancé docker ps et a vu un conteneur avec un nom absurde et un timestamp de création récent.
Ils l’ont tué. Il est revenu. Ils l’ont tué encore. Il est revenu encore. Ce jour-là, ils ont découvert que le daemon était accessible
depuis l’extérieur et que l’attaquant repostait simplement des requêtes « create container ».
La contention a été rapide : bloquer le 2375 au périmètre, arrêter Docker, snapshotter le disque pour analyse. La partie douloureuse a été la rotation des identifiants.
Le nœud de staging avait accès à des identifiants de registre partagés et à quelques clés API « temporaires » qui étaient aussi utilisées en dev.
La compromission n’a pas sauté d’environnement, mais l’évaluation du rayon d’impact a pris une semaine.
La mauvaise hypothèse n’était pas « Docker est insecure ». La mauvaise hypothèse était « réseau interne == sûr par défaut ».
Dans les réseaux cloud, interne est une politique que vous appliquez en permanence, pas une ambiance.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Une grande entreprise gérait une flotte de serveurs de build. Les builds étaient lents, alors un ingénieur a essayé d’accélérer en laissant les jobs de build parler
directement à un daemon Docker distant sur TCP. La théorie : éviter la virtualisation imbriquée et réduire l’usure disque locale.
Ça a marché. Les temps de build ont nettement diminué.
Puis est venue la « simplification mineure ». Au lieu de gérer des certificats TLS dans le système CI, ils sont passés temporairement au 2375
avec l’intention de « resserrer plus tard », protégés seulement par des listes d’IP. Cette liste vivait dans une config de pare-feu gérée par une équipe séparée.
Les demandes de changement prenaient des jours. Alors ils l’ont élargie un peu. Et encore un peu. Finalement, « juste pour une semaine », c’était joignable depuis une large plage corporate.
Ce qui a mal tourné, ce n’est pas seulement qu’un attaquant externe l’ait trouvé immédiatement (même si ça arrive souvent). Ce qui a mal tourné, c’est le mouvement latéral interne.
Un poste développeur compromis s’est connecté au VPN corporate. De là, il pouvait atteindre le daemon Docker.
L’attaquant n’avait pas besoin d’un administrateur de domaine. Il avait besoin d’un daemon accessible et d’un moyen de lancer des conteneurs qui pouvaient monter des secrets depuis les caches de build.
Le retour de bâton a été brutal car la pipeline était devenue un point d’agrégation d’identifiants : tokens de registre, clés de signature, proxies de dépendances.
Même sans breakout complet de l’hôte, lire des volumes de workspace suffisait pour faire de vrais dégâts.
La correction post-incident n’a pas été « builds plus rapides ». Ce fut « isolation des builds qui n’implique pas une API root accessible à distance ».
Blague #2 : « On ajoutera TLS plus tard » est l’équivalent sécurité de « je commencerai les sauvegardes demain » — un beau plan qui ne survit jamais à la réalité.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la situation
Une autre entreprise avait une baseline stricte : les daemons Docker n’étaient pas autorisés à écouter sur TCP, point final.
Si une équipe avait besoin de contrôle distant, elle utilisait SSH vers un bastion et un tunnel court vers le socket Unix, avec enregistrement de session.
Tout le monde trouvait ça un peu pénible. Ce qui est souvent le signe que ça fonctionne.
Un après-midi, une alerte de monitoring a déclenché : des connexions sortantes en pic depuis un nœud de production vers des plages IP inconnues.
La suspicion immédiate était « API Docker exposée » parce que l’équipe sécurité avait déjà vu ce film.
L’ingénieur de garde a vérifié ss -lntp et n’a rien vu sur 2375/2376. Cela a éliminé rapidement une grande catégorie de défaillances.
Ils ont pivoté. Il s’est avéré que c’était un conteneur applicatif compromis qui appelait à l’extérieur, pas un hôte compromis.
Étant donné que le daemon n’était pas accessible à distance, l’attaquant n’a pas pu facilement créer des conteneurs privilégiés ou monter le système de fichiers hôte.
Le rayon d’impact est resté dans les permissions et secrets de l’app.
La réponse a tout de même été sérieuse : rotation des secrets applicatifs, patch de la dépendance vulnérable, redeploiement.
Mais ils ont évité le cauchemar « reconstruire toute la flotte d’hôtes et faire tourner toutes les rotations ». La baseline ennuyeuse n’a pas empêché tous les problèmes.
Elle a empêché que le problème devienne une catastrophe.
Erreurs courantes : symptômes → cause racine → correction
1) Symptôme : CPU saturé, conteneurs étranges, egress réseau inexpliqué
Cause racine : daemon Docker exposé sur le 2375 ; un attaquant exécute des conteneurs mineurs et les recrée après suppression.
Correction : bloquer immédiatement les entrées 2375/2376, retirer l’écouteur TCP de dockerd, reconstruire l’hôte si des conteneurs privilégiés ont monté l’hôte.
2) Symptôme : « Nous utilisons TLS » mais n’importe qui peut encore se connecter
Cause racine : TLS côté serveur seulement ; tlsverify non activé ; authentification par certificat client non enforced.
Correction : définir "tlsverify": true et exiger des certificats clients signés par votre CA ; confirmer que curl -sk non autorisé échoue.
3) Symptôme : Docker « n’écoute pas », mais l’accès distant fonctionne toujours
Cause racine : un proxy sidecar ou un autre processus fait du forwarding vers le socket Unix ; ou une instance différente du daemon est liée via un drop-in systemd.
Correction : vérifier systemctl cat docker pour des overrides, chercher des configs de proxy, et vérifier les sockets réels avec ss -lntp.
4) Symptôme : Les développeurs peuvent « juste exécuter des commandes Docker » sans sudo
Cause racine : les utilisateurs sont dans le groupe docker, qui équivaut effectivement à root sur cet hôte.
Correction : supprimer les utilisateurs inutiles du groupe ; imposer les opérations privilégiées via une automatisation contrôlée ou sudo avec audit.
5) Symptôme : « Le conteneur revient » après suppression
Cause racine : un acteur externe appelle l’API distante pour recréer des conteneurs ; ou vous avez un orchestrateur/CI compromis qui le fait.
Correction : coupez l’accès réseau au daemon d’abord, puis enquêtez sur les identifiants d’orchestration et les logs.
6) Symptôme : « On l’a ouvert seulement au VPC » et on s’est quand même fait attaquer
Cause racine : le VPC n’est pas aussi privé que vous le pensez : IP publique attachée, peering, VPN, règle de security group mal routée, ou un hôte interne compromis.
Correction : supposez que les réseaux internes sont hostiles ; restreignez aux sous-réseaux de gestion, exigez TLS mutuel et segmentez les systèmes de build du reste.
Listes de contrôle / plan étape par étape
Étape par étape : verrouiller un hôte aujourd’hui
-
Trouver les écouteurs : lancez
ss -lntpet confirmez que rien ne se lie sur 2375/2376 sur des interfaces publiques. -
Confirmer la config du service : vérifiez
systemctl cat dockeret/etc/docker/daemon.jsonpour des hôtestcp://. - Bloquer au périmètre : retirez les règles de security group / pare-feu pour 2375 et 2376 sauf si vous avez un réseau de gestion et TLS mutuel.
- Bloquer localement pour defence-in-depth : droppez le 2375 entrant avec iptables/nftables (contingence temporaire et filet de sécurité).
-
Réduire la propagation des privilèges : auditez le groupe
docker; retirez les utilisateurs qui n’en ont pas besoin. -
Activer la surveillance : alertez sur de nouveaux conteneurs privilégiés, conteneurs montant
/, et tout nouvel écouteur TCP pourdockerd. - Documenter un processus d’exception : si une équipe a vraiment besoin de Docker distant, exigez TLS mutuel + restrictions CIDR + date d’expiration sur les règles de pare-feu.
Étape par étape : si vous suspectez qu’une exposition a déjà eu lieu
- Contenir : bloquez immédiatement l’accès entrant à l’API Docker (périmètre + local). Ne discutez pas.
- Préserver les preuves : exportez
journalctl -u docker,docker ps -a,docker inspectpour les conteneurs suspects, et les logs système. - Évaluer la compromission de l’hôte : si vous trouvez des conteneurs privilégiés avec montages d’hôte, présumez une modification au niveau hôte.
- Faire tourner les identifiants : tokens de registre, identifiants de rôle d’instance cloud (si applicable), secrets d’app sur disque, identifiants CI utilisés sur ce nœud.
- Reconstruire proprement : privilégiez la reconstruction du nœud depuis une image connue bonne plutôt que le « nettoyage ». Le nettoyage est la façon dont on rate les persistances.
- Fermer la boucle : ajoutez la détection pour que la même exposition ne revienne pas discrètement lors du prochain changement « temporaire ».
Une position politique qui fonctionne
- Interdire le Docker TCP en clair (2375) entièrement. Aucune exception.
- Autoriser Docker TCP TLS (2376) uniquement avec TLS mutuel et une délimitation réseau stricte.
- Traiter l’accès au daemon Docker comme du root en production. Parce que c’en est.
FAQ
1) Est-il acceptable d’exposer le port 2375 ?
Non. Ni « rarement », ni « derrière un pare-feu », ni « juste pour une semaine ». L’API Docker en clair et non authentifiée est un root distant par conception.
Si vous avez besoin d’un contrôle distant, utilisez TLS mutuel sur 2376 et des restrictions réseau strictes — ou mieux, n’exposez rien du tout.
2) Si je lie Docker à 127.0.0.1, suis-je en sécurité ?
Plus sûr, oui. Sûr, pas automatiquement. Les liaisons locales réduisent l’exposition distante, mais toute compromission locale (y compris SSRF depuis une appli web pouvant atteindre localhost)
peut encore l’abuser. Traitez-le comme sensible même sur loopback.
3) Pourquoi l’API Docker est-elle considérée comme équivalente à root ?
Parce qu’elle peut lancer des conteneurs privilégiés, monter des chemins hôtes et manipuler le réseau. Ce sont des actions d’administration d’hôte.
Si le daemon peut le faire, et que l’API peut ordonner au daemon de le faire, alors l’API est un administrateur d’hôte.
4) L’utilisation de Docker en mode rootless règle-t-elle le problème ?
Cela réduit le rayon d’impact de la compromission du daemon, mais cela ne rend pas une API exposée acceptable.
Un attaquant peut toujours exécuter des charges, voler les secrets de cet utilisateur et potentiellement pivoter via des identifiants et l’accès réseau.
5) Nous utilisons Kubernetes ; cela s’applique-t-il toujours ?
Oui. Beaucoup de nœuds Kubernetes utilisent des runtimes de conteneurs qui ne sont pas Docker, mais vous pouvez toujours avoir Docker installé pour des workloads legacy,
du debugging ou la CI. Tout daemon exposé sur un nœud est un point d’appui pour un attaquant dans l’environnement du cluster.
6) Puis-je mettre une authentification basique devant l’API Docker avec un reverse proxy ?
Vous pouvez, mais ne l’appelez pas « sécurisé » à moins d’avoir aussi une forte sécurité de transport, un allowlist d’endpoints strict et une politique crédible de rotation des identifiants.
Le TLS mutuel est la norme parce qu’il est plus difficile à affaiblir par accident.
7) Quelle est la façon la plus rapide de savoir si nous sommes exposés maintenant ?
Depuis l’extérieur de votre réseau, essayez de récupérer /version sur le port 2375. Si ça répond, vous êtes exposés.
En interne, vérifiez ss -lntp et vos règles de pare-feu/groupes de sécurité pour 2375/2376.
8) Si nous avons été exposés, devons-nous vraiment reconstruire l’hôte ?
Si des attaquants ont lancé des conteneurs privilégiés avec des montages d’hôte ou si vous ne pouvez pas prouver qu’ils ne l’ont pas fait, reconstruire est le choix raisonnable.
« Nous avons supprimé le conteneur » n’est pas une garantie. La persistance est bon marché.
9) Comment garder l’automatisation fonctionnelle sans exposer Docker ?
Exécutez l’automatisation sur l’hôte (ou via SSH), utilisez un bastion avec accès audité, ou migrez les workflows de build vers des builders isolés où le socket Docker n’est jamais exposé à des réseaux non fiables.
Si le contrôle distant est obligatoire, utilisez TLS mutuel et des sous-réseaux de gestion.
Conclusion : que faire ensuite
L’API distante Docker est un outil puissant. Laissez-la sur un banc public et quelqu’un construira un abri que vous n’avez pas commandé.
Votre travail est de rendre l’« exposition accidentelle de root » structurellement difficile.
Étapes pratiques suivantes :
- Scannez votre flotte pour détecter des écouteurs sur 2375/2376 et pour des arguments
dockerdcontenanttcp://. - Supprimez le 2375 en clair partout. Si vous le trouvez, traitez-le comme un incident de sécurité jusqu’à vérification contraire.
- Si Docker distant est réellement nécessaire, exigez TLS mutuel, restreignez par CIDR de gestion et faites tourner les certificats selon un calendrier défendable.
- Auditez l’appartenance au groupe
dockercomme si c’était sudo, car en pratique ça l’est. - Rédigez le runbook maintenant, avant que le mineur n’arrive.