Le :ro sur le socket Docker ne protège rien
Il existe, dans toute revue de code où quelqu’un a eu le malheur de monter /var/run/docker.sock dans un conteneur, un petit rituel qui consiste à faire remarquer d’un air entendu qu’on pourrait au moins le passer en lecture seule, sur quoi tout le monde hoche la tête, ajoute :ro dans le compose et referme la merge request avec le sentiment du devoir accompli. C’est du théâtre et j’aimerais expliquer précisément pourquoi, parce que l’erreur est instructive bien au-delà de Docker.
Ce que fait vraiment :ro
Le flag lecture seule pose l’attribut MS_RDONLY au niveau du montage, dans le VFS du noyau. Ce que cela régit, c’est l’écriture sur des fichiers, la création et la suppression d’inodes, tout ce qui modifie effectivement le contenu du système de fichiers. Or une socket de domaine Unix n’est pas un fichier qu’on écrit. C’est un point de rendez-vous vers lequel on ouvre une connexion avec connect(), après quoi l’on dialogue en envoyant et recevant des octets sur le descripteur obtenu, exactement comme sur une socket réseau. Ce dialogue n’est pas une écriture de système de fichiers, MS_RDONLY n’entre jamais dans l’équation et le noyau n’a strictement aucune raison de s’y opposer.
La conséquence est immédiate et déplaisante. Un POST /containers/{id}/stop, un POST /containers/create demandant un bind mount de la racine de l’hôte, tout cela passe sans le moindre accroc à travers votre socket montée en lecture seule, parce qu’aucune de ces opérations n’écrit dans un fichier, elles parlent un protocole HTTP à un démon qui, lui, a tous les droits.
Le vrai modèle de menace
Il faut appeler les choses par leur nom. Un accès à docker.sock, c’est root sur l’hôte, point final. Le démon Docker tourne en root et l’API qu’il expose sur cette socket permet de créer un conteneur privilégié qui bind-monte le système de fichiers de l’hôte et s’y installe, ce qui revient à une évasion complète en trois requêtes. Le :ro ne change absolument rien à cette réalité, il donne juste l’illusion réconfortante d’avoir fait quelque chose. C’est un autocollant “ne pas ouvrir” sur une porte qu’on a laissée déverrouillée et qu’on appelle ensuite un coffre-fort.
Ce qui protège pour de bon
Puisqu’il faut finir sur du constructif, voici ce qui déplace réellement l’aiguille, par ordre de préférence. La première question, toujours, est de savoir si le conteneur a vraiment besoin de l’API Docker complète, parce que la réponse est souvent non et que la meilleure socket est celle qu’on ne monte pas.
Quand un accès reste nécessaire, on interpose un proxy filtrant, du type tecnativa/docker-socket-proxy ou wollomatic/socket-proxy, qui n’autorise que les endpoints strictement requis, de sorte qu’un dashboard qui a besoin de lister les conteneurs n’obtienne qu’un GET /containers/json et rien d’autre, surtout pas la création de conteneurs. On passe au démon en mode rootless
quand le workload s’y prête, pour que le démon ne soit plus root et qu’une évasion ne donne plus l’hôte. Et si l’accès doit être distant, on expose l’API en TCP avec mTLS et un périmètre réduit, plutôt que de partager une socket qui, par nature, ne sait pas dire non à moitié.
La morale dépasse Docker. Confondre une restriction posée sur un objet, ici le fichier socket, avec une restriction posée sur le canal que cet objet expose, ici l’API du démon, c’est le genre d’erreur de raisonnement qu’on retrouve à toutes les échelles et qui donne l’illusion de la sécurité là où il n’y a que de la décoration.
[edit: 2026] L’implémentation Ansible de tout ceci: le rôle docker .