Skip to content

Hardening Docker

Docker’s security model has one fact at the centre of it, and everything else on this page follows from it:

Access to the Docker socket is root on the host. Not “close to root”, not “root inside a container” — root on the machine. Anyone who can talk to the daemon can start a container that mounts / and step into it. That is not a bug or a misconfiguration; it is what the API is for.

So Docker hardening is mostly about deciding who can reach the daemon, and then about narrowing what the containers it starts are allowed to do.

7 controls

Worth stating separately because it surprises people who have been running docker commands for years:

Terminal window
sudo usermod -aG docker alice

That command grants Alice root on the host. Docker’s own documentation says so. The docker group exists to let you skip sudo, and skipping sudo here means talking directly to a root-owned daemon that will do anything you ask — including docker run -v /:/host --privileged.

If your mental model is “the docker group is a convenience”, replace it with “the docker group is the wheel group”. It changes who you’re willing to add.

The way out of that trade-off is rootless mode, which is the one structural fix on this list.

Worth knowing before you invest in hardening Docker specifically.

Podman 5.x is the default container tool on RHEL, Fedora and CentOS — Red Hat ships and supports it, and Docker is not what you get by default there. The architectural difference is the relevant part: Podman is daemonless and rootless by default, so the “socket is root” problem doesn’t exist in the same shape. There is no long-running root daemon to reach.

Podman is largely CLI-compatible with Docker (alias docker=podman gets many people surprisingly far), and RHEL 9.5+ uses pasta as the default rootless network handler.

This is not an argument to migrate a working Docker setup. It is an argument that if you’re on RHEL and starting fresh, the rootless-by-default option is already installed, and most of this cluster becomes moot.

Terminal window
docker version
runc --version

Docker Engine 29.x is current (29.5 in May 2026, which changed the rootless network driver from slirp4netns to gvisor-tap-vsock).

Check runc too, and not casually — runc is where container escapes live. The Leaky Vessels set (CVE-2024-21626 and the BuildKit CVEs) were fixed in runc 1.1.12 and BuildKit v0.12.5. A current Docker with an old runc is a current Docker with a container escape.

/etc/docker/daemon.json — which frequently does not exist, because Docker runs fine without it. Its absence is normal, not a finding.

Terminal window
docker info
sudo systemctl show docker -p ExecStart

docker info reports the daemon’s effective configuration. Check ExecStart too: daemon flags are often set in the systemd unit rather than the JSON file, and a -H tcp://... hiding there is the single worst thing in this cluster.

Terminal window
sudo systemctl reload docker # picks up most daemon.json changes
sudo systemctl restart docker # needed for some, and restarts containers

Most container-level settings here are per-container flags, not daemon settings — so they apply on the next docker run, and existing containers keep their original settings until recreated. A hardened docker-compose.yml does nothing until you up -d --force-recreate.