Docker & ContainersXI Β· Container & Host SecurityUser namespaces
User namespaces and rootless Docker (security perspective)
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
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
# /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.
sudo systemctl restart docker
docker info --format 'userns: {{json .SecurityOptions}}'
ls -ld /var/lib/docker/100000.100000Verify it from the kernel
$ PID=$(docker inspect --format '{{.State.Pid}}' web); cat "/proc/$PID/uid_map" 0 100000 65536Illustrative output
$ 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
0Illustrative 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.
$ ls -n /srv/api/conf/secret.yaml-rw------- 1 0 0 812 Aug 9 11:04 /srv/api/conf/secret.yamlIllustrative output
SUBUID_BASE=100000
sudo chown "$SUBUID_BASE:$SUBUID_BASE" /srv/api/conf/secret.yaml
ls -n /srv/api/conf/secret.yamlNamed 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-remap | Why it matters |
|---|---|
--pid=host | monitoring agents and process exporters |
--network=host | anything relying on host networking |
--privileged without --userns=host | DinD, storage agents |
| storage/volume drivers unaware of ID mappings | some CSI and cloud volume plugins |
mknod inside the container | images 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_namespacesand, on Debian-family hosts,kernel.unprivileged_userns_cloneexist 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=hostcontainer 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-remap | rootless | |
|---|---|---|
| Daemon runs as | root | unprivileged user |
| Container root maps to | unprivileged host UID | unprivileged host UID |
| Compromising the daemon gives | host root | that user |
| Networking | normal, iptables-managed | user-mode stack via RootlessKit; slower for high packet rates |
| Privileged ports | works | needs net.ipv4.ip_unprivileged_port_start or a helper |
| cgroup limits | full | needs cgroup v2 with systemd delegation |
| Multi-tenant isolation | one shared daemon | one 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
Q1. You enable "userns-remap" in daemon.json on a busy host and restart the daemon. What do you expect to see?
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?
Q3. Which of these are documented as incompatible with userns-remap? Select all that apply.
Q4. User namespaces expand the kernel attack surface reachable from a container, even though they reduce the consequences of a successful escape.
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.