Skip to main content
RunBook Academy

LinuxLXXVIII · Containers from the Linux PerspectiveNamespaces

User namespaces and UID mapping - the kernel side of rootless

Advanced⏱ ~18 minutil-linuxlibcap2-bin

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

Not yet marked complete on this device.

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:

Read-only / Safe
$ cat /proc/self/uid_map
         0          0 4294967295

Three 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:

Read-only / Safe
$ unshare --user id
uid=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:

Read-only / Safe
$ cat /etc/subuid
ebrandi:100000:65536

That 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 nobody inside - 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:

  1. Change the ownership on the host to a UID that is mapped. Crude, and it makes the directory unusable by the host user.

  2. 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.

  3. 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/data

    The X-mount.idmap option 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 mount documents 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:

Read-only / Safe
$ unshare --map-root-user id
unshare: write failed /proc/self/uid_map: Operation not permitted

The namespace was created. The map write was denied. Look at why:

Read-only / Safe
$ sysctl kernel.apparmor_restrict_unprivileged_userns
kernel.apparmor_restrict_unprivileged_userns = 1
Read-only / Safe
$ unshare --user grep CapEff /proc/self/status
CapEff:	0000000000000000

An 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

  1. Q1. Inside a user namespace, a bind-mounted directory shows as owned by nobody:nogroup and chown fails with "Invalid argument". What is happening?

  2. Q2. Which are required for a rootless runtime to map a whole range of UIDs into a container? Select all that apply.

  3. Q3. A process that holds CAP_SYS_MODULE inside a user namespace can load a kernel module.

  4. 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.