LinuxLXXVIII · Containers from the Linux PerspectiveNamespaces
User namespaces and UID mapping - the kernel side of rootless
What you'll learn
- Read and write a UID mapping and predict the resulting ownership
- Explain the role of subuid ranges and the newuidmap helpers
- Diagnose files that appear owned by nobody inside a namespace
- Recognise the AppArmor restriction on unprivileged user namespaces
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11
Of the namespace types, the user namespace is the one that changes the security model rather than the view. The others give a process a private copy of something. This one changes what its credentials mean.
Without it, a container process running as UID 0 is UID 0 to the kernel. It is constrained root - the runtime drops capabilities, applies seccomp and loads an LSM profile - but the kernel sees root, so a bug that escapes the container escapes as root. A user namespace changes the arithmetic: UID 0 inside maps to an unprivileged UID outside, and an escape lands as a user who owns nothing.
The mapping
Every process belongs to a user namespace, and every user namespace has a UID map and a GID map. The initial namespace has the trivial one:
$ cat /proc/self/uid_map 0 0 4294967295Three columns: first ID inside, first ID outside, count. Read that line as “IDs 0 through 4294967294 inside map to the same values outside” - which is the identity mapping, because this is the root namespace.
Create a new namespace and the map starts empty:
unshare --user cat /proc/self/uid_map
That prints nothing. An empty map means no ID inside corresponds to any ID outside, and the kernel reports every unmapped ID as the overflow ID:
$ unshare --user iduid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)65534 is not a special container value; it is
/proc/sys/kernel/overflowuid, the value the kernel substitutes
when it has to name an identity that does not exist in this
namespace. Every nobody you have ever seen in a container is
this.
Ask unshare to write a map and the picture changes. On a host
that permits unprivileged user namespaces - see the restriction
section below, because many now do not:
unshare --map-root-user id
# uid=0(root) gid=0(root) groups=0(root)
unshare --map-root-user cat /proc/self/uid_map
# 0 1000 1
You are root inside and UID 1000 outside. One ID is mapped;
everything else is still nobody.
What an unprivileged process may map
The rules exist because an unmapped range would be a privilege escalation, and they are strict.
An unprivileged process writing its own uid_map may write
exactly one line, and that line may map only its own
effective UID. That is what --map-root-user does. If you want
a range - which any container needs, because the image expects
dozens of UIDs to exist - you need help.
The help is newuidmap and newgidmap, small setuid binaries
from the uidmap package. They will write a multi-line map on
behalf of an unprivileged process, but only within the ranges
delegated to that user in /etc/subuid and /etc/subgid:
$ cat /etc/subuidebrandi:100000:65536That line delegates 65536 consecutive host UIDs, starting at
100000, to the user ebrandi. A rootless container runtime then
builds a map like:
# inside outside count
0 1000 1
1 100000 65535
Container UID 0 is the invoking user; container UIDs 1 through 65535 come out of the delegated block. Nothing in that block belongs to a real account, which is the point.
unshare can express this directly once the ranges exist:
unshare --user --map-users 1:100000:65535 --map-groups 1:100000:65535 \
--map-root-user --mount-proc --pid --fork bash
Ownership arithmetic, and the nobody problem
Ownership on disk is stored as a host UID. The kernel translates it through the map on the way in and on the way out. So:
- A file owned by host UID 100000, in a namespace mapping
1 -> 100000, appears as UID 1 inside. - A file owned by host UID 1000, in a namespace where 1000 is
not mapped, appears as
nobodyinside - and a write is refused, because a process cannot act on an identity the namespace cannot name.
This is the whole explanation for the most common rootless
container complaint. A bind mount of a host directory owned by
your user shows up inside the container as nobody:nogroup, and
chown inside the container fails:
chown: changing ownership of '/data': Invalid argument
EINVAL rather than EPERM, because the target UID does not
exist in the namespace. There is no permission to grant; the
identity is not expressible.
Three real answers, in increasing order of quality:
-
Change the ownership on the host to a UID that is mapped. Crude, and it makes the directory unusable by the host user.
-
Let the runtime chown the volume into the mapped range on first use. Works, but it rewrites every inode and it is slow on a large tree.
-
Use an idmapped mount. The kernel applies a translation at the mount, so the same files present different ownership through that mount without touching the disk:
sudo mount --bind -o X-mount.idmap=u:0:100000:65536 /srv/data /mnt/dataThe
X-mount.idmapoption also accepts a user namespace directly -X-mount.idmap=/proc/PID/ns/user- which attaches the mapping of a running container. It needs a recent kernel and a filesystem that supports idmapped mounts;man 8 mountdocuments the exact syntax on your system.
Capabilities inside a user namespace
When a process creates a user namespace, the kernel grants it a
full capability set within that namespace. That is what
makes rootless containers possible: CAP_SYS_ADMIN inside the
namespace is enough to create a mount namespace and mount a
filesystem, without being root on the host.
Those capabilities are meaningless outside. CAP_SYS_MODULE
inside a user namespace does not let you load a module, because
the module subsystem is not namespaced and the check is made
against the initial namespace. The rule of thumb: a capability
works on a resource the namespace owns, and does nothing on a
resource it does not.
The restriction that breaks rootless tooling
Unprivileged user namespaces have been a productive source of kernel vulnerabilities, because they let an unprivileged user reach code paths that previously required root. Distributions have responded by restricting them, and the restriction is now the default on recent Ubuntu.
On such a host, an unprivileged unshare --user still succeeds -
and then everything inside it fails:
$ unshare --map-root-user idunshare: write failed /proc/self/uid_map: Operation not permittedThe namespace was created. The map write was denied. Look at why:
$ sysctl kernel.apparmor_restrict_unprivileged_usernskernel.apparmor_restrict_unprivileged_userns = 1$ unshare --user grep CapEff /proc/self/statusCapEff: 0000000000000000An unrestricted user namespace grants a full capability set to its creator. Here the effective set is empty, because AppArmor transitioned the process into a special profile on namespace creation:
# /etc/apparmor.d/unprivileged_userns
profile unprivileged_userns {
audit deny capability,
audit deny change_profile,
...
allow userns,
}
audit deny capability is the line doing it. The namespace
exists and is inert.
What user namespaces do and do not buy
They convert a container escape from a root compromise into an unprivileged-user compromise. That is a large improvement and it is the reason to enable them.
They do not make the kernel smaller. The escaped process still talks to the same kernel through the same syscall interface, and a kernel vulnerability that does not check namespace ownership is exploitable from inside. User namespaces are one layer, alongside seccomp, capability dropping and an LSM profile - and, for genuinely hostile workloads, alongside a kernel boundary that a namespace cannot provide.
Knowledge check
Knowledge check · 4 questions
Q1. Inside a user namespace, a bind-mounted directory shows as owned by nobody:nogroup and chown fails with "Invalid argument". What is happening?
Q2. Which are required for a rootless runtime to map a whole range of UIDs into a container? Select all that apply.
Q3. A process that holds CAP_SYS_MODULE inside a user namespace can load a kernel module.
Q4. After an Ubuntu upgrade, rootless Podman and a browser sandbox both fail to start on a fleet of workstations. unshare --user succeeds but writing uid_map returns EPERM. What is the cause and what are the options?
Passing score: 75%. Answers are checked in this browser.