The :ro flag on the Docker socket protects nothing
In every code review where someone has had the misfortune of mounting /var/run/docker.sock into a container, there is a small ritual consisting of pointedly noting that one could at least make it read-only, upon which everyone nods, adds :ro to the compose file and closes the merge request with the feeling of duty done. It is theater and I would like to explain precisely why, because the mistake is instructive well beyond Docker.
What :ro really does
The read-only flag sets the MS_RDONLY attribute at the mount level, in the kernel’s VFS. What that governs is writing to files, creating and deleting inodes, everything that actually modifies the filesystem’s content. Now a Unix domain socket is not a file you write to. It is a rendezvous point toward which you open a connection with connect(), after which you talk by sending and receiving bytes on the obtained descriptor, exactly like on a network socket. That dialogue is not a filesystem write, MS_RDONLY never enters the equation and the kernel has strictly no reason to object.
The consequence is immediate and unpleasant. A POST /containers/{id}/stop, a POST /containers/create requesting a bind mount of the host’s root, all of that goes through your read-only-mounted socket without a hitch, because none of these operations writes to a file; they speak HTTP to a daemon which, itself, has all the rights.
The real threat model
Let us call things by their name. Access to docker.sock is root on the host, period. The Docker daemon runs as root and the API it exposes on that socket lets you create a privileged container that bind-mounts the host’s filesystem and installs itself there, which amounts to a complete escape in three requests. The :ro changes absolutely nothing about that reality; it just gives the comforting illusion of having done something. It is a “do not open” sticker on a door left unlocked, which we then call a safe.
What actually protects
Since we must end on something constructive, here is what actually moves the needle, in order of preference. The first question, always, is whether the container really needs the full Docker API, because the answer is often no and the best socket is the one you do not mount.
When access remains necessary, you interpose a filtering proxy, of the tecnativa/docker-socket-proxy or wollomatic/socket-proxy kind, which only allows the strictly required endpoints, so that a dashboard needing to list containers gets exactly GET /containers/json and nothing else and definitely no container creation. You move the daemon to rootless mode
when the workload allows it, so the daemon is no longer root and an escape no longer yields the host. And if access must be remote, you expose the API over TCP with mTLS and a reduced scope, rather than sharing a socket which, by nature, cannot say half-yes.
The moral goes beyond Docker. Confusing a restriction placed on an object, here the socket file, with a restriction placed on the channel that object exposes, here the daemon’s API, is the kind of reasoning error found at every scale and it produces the illusion of security where there is only decoration.
[edit: 2026] The Ansible implementation of all this: the docker role .