Skip to main content
RunBook Academy

Docker & ContainersXI Β· Container & Host SecurityUser namespaces

User namespaces and rootless Docker (security perspective)

Advanced⏱ ~30 min

What you'll learn

  • Apply user namespaces on a rootful daemon
  • Recognise which workloads break under rootless
  • Decide between rootful+userns-remap and rootless
  • Price the operational cost before enabling it, not after

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-09

Not yet marked complete on this device.

Every other control in this part narrows what container root may do. User namespaces change what container root is. UID 0 inside the container becomes an unprivileged UID on the host, and the kernel enforces that translation on every filesystem operation, every signal, and every capability check against a host resource.

That is a stronger guarantee than a capability set, because it does not depend on the container failing to regain capabilities. It is also, by a wide margin, the most operationally expensive control on this list β€” and this lesson spends as much time on the cost as on the benefit, because the cost is what stops teams deploying it.

The configuration

Configuration changesubordinate ID ranges
# /etc/subuid
dockremap:100000:65536

# /etc/subgid
dockremap:100000:65536
// /etc/docker/daemon.json
{
  "userns-remap": "default"
}

"default" tells the daemon to create and use a user and group called dockremap, and to allocate the ranges itself if /etc/subuid and /etc/subgid do not already have an entry. You can also name an existing user ("myuser"), a user:group pair, or numeric IDs.

After a daemon restart, container UID 0 is host UID 100000, container UID 1000 is host UID 101000, and container UID 65536 β€” outside the range β€” is not mapped at all and appears as nobody.

Service impact possibleapply it
sudo systemctl restart docker
docker info --format 'userns: {{json .SecurityOptions}}'
ls -ld /var/lib/docker/100000.100000

Verify it from the kernel

Read-only / Safethe mapping the kernel installed
$ PID=$(docker inspect --format '{{.State.Pid}}' web); cat "/proc/$PID/uid_map"
         0     100000      65536

Illustrative output

Read-only / Safewho the host thinks is running
$ PID=$(docker inspect --format '{{.State.Pid}}' web); ps -o pid,user,uid,comm -p "$PID"; docker exec web id -u
    PID USER       UID COMMAND
31840 100000  100000 nginx
0

Illustrative output

id -u returns 0 inside and 100000 outside. Those two lines together are the proof; either alone is not. An empty uid_map β€” or one reading 0 0 4294967295 β€” means the container is not remapped, which is what a --userns=host container looks like.

What it costs

This is the part that gets skipped, and it is why most production estates do not run userns-remap.

It is all-or-nothing, per daemon

There is no per-container opt-in. Enabling remap in daemon.json remaps every container on the host. The only per container control is --userns=host, which opts a single container back out. One workload that cannot tolerate remapping therefore does not force you off the feature β€” but it does mean that workload runs with the old boundary while everything around it is protected, and the audit needs to know which ones.

The storage root moves

Bind-mount ownership stops matching

This is the failure you will actually hit day to day.

A host file owned by UID 0, mode 0600, bind-mounted into a container: the container’s root is host UID 100000, which is not the owner, so the read fails with Permission denied. Ownership looks correct in ls -l on the host and correct in ls -l inside the container, and it still fails β€” because the two 0s are different numbers.

Read-only / Safesee the real owner
$ ls -n /srv/api/conf/secret.yaml
-rw------- 1 0 0 812 Aug  9 11:04 /srv/api/conf/secret.yaml

Illustrative output

Configuration changefix the ownership to match the mapping
SUBUID_BASE=100000
sudo chown "$SUBUID_BASE:$SUBUID_BASE" /srv/api/conf/secret.yaml
ls -n /srv/api/conf/secret.yaml

Named volumes do not have this problem: Docker creates them under the namespaced storage root with the mapped ownership already correct. Preferring named volumes over bind mounts is the single change that makes userns-remap liveable.

The documented incompatibilities

Docker publishes the list, and it is short but load-bearing:

Not supported with userns-remapWhy it matters
--pid=hostmonitoring agents and process exporters
--network=hostanything relying on host networking
--privileged without --userns=hostDinD, storage agents
storage/volume drivers unaware of ID mappingssome CSI and cloud volume plugins
mknod inside the containerimages that create device nodes at start

Notice how many of those are the infrastructure containers rather than the applications. That is the usual shape of a rollout: the applications remap cleanly, and the node exporter, the log shipper and the CI runner each need --userns=host β€” which is a defensible outcome as long as the exception list is written down and reviewed, rather than growing quietly.

What it does not fix

Being precise here is worth more than enthusiasm:

  • Kernel vulnerabilities. The mapping is enforced by the kernel; a bug in the kernel is not constrained by it. Worse, user namespaces expand the reachable kernel surface, because code paths that previously required real root become reachable by an unprivileged process holding capabilities inside a namespace. Several container escapes over the years have gone through exactly that door. user.max_user_namespaces and, on Debian-family hosts, kernel.unprivileged_userns_clone exist to bound it.
  • Anything outside the filesystem and process model. A bind-mounted secret that the mapped UID can read is still read. A --network=host container still sees the host network.
  • Resource exhaustion. cgroup limits are a separate mechanism; a remapped container can still fork-bomb its cgroup.

The socket case is the pleasant surprise: with remapping on, a container’s mapped UID does not own /var/run/docker.sock and cannot connect to it. That is a genuine, free mitigation for the worst mount in the catalogue β€” and it also means CI tooling that depended on the socket stops working, which is the same fact seen from the other side.

Rootless, and how to choose

Rootless Docker moves the whole daemon inside a user namespace rather than just the containers. The daemon itself runs as an unprivileged user, so compromising dockerd does not yield host root either.

rootful + userns-remaprootless
Daemon runs asrootunprivileged user
Container root maps tounprivileged host UIDunprivileged host UID
Compromising the daemon giveshost rootthat user
Networkingnormal, iptables-manageduser-mode stack via RootlessKit; slower for high packet rates
Privileged portsworksneeds net.ipv4.ip_unprivileged_port_start or a helper
cgroup limitsfullneeds cgroup v2 with systemd delegation
Multi-tenant isolationone shared daemonone daemon per user

For multi-tenant CI runners and shared development hosts, rootless is the right default β€” one daemon per user is a real isolation boundary that userns-remap cannot provide. For single-tenant production hosts, rootful with userns-remap keeps normal networking and normal cgroup behaviour, and is easier to operate.

Knowledge check

Knowledge check Β· 5 questions

  1. Q1. You enable "userns-remap" in daemon.json on a busy host and restart the daemon. What do you expect to see?

  2. Q2. With `dockremap:100000:65536`, a bind-mounted host file owned by UID 0 mode 0600 cannot be read by a container running as root. Why?

  3. Q3. Which of these are documented as incompatible with userns-remap? Select all that apply.

  4. Q4. User namespaces expand the kernel attack surface reachable from a container, even though they reduce the consequences of a successful escape.

  5. Q5. Which file on the host proves that a specific container is actually remapped, and what does its content look like?

Passing score: 75%. Answers are checked in this browser.