Chapter 10: Security
Topic 65

Rootless Docker and User Namespaces

RootlessUser namespace

Every control so far hardened the container. This one hardens the daemon. By default dockerd runs as root (Chapter 1, topic 05), so the daemon itself is a root-owned service and container root is host root. Rootless Docker runs the daemon and the containers it starts as an unprivileged user, and user-namespace remapping maps container UID 0 to an unprivileged host UID — so a container escape through that mapping lands as an unprivileged user, not root.

This is the last layer in the stack. The previous topics made driftwood/web run as a non-root user with no capabilities on a read-only filesystem, but all of that still sat on top of a root-owned daemon, and a daemon compromise or a deep escape was still a path to host root. Rootless mode removes that premise: a container escape no longer lands on a root-owned daemon, because there is no host root in the chain to begin with — the blast radius is the unprivileged account that started Docker. (A kernel-level exploit can still bypass any container boundary; rootless shrinks the damage, it does not abolish it.)

The Daemon Runs as Root by Default

dockerd, and every container it starts, runs with host root authority by default (Chapter 1, topic 05). That is why docker.sock is root-equivalent — anyone who can talk to the socket can start a container that mounts the host and reads or writes anything — and why a container escape is a root escape. The whole threat model of this chapter rests on that one default, and rootless mode is what finally removes it.

Rootless Docker

In rootless mode the daemon, containerd, and runc all run as a normal unprivileged user, not root. The containers that daemon launches are owned by that user. A compromise of the daemon, or an escape from a container, is bounded to that one unprivileged account instead of host root — the attacker lands as the user who started Docker, with whatever that user can do and nothing more. The root-owned socket and the root daemon, the two things that made escapes catastrophic, are simply not there.

User-Namespace Remapping

The user namespace maps the container's UID range onto a different host range, so container UID 0 becomes some high, unprivileged host UID — 100000 is the typical base. A process that believes it is root inside the container has no privileges at all on the host; its "root" is host UID 100000, which owns nothing important. This is the mechanism that makes "container root" safe, and it is precisely the namespace that was off back in topic 60, where container UID 0 was host UID 0. Turning it on is what severs that identity.

User-namespace remapping severs container root from host root
container UID 0
mapped to host UID 100000+
escape lands unprivileged

The Limitations

Rootless is not a free swap. It can't bind host ports below 1024 without extra configuration — the same privileged-port constraint the non-root app user hit in topic 60, now applied to the whole daemon. Some storage drivers and networking features are restricted or slower, and a few capability-dependent workloads simply won't run. You trade some convenience and a handful of edge features for a much smaller blast radius, which is a good trade on hosts where a root-owned daemon is the unacceptable risk.

Run driftwood/web fully hardened under a rootless daemon
# rootless dockerd already runs as your unprivileged user;
# the per-container hardening from this chapter still applies
docker run -d --name web \
  --user app \
  --cap-drop=ALL \
  --read-only --tmpfs /tmp \
  --security-opt no-new-privileges \
  -p 8000:8000 \
  driftwood/web

# publish a high port; rootless can't bind below 1024 by default,
# so the proxy on the host holds 80/443 in front of this

Nothing about the container flags changes under rootless Docker — the non-root user, dropped capabilities, read-only rootfs, and no-new-privileges are identical. What changes is underneath: the daemon serving this run is an unprivileged process, so the bottom of the stack is no longer root either. Driftwood is now hardened from the application down to the daemon.

Podman Is Rootless by Default

Podman runs rootless and daemonless out of the box — the full treatment is Chapter 12, topic 74. The posture that rootless Docker opts into is Podman's starting point: each podman run forks the container as a child of your shell, with no root-owned daemon anywhere. The trade-offs are the same, because both rest on the same user-namespace kernel feature — the low-port restriction and the storage-driver limits apply to Podman exactly as they do to rootless Docker. If you want rootless as the default rather than an opt-in, Podman is the tool that starts there.

Common Mistakes
  • Assuming a hardened container (non-root, dropped caps, read-only) is enough while the daemon still runs as root — the root-owned daemon and docker.sock remain the host-root path that only rootless mode closes.
  • Expecting rootless Docker to bind port 80 with no extra steps — it can't bind below 1024 by default; publish a high port behind a proxy or apply the documented workaround, the same constraint as the non-root user in topic 60.
  • Enabling user-namespace remapping and being surprised that existing named volumes have the wrong ownership — the remapped UID range doesn't match files created under the old root daemon, so plan the migration.
  • Treating rootless as a drop-in for every workload — some storage drivers, networking modes, and privileged workloads don't work rootless, so verify the stack before switching production hosts.
Best Practices
  • Run rootless Docker (or Podman) on hosts where a root-owned daemon is an unacceptable risk, so a container escape lands as an unprivileged user rather than root.
  • Enable user-namespace remapping so container UID 0 maps to an unprivileged host UID and "container root" is no longer host root.
  • Plan for the low-port and storage-driver limitations before migrating — publish high ports behind a proxy and confirm the storage and network features the workload needs are supported rootless.
  • Reach for Podman when you want rootless and daemonless as the default rather than an opt-in (Chapter 12, topic 74), accepting the same user-namespace trade-offs.
Comparable tools Podman is rootless and daemonless by default — the natural comparison (Chapter 12, topic 74) nerdctl rootless · LXC unprivileged rest on the same user-namespace kernel feature Kubernetes expresses the per-container half with runAsNonRoot and user-namespace support — no direct equivalent for daemon-rootless

Knowledge Check

What does rootless Docker move off root that the earlier controls did not?

  • The daemon itself — dockerd, containerd, and runc run as an unprivileged user, not just the container process
  • Only the container's single main process, which is exactly what the USER app instruction in the Dockerfile already did for us back in topic 60
  • The container's full set of Linux capabilities, which the rootless mode strips for you automatically at launch
  • The host kernel itself, swapping it out for a separate unprivileged kernel running per container

How does user-namespace remapping make "container root" safe?

  • It maps container UID 0 to a high, unprivileged host UID, so a process that is root inside has no host privileges
  • It outright blocks any process from ever becoming root inside the container, even when the image asks for it
  • It filters the exact syscalls root may make, so that root inside the container simply can't do anything dangerous
  • It transparently encrypts the container's whole filesystem so that root inside can never read the plaintext

What is a real limitation of running rootless?

  • It can't bind host ports below 1024 by default, and some storage drivers and certain workloads are restricted
  • It quietly removes namespace isolation between containers, so every container can see and signal the others' processes
  • It requires rebuilding every image into a special rootless-only format before any of them will run unprivileged
  • It silently disables --cap-drop and --read-only, so containers running rootless can no longer be hardened

Why does Podman start from the posture rootless Docker has to opt into?

  • It runs rootless and daemonless by default, forking each container as a child of your shell with no root-owned daemon
  • It relies on a different kernel feature entirely, one that sidesteps the low-port restriction and the storage-driver limits
  • It runs a proprietary, Podman-only image format that happens to be rootless by construction from the start
  • It runs a second hardened root daemon that supervises the first one for safety and restarts it on failure

You got correct