$RodHat_
Console Tips

Your container is six clone(2) flags. nsenter gets you back in.

Published by

A classic MS-DOS terminal screen displayed on a laptop keyboard with vivid illumination.
Photo: Rafael Minguet Delgado / Pexels

Most people learned about Linux namespaces through Docker. The actual order of operations: namespaces shipped in the Linux kernel years before Docker existed, clone(2) already had the flags, and Docker is what made the whole thing exploitable for a press release. Six namespace types — mount, PID, network, UTS, IPC, and user — are what separate your container from the host. Not virtualization. Not a hypervisor. Not magic. Six flags to one syscall.

nsenter is the tool that walks back through those flags from the outside. It’s been in util-linux since 2.23. You already have it.

Find the PID first

nsenter needs a PID in the host’s PID namespace to anchor against. With Docker:

pid=$(docker inspect --format '{{.State.Pid}}' mycontainer)

Without Docker:

pid=$(pgrep -f my-app-binary)

You need the PID as the kernel sees it from the host, not the PID-1 the container thinks it is. Those are different numbers. The container’s PID namespace remaps its processes to start from 1; the host still knows the real numbers. They live in /proc.

Enter one namespace, not all of them

The most useful form isn’t --all. It’s picking the namespace you actually need:

# Container's network stack, host's tcpdump
sudo nsenter -t "$pid" --net -- tcpdump -i eth0 -n

# See the container's routing table and interfaces
sudo nsenter -t "$pid" --net -- ip route
sudo nsenter -t "$pid" --net -- ss -tulpn

You’re inside the container’s network namespace but running the host’s binaries. The container doesn’t need tcpdump installed. It doesn’t need any tools installed. You brought your own.

Same logic for IPC namespaces — if you need to inspect shared memory or semaphores:

sudo nsenter -t "$pid" --ipc -- ipcs

For the container’s PID namespace — see its process tree from its own perspective, where its init is PID 1:

sudo nsenter -t "$pid" --pid -- ps auxf

The “no shell in the image” case

Someone shipped a distroless container. The image has the application binary and nothing else. No shell, no ls, no curl. docker exec drops you into nothing useful.

# Container's PID + network + IPC + UTS namespaces, host's /bin/sh
sudo nsenter -t "$pid" --pid --net --ipc --uts -- /bin/sh

Notice what’s missing: --mount. Without the mount namespace, you’re running against the host’s filesystem. Your /bin/sh is the host’s. So is your strace, your lsof, your ss. You can see the container’s process list, its network interfaces, its IPC objects, and its hostname. You cannot see the container’s filesystem layout.

That’s almost always the right tradeoff when you’re debugging a wedged process — you want your tools, and you can get the container’s filesystem another way:

ls /proc/"$pid"/root/
cat /proc/"$pid"/root/etc/passwd

The kernel exposes the container’s root as /proc/<pid>/root. No mounting required. You can grep through its configs, read its logs, inspect its state — all from the host.

What /proc/PID/ns/ actually is

ls -la /proc/"$pid"/ns/

You’ll see symlinks like:

cgroup -> cgroup:[4026531835]
ipc    -> ipc:[4026532318]
mnt    -> mnt:[4026532316]
net    -> net:[4026532321]
pid    -> pid:[4026532319]
uts    -> uts:[4026532317]

The numbers in brackets are inodes in a kernel pseudo-filesystem. Same inode, same namespace. If two containers have the same net: inode, they share a network stack — intentional pod networking or an ops mistake, depending on what you were trying to do.

nsenter opens these symlinks and calls setns(2). You can do it manually:

# Hold a reference to a namespace after its processes exit
exec 9</proc/"$pid"/ns/net

# Enter it by fd — the namespace outlives the container
sudo nsenter --net=/proc/self/fd/9 -- ip addr

This keeps a namespace alive for forensics. The namespace object persists as long as any fd to it stays open, even after every process in it has exited. Useful when something dies and you need to examine the state it left behind without racing against the runtime cleaning up.

unshare: make your own

nsenter enters an existing namespace. unshare creates a fresh one:

# Isolated network namespace — loopback only, no host interfaces visible
sudo unshare --net /bin/sh
ip addr  # just lo

This is the right tool for testing: you want to see what your application does with no network, with an empty routing table, or when specific interfaces don’t exist. No VMs, no Docker daemon, no docker-compose.yml you’ll have to delete later. One command, one fork.

# The six-flag isolation Docker approximates
sudo unshare --mount --pid --net --uts --ipc --fork /bin/sh

That shell runs in six fresh namespaces. The host is unchanged on the other side of those CLONE_NEW* flags. You can break everything in there and exit back to reality. Useful for testing init scripts, service startup sequences, or firewall rules without touching production state. For the BSD equivalent of mount and process isolation without the namespace model, jails get there differently — same goal, older design.

The limits

User namespaces (CLONE_NEWUSER) are the complicated one. If the container was launched with a user namespace — where uid 0 inside maps to some unprivileged uid on the host — nsenter --user handles entry. Most production containers skip user namespaces because rootless containers trade certain isolation properties for the privilege reduction, and the tradeoffs are irritating and shop-specific.

nsenter requires root, or CAP_SYS_ADMIN, for most namespace types. Same restriction as ptrace: the kernel doesn’t let unprivileged processes reach into someone else’s namespace. If you’re in a situation where you need this and don’t have root, that’s a different problem than nsenter solves.

And if you want the full clone(2), setns(2), unshare(2) treatment — what each flag actually does to the kernel’s view of the process, how user namespaces interact with capability sets, why CLONE_NEWPID doesn’t fully take effect until the first fork() — Kerrisk’s The Linux Programming Interface has the definitive chapter on it. Everything above is the operational abbreviation.