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.
| Namespace | Isolates | Security relevance |
|---|---|---|
| PID | Process IDs | Contained process cannot see or signal host processes; its init is PID 1 inside |
| Mount (mnt) | Filesystem mount points | Private root filesystem; host paths simply are not mounted |
| Network (net) | Interfaces, routes, firewall, sockets | Own loopback and interfaces; no access to host sockets |
| UTS | Hostname and domain name | Container has its own hostname |
| IPC | System V IPC, POSIX message queues | No shared shared-memory segments with the host |
| User (userns) | UID/GID mappings | Root inside maps to unprivileged outside: the basis of rootless containers |
| Cgroup | The cgroup root the process sees | Hides the host’s cgroup layout from the container |
| Time | Boot and monotonic clock offsets | Independent 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
- 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. - Remember host root is outside every namespace. Namespaces isolate contained processes from each other and from the host, not the host from the containers.
- Prefer user namespaces (rootless containers) for untrusted workloads so an escape yields an unprivileged UID, not host root.
- Use
lsnsandnsenterto debug containers from the host without installing tools inside them; it is faster and does not enlarge the container’s attack surface. - Keep unprivileged user namespaces enabled but restricted (as Ubuntu does with AppArmor) rather than disabling them outright, unless a specific threat model demands it.