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
- Severity: critical
- Severity: high
- Severity: high
- Severity: medium
- Severity: medium
- Severity: medium
- Severity: low
The Docker group is root
Section titled “The Docker group is root”Worth stating separately because it surprises people who have been running
docker commands for years:
sudo usermod -aG docker aliceThat 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.
Podman is already installed on RHEL
Section titled “Podman is already installed on RHEL”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.
Versions
Section titled “Versions”docker versionrunc --versionDocker 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.
Where the config lives
Section titled “Where the config lives”/etc/docker/daemon.json — which frequently does not exist, because Docker runs
fine without it. Its absence is normal, not a finding.
docker infosudo systemctl show docker -p ExecStartdocker 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.
Reloading
Section titled “Reloading”sudo systemctl reload docker # picks up most daemon.json changessudo systemctl restart docker # needed for some, and restarts containersMost 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.