Processes & Privileges

Namespaces

PID, mount, network, and user namespaces: the isolation behind containers.

Capabilities and seccomp limit what a process may do. Namespaces limit what a process may see. A process in its own PID namespace cannot even name the processes outside it, let alone signal them; a process in its own network namespace sees a different set of interfaces and routes. This “you cannot attack what you cannot perceive” property is the core of container isolation. Every container you have ever run is, at bottom, a normal process placed into a fresh set of namespaces. Understanding them directly, without a runtime in the way, is what lets you reason about where the isolation boundary actually is and where it leaks.

The namespace types

The kernel provides several namespace types, each isolating one kind of global resource. A container typically uses most of them at once.

NamespaceIsolatesSecurity relevance
PIDProcess IDsContained process cannot see or signal host processes; its init is PID 1 inside
Mount (mnt)Filesystem mount pointsPrivate root filesystem; host paths simply are not mounted
Network (net)Interfaces, routes, firewall, socketsOwn loopback and interfaces; no access to host sockets
UTSHostname and domain nameContainer has its own hostname
IPCSystem V IPC, POSIX message queuesNo shared shared-memory segments with the host
User (userns)UID/GID mappingsRoot inside maps to unprivileged outside: the basis of rootless containers
CgroupThe cgroup root the process seesHides the host’s cgroup layout from the container
TimeBoot and monotonic clock offsetsIndependent clock offset (rarely used for security)

The user namespace is the special one. Inside it a process can appear to be root (UID 0) while the kernel maps that to an unprivileged UID on the host. Its capabilities are real within the namespace but meaningless outside it, which is what makes unprivileged and rootless containers possible.

Hands-on: build isolation by hand

unshare creates new namespaces and runs a command inside them. Start with a UTS namespace so a hostname change stays contained.

# Change the hostname only inside a new UTS namespace
sudo unshare --uts sh -c 'hostname contained && hostname'
hostname

Expected output:

contained
lab-vm

The command reported contained, but the host’s hostname is still lab-vm: the change never escaped the namespace. Now combine several namespaces to approximate a container, using a user namespace so no real root is involved.

# A rootless mini-container: new user, PID, mount, and UTS namespaces
unshare --user --map-root-user --pid --mount --uts --fork \
  sh -c 'mount -t proc proc /proc; echo "inside uid=$(id -u)"; ps -e'

Expected output:

inside uid=0
    PID TTY          TIME CMD
      1 pts/0    00:00:00 sh
      6 pts/0    00:00:00 ps

Two things stand out. You appear to be UID 0 (--map-root-user) even though you launched this as an ordinary user, and ps shows only two processes because the new PID namespace hides everything on the host. The private /proc mount is what makes ps report the namespaced view rather than the host’s.

Inspecting and entering namespaces

lsns lists namespaces and their members; nsenter joins an existing one, which is how docker exec and kubectl exec work under the hood.

# List the network namespaces on the host
sudo lsns -t net

Expected output:

        NS TYPE NPROCS   PID USER   NETNSID NSFS                           COMMAND
4026531840 net     214     1 root unassigned                               /sbin/init
4026532210 net       3  4471 root         0 /run/docker/netns/abc123def456 nginx

Two network namespaces exist: the host’s (init, PID 1) and a container’s (nginx, PID 4471). To debug that container’s network without any tooling inside it, enter its namespace from the host:

# Run host tools inside the container's network namespace
sudo nsenter -t 4471 -n ip addr show

Expected output:

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 ...
    inet 127.0.0.1/8 scope host lo
12: eth0@if13: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    inet 172.17.0.2/24 brd 172.17.0.255 scope global eth0

You are now seeing the container’s private interfaces and its 172.17.0.2 address, using the host’s ip binary. This is invaluable for debugging and is also exactly the move an attacker with host root would make to reach into a container, which is why host root remains outside every namespace boundary.

User namespaces and rootless containers

The user namespace is what lets an unprivileged user run containers safely. Because “root” inside maps to a normal UID outside, a container escape lands the attacker as an unprivileged user on the host rather than as real root. This is the model behind rootless Podman and rootless Docker. The trade-off is that user namespaces have historically been a source of kernel escalation bugs (unprivileged users gaining CAP_SYS_ADMIN inside a namespace and reaching newly exposed kernel code paths), so some hardened distros restrict them. On Ubuntu you can check the current policy:

sysctl kernel.unprivileged_userns_clone kernel.apparmor_restrict_unprivileged_userns

Expected output:

kernel.unprivileged_userns_clone = 1
kernel.apparmor_restrict_unprivileged_userns = 1

Here unprivileged user namespaces are permitted but AppArmor restricts what they can do, which is Ubuntu 24.04’s default posture. The Mandatory Access Control group covers that AppArmor layer, and the Kubernetes Security foundation revisits namespaces as the isolation each pod is built from.

Practical Guidance

  1. Reason about isolation namespace by namespace. Ask which namespaces a workload actually has; a container sharing the host PID or network namespace (hostPID, hostNetwork) has given up a large part of its isolation.
  2. Remember host root is outside every namespace. Namespaces isolate contained processes from each other and from the host, not the host from the containers.
  3. Prefer user namespaces (rootless containers) for untrusted workloads so an escape yields an unprivileged UID, not host root.
  4. Use lsns and nsenter to debug containers from the host without installing tools inside them; it is faster and does not enlarge the container’s attack surface.
  5. Keep unprivileged user namespaces enabled but restricted (as Ubuntu does with AppArmor) rather than disabling them outright, unless a specific threat model demands it.