all posts

User namespaces, explained for sandboxing: what root-in-a-namespace actually buys you

Ajay Kumar··9 min read

A user namespace lets a process look at its own credentials, see uid 0, and be completely wrong about what that means. Inside the namespace it is root: it owns the id, it holds a full capability set, it can do root-ish things to resources that belong to that namespace. Outside — to the host kernel, to every file on a disk it didn't create — it is the same unprivileged uid it always was. Root inside the namespace is a job title, not a power.

I'm Ajay; I build PandaStack, which runs untrusted and model-generated code in Firecracker microVMs. This post covers what a user namespace actually is, the mechanics you'll get wrong the first time (uid_map, gid_map, and the setgroups step that exists purely because of an old bug), the distro toggles that decide whether you get one at all, what the feature genuinely enables — rootless containers are real and good — and the honest security read. Short version, up front: a user namespace remaps identity. It does not add a boundary. Use it as a hardening layer, never as the wall between you and hostile code.

What a user namespace actually is

Every other namespace type partitions a resource: pid namespaces partition process ids, network namespaces partition interfaces, mount namespaces partition the filesystem view. A user namespace partitions identity. It holds two translation tables — one for user ids, one for group ids — mapping a range of ids inside the namespace onto a range in its parent. That's it: a pair of piecewise-linear functions on integers, plus one rule about capabilities.

Internally the kernel doesn't store "uid 0 in namespace X" — it stores a kuid, a global namespace-independent identity, and translates on the way in and out. If a file's owner has no mapping in your namespace you don't get an error, you get the overflow id, which is why most of the host filesystem shows up as nobody the moment you step inside. Nothing changed about the file. You just lost the vocabulary to name its owner.

The second rule makes the feature useful rather than a curiosity. A process that creates a user namespace receives a full set of capabilities in it — every CAP_ bit, unconditionally. But capabilities are namespace-relative: one is honoured only against a resource owned by the namespace that granted it, or by a descendant. So your brand-new CAP_SYS_ADMIN is real about exactly the things your namespace owns — the mount namespace you create inside it, the network namespace you create inside it, files whose owning uid maps into your range. Against the host's init user namespace it buys nothing.

The mental model: a user namespace is a currency exchange, not a mint. It lets you print as many units of authority as you like, denominated in a currency that only your namespace accepts. Inside the shop, you're rich. The kernel's central bank has never heard of you.

The mechanics: unshare, uid_map, and the setgroups tax

You create one with clone(CLONE_NEWUSER) or unshare(CLONE_NEWUSER) — from a shell, unshare -U. Before any map is written the namespace sits in a strange half-state: no id inside maps to anything, so the process sees the overflow uid for everything including itself. The maps are written afterwards, once, to /proc/<pid>/uid_map and /proc/<pid>/gid_map. The -r flag writes the obvious one for you: your current uid becomes 0 inside.

# ---- OUTSIDE: an ordinary unprivileged user. No sudo anywhere in this post. --
id
# uid=1000(ajay) gid=1000(ajay) groups=1000(ajay)

# ---- INSIDE: -U creates a user namespace, -r maps your uid/gid to 0. --------
unshare -Ur /bin/bash

id
# uid=0(root) gid=0(root) groups=0(root)
#
# Congratulations, you are root. Let's find out what that is worth.

cat /proc/self/uid_map
#          0       1000          1
#   ^ id inside   ^ id outside   ^ range length
#   Read it as: "uid 0 in here IS uid 1000 out there." One line, one uid.

cat /proc/self/status | grep -E '^Cap(Eff|Bnd)'
# CapEff: 000001ffffffffff   <- a FULL capability set. Every bit. Really.
# CapBnd: 000001ffffffffff

# So: root, with all capabilities. Try being root AT something.
cat /etc/shadow
# cat: /etc/shadow: Permission denied
#
# Why: the file is owned by host uid 0, which is NOT in our map. The DAC check
# runs against the GLOBAL ids, where we are still 1000, and CAP_DAC_OVERRIDE is
# only honoured against objects owned by THIS namespace. /etc/shadow isn't.

ls -l /etc/shadow
# -rw-r----- 1 nobody nogroup 1543 ... /etc/shadow
#             ^^^^^^ the overflow id: an owner we can no longer even name.

# What the capabilities ARE good for: resources this namespace owns.
# Add a mount namespace and you can mount inside it, unprivileged:
unshare -Urm /bin/bash -c 'mount -t tmpfs tmpfs /mnt && df -h /mnt | tail -1'
# tmpfs   3.9G  0  3.9G  0% /mnt      <- a real mount, made by a real non-root user

# ...and a network namespace you fully control, also unprivileged:
unshare -Urn /bin/bash -c 'ip link add veth0 type veth peer name veth1; ip -br link'
# lo    DOWN  00:00:00:00:00:00
# veth0 DOWN  ...   <- CAP_NET_ADMIN, honoured, because we own this netns

# Meanwhile, from ANOTHER terminal on the host, the truth:
# $ ps -o pid,user,cmd -C bash
#   PID USER  CMD
# 48213 ajay  /bin/bash       <- still just ajay. The host never believed you.

That's the whole feature in one screen: full capabilities, zero authority over anything you didn't already own, real power over the namespaces you create underneath. The tmpfs mount and the veth pair aren't toys — they used to require root, and an unprivileged user just did them.

Writing the maps by hand, and the setgroups step

unshare -r hides three rules you'll meet the moment you write your own runtime. The maps are write-once: one successful write to uid_map and the mapping is frozen forever. An unprivileged writer may map only a single id, its own effective id; anything richer needs CAP_SETUID in the parent, meaning the setuid helpers newuidmap and newgidmap reading ranges an administrator allocated you in /etc/subuid. And — the one that produces the confusing EPERM — before an unprivileged process may write gid_map at all, it must first write deny to /proc/<pid>/setgroups.

That requirement isn't bureaucracy, it's a scar. Group memberships can subtract access as well as add it — a mode of 0704 denies the group and allows others — so dropping a group can gain you access. Early user namespaces let an unprivileged process shed groups it was never allowed to shed, turning a namespace into a permission upgrade. Remember that next time someone calls user namespaces a clean, purely-additive design: the API has a mandatory step whose only purpose is closing a privilege-escalation hole. It won't be the last time that sentence appears here.

#!/usr/bin/env bash
# What `unshare -Ur` does underneath -- written out so the setgroups step is
# visible. Run as an ordinary user; there is deliberately no sudo here.
set -euo pipefail

# 1. Create the namespace with NO mapping yet, and park a process in it.
unshare --user --fork --pid --mount-proc sleep 300 &
CHILD=$!
sleep 0.2   # let the child actually enter the namespace

# Before any map exists, the child sees the overflow id for EVERYTHING --
# including itself. An unmapped user namespace is a very lonely place.
nsenter --user --target "$CHILD" id 2>/dev/null || true
# uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

# 2. The PARENT (still your normal uid) writes the map. Format is:
#      <first-id-inside> <first-id-outside> <count>
#    ...one range per line. This write is ONE-SHOT: succeed once and the
#    mapping is frozen for the lifetime of the namespace.
echo "0 $(id -u) 1" > "/proc/$CHILD/uid_map"

# 3. THE STEP EVERYONE MISSES. An unprivileged writer must disable setgroups(2)
#    BEFORE writing gid_map, or the gid_map write fails with EPERM. This exists
#    because dropping a group can GRANT access (mode 0704 denies group, allows
#    other), which made early user namespaces a privilege-escalation primitive.
#    It is also one-way: you cannot re-enable it in this namespace.
echo deny > "/proc/$CHILD/setgroups"
echo "0 $(id -g) 1" > "/proc/$CHILD/gid_map"

# 4. Now the child is "root".
nsenter --user --target "$CHILD" id
# uid=0(root) gid=0(root) groups=0(root)

# --- Mapping MORE than one id needs delegated ranges. ------------------------
# An unprivileged process may only ever map its own effective id. A whole
# 65536-id range -- what rootless Docker/Podman need so files inside the
# container can have plausible owners -- requires CAP_SETUID in the parent,
# which in practice means the setuid helpers reading an admin-allocated range:
grep "^$(id -un):" /etc/subuid /etc/subgid
# /etc/subuid:ajay:100000:65536
# /etc/subgid:ajay:100000:65536
#
# newuidmap "$CHILD" 0 "$(id -u)" 1  1 100000 65535
# newgidmap "$CHILD" 0 "$(id -g)" 1  1 100000 65535
#   -> uid 0 inside == you; uids 1..65535 inside == host 100000..165534.
#   Note what this means: "rootless" containers still depend on a privileged
#   setuid binary and a range an administrator handed you in advance.

kill "$CHILD" 2>/dev/null || true

Nesting, ownership, and who owns whom

User namespaces nest, to a bounded depth, each a child of the namespace its creator was in. That parent link is load-bearing: every other namespace object is owned by the user namespace active when it was created, and capability checks are evaluated against that owner. So CAP_NET_ADMIN in a descendant lets you reconfigure a network namespace created inside that descendant, never the host's. Nesting only narrows — a child's map is a subset of its parent's — which is why it's safe to let unprivileged code do this, and why nesting is uninteresting for security: no depth gets you closer to the host.

In real code you rarely shell out to unshare. In Go, the runtime will even do the map-writing dance for you — setgroups deny included — if you fill in the right struct fields.

package main

// Spawn a child in a fresh user namespace (plus the namespaces that a user
// namespace unlocks for unprivileged callers) using clone(2) via Go's
// SysProcAttr. Build and run this as a NORMAL user -- that's the whole point.

import (
	"fmt"
	"os"
	"os/exec"
	"syscall"
)

func main() {
	cmd := exec.Command("/bin/sh", "-c", `
		echo "--- inside ---"
		id
		echo "pid: $$"                       # 1, thanks to CLONE_NEWPID
		mount -t tmpfs tmpfs /mnt && echo "tmpfs mounted by a non-root user"
		ip link add dummy0 type dummy && ip -br link
		cat /etc/shadow 2>&1 | tail -1       # still denied. Always denied.
	`)
	cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr

	cmd.SysProcAttr = &syscall.SysProcAttr{
		// CLONE_NEWUSER is the one that matters: it is what makes the OTHER
		// flags legal for an unprivileged caller. Without it, asking for
		// CLONE_NEWNET or CLONE_NEWPID as uid 1000 is a straight EPERM.
		Cloneflags: syscall.CLONE_NEWUSER |
			syscall.CLONE_NEWNS | // private mount view
			syscall.CLONE_NEWPID | // child becomes pid 1 in its own pid ns
			syscall.CLONE_NEWNET | // empty netns: lo only, no route out
			syscall.CLONE_NEWUTS | // own hostname
			syscall.CLONE_NEWIPC,

		// Go writes /proc/<child>/uid_map for us from this slice. As an
		// unprivileged parent we may map exactly ONE id: our own.
		UidMappings: []syscall.SysProcIDMap{
			{ContainerID: 0, HostID: os.Getuid(), Size: 1},
		},
		GidMappings: []syscall.SysProcIDMap{
			{ContainerID: 0, HostID: os.Getgid(), Size: 1},
		},
		// false => Go writes "deny" to /proc/<child>/setgroups BEFORE gid_map.
		// Leaving this false is correct for unprivileged use; setting it true
		// requires CAP_SETUID/CAP_SETGID in the parent namespace and re-opens
		// the group-dropping escalation the deny switch was added to close.
		GidMappingsEnableSetgroups: false,

		// Unrelated to isolation, but the difference between a tidy sandbox
		// and orphans reparented to init when your supervisor dies.
		Pdeathsig: syscall.SIGKILL,
	}

	if err := cmd.Run(); err != nil {
		fmt.Fprintf(os.Stderr, "child failed: %v\n", err)
		os.Exit(1)
	}

	// Reality check for the reader: the child was uid 0 with a full capability
	// set, in six namespaces, and every syscall it made went to the SAME host
	// kernel this process is running on. Nothing above created a second kernel.
}

Kernel knobs and distro policy: whether you get one at all

Creating a user namespace isn't universally available, and the reason it's gated is itself the tell. Mainline exposes /proc/sys/user/max_user_namespaces, a per-namespace count that doubles as a kill switch at zero. Distributions have layered on their own controls — a Debian-lineage kernel.unprivileged_userns_clone sysctl, an enterprise default of zero, more recently AppArmor restrictions permitting unprivileged creation only for profiled binaries. The specifics move between releases, so check the running system rather than trusting anyone's table, mine included.

# Can unprivileged code here create a user namespace? Ask the machine.
cat /proc/sys/user/max_user_namespaces         # 0 == disabled outright
sysctl -n kernel.unprivileged_userns_clone 2>/dev/null   # Debian-lineage toggle
sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null  # newer AppArmor gate

# The only test that doesn't lie:
unshare -Ur true && echo "unprivileged userns: ALLOWED" \
                 || echo "unprivileged userns: BLOCKED"

# If it's blocked, that is very often deliberate hardening, not a misconfiguration.
# Turning it on is a security DECISION, not a prerequisite you satisfy on the way
# to something else. See the next section for what you are deciding.

Treat enabling unprivileged user namespaces as a policy decision with a real cost, because that's what it is. The feature hands unprivileged code a supported route into kernel subsystems that previously required root — the mount machinery, network configuration, netfilter, the id-mapping paths — and all of it is kernel code, which has bugs. Distributions that ship it off, or restrict it to profiled binaries, aren't being paranoid; they're trading a useful capability against a wider attack surface for the least-trusted code on the box. Turn it on for your build tooling and you turned it on for everything else running as that user.

What user namespaces genuinely buy you

None of that is a case against the feature. User namespaces solved a real problem: before them, an enormous amount of ordinary tooling required root not because it was dangerous but because Linux had no way to express "I want to be in charge of this little world over here."

  • Rootless containers — Podman, rootless Docker, and Buildah exist because of this feature. A normal user gets a container with its own mount, pid, and network namespaces and a plausible uid layout inside, with no root daemon holding the machine's keys. Deleting a permanently-root daemon is a real win, independent of whether the container is a boundary.
  • The other namespaces become reachable — the mechanical core of it. Unprivileged mount, pid, network, and uts namespaces are all legal once you own a user namespace. Half of what people credit to containers is really this one enabling step.
  • Id-shifting for filesystem access — a file written as uid 0 inside lands as an unremarkable high uid outside. That's what makes unpacking a tarball of root-owned files work as a normal user, and what id-mapped mounts generalise so one on-disk tree can present different ownership to different namespaces. CI runners, image builders, and package tools that used to demand root now don't.
  • Honest defence in depth — being unprivileged beats being privileged, even against hostile code. Mapping a container's uid 0 to a boring host uid means an escape lands as that boring uid rather than as real root. A meaningful reduction in what an escape gets. Not prevention — and that last distinction is where the honest analysis has to begin.

The honest security analysis: remapping identity is not a boundary

Here is the thing to internalise. A user namespace changes which integer the kernel associates with your process. It does not change which kernel that is. Every syscall your namespaced, capability-laden, uid-0-looking process makes goes to the same host kernel as every other tenant's. There is one referee, it is the thing you want protection from, and renumbering your shirt does not produce a second one.

Now compare surfaces. seccomp removes syscalls. Landlock removes filesystem objects. cgroups remove resources — every other layer is subtractive. A user namespace is the one that gives something back: it makes previously-privileged kernel functionality legally reachable by unprivileged code. As an attack-surface calculation that's the wrong direction, and the historical record has been unambiguous: unprivileged user namespaces have been a recurring first step in Linux local-privilege-escalation chains. The pattern outlives the individual bugs, because the escalation usually isn't in the namespace code — it's in whatever the namespace let you reach.

The uncomfortable shape of it: the same property that makes user namespaces useful — unprivileged code can now drive kernel machinery that used to need root — is the property that makes them risky. You cannot keep the first half and discard the second. They are the same sentence read in two directions.

There's a subtler failure mode. The process genuinely holds a full capability set, so tooling that inspects capabilities to judge danger is misled, and code asking "am I root?" answers yes and takes the privileged branch. And it all rests on capability checks scattered across the whole kernel, each of which must correctly identify the owning namespace of the object in question. Most do. The interesting bugs are the ones that don't.

A container is a polite suggestion to the kernel about how it should file your process. A user namespace is a polite suggestion about what to call you while it does. Neither is a wall, and the kernel is free to be persuaded otherwise by a sufficiently motivated bug.

So the operational rule is simple. If the code is yours or your team's and the threat model is accidents rather than an adversary, user namespaces are excellent — prefer rootless everything. If the code is attacker-controlled, or was written by a language model ninety seconds ago and read by nobody, a user namespace is a layer in your defence and cannot be the boundary of it. Being uid 100000 instead of uid 0 changes what an escape wins, not whether one is possible.

User namespace vs microVM, property by property

  • What it changes — User namespace: the mapping between ids inside and outside, and which namespace your capabilities are judged against. MicroVM: which kernel receives the syscalls at all. A relabelling versus a different machine.
  • Kernel — User namespace: one, shared with every tenant, simultaneously enforcer and target. MicroVM: one guest kernel per workload; the host is reachable only through a CPU-enforced VM exit.
  • Syscall attack surface — User namespace: unchanged in width, wider in reach, since root-only kernel paths become drivable by unprivileged code. MicroVM: replaced — guest syscalls land on the guest kernel.
  • Root inside — User namespace: a full capability set honoured only against objects that namespace owns. MicroVM: actual root in a real kernel, which is fine, because that kernel is disposable and belongs to one job.
  • Resource limits — User namespace: none; that's cgroups, a separate mechanism. MicroVM: structural — vCPU and RAM are properties of the machine, and the guest's OOM killer eats the guest's own processes.
  • Blast radius against a hostile workload — User namespace: the host kernel and every co-tenant, if a permitted syscall reaches a kernel bug. MicroVM: one guest you were going to delete anyway.
  • Startup cost — User namespace: microseconds; a clone flag and two writes to procfs. MicroVM: on PandaStack, 179ms p50 and ~203ms p99 for a snapshot-restore create, of which roughly 49ms is the restore; a same-host fork is 400-750ms, and the ~3s cold boot happens once, at bake time.
  • Right job for it — User namespace: rootless tooling, build systems, de-privileging your own services, an inner layer inside a stronger boundary. MicroVM: code you have no reason to trust, running beside other people's.

The startup-cost row usually settles the argument. Teams reach for namespace-only isolation because the alternative is imagined as slow and heavy. That was true once. A fifth of a second for a private kernel makes "VMs are too expensive to give each job one" a statement about a decision made years ago, not about current costs.

Composing the layers properly

Because each primitive restricts a different noun, stack them rather than pick a winner. A user namespace de-privileges identity. seccomp removes syscalls. Landlock removes filesystem objects. cgroups v2 caps consumption. A network namespace decides what it can talk to. Applied together to your own cooperating code, that's a strong, cheap, well-understood configuration.

Then be clear about the drop-off at the end of that list. Every layer above is enforced by the host kernel, so every layer shares one failure mode: a memory-corruption bug reached through some permitted syscall compromises the enforcer, and a compromised kernel does not stop to consult the policies it was enforcing a microsecond earlier. Five host-kernel mechanisms give you five reductions in probability and zero reduction in blast radius. The step that changes blast radius is giving the workload its own kernel.

Firecracker is the cleanest illustration of the intended relationship. The boundary is KVM: guest code talks to a guest kernel and reaches the host only through a hardware trap. Underneath, the VMM is a host userspace process, so it runs under a jailer that drops to an unprivileged uid, chroots, applies cgroups and namespaces, and installs a strict seccomp filter. De-privileging and namespaces do real work there — below the boundary, hardening a small host-side process, rather than being asked to be the boundary. That's the pattern worth copying.

What this looks like in practice

PandaStack runs every sandbox, managed Postgres database, and hosted app as its own KVM-backed Firecracker microVM. The workload gets a real kernel it can be real root in, and when it's finished the machine, its filesystem, its memory, and anything it left behind stop existing together. Inside the guest, every layer from this post is still worth applying — the boundary that stops your tenants reaching each other has no opinion about what the workload does inside its own guest.

from pandastack import Sandbox

# THE BOUNDARY: the code gets its own kernel. Everything the rest of this post
# discussed -- user namespaces, capabilities, seccomp -- belongs INSIDE this,
# as hardening. None of it substitutes for it.
sbx = Sandbox.create(template="code-interpreter", ttl_seconds=600)

try:
    sbx.filesystem.write("/work/task.py", generated_source)

    # Inside the guest, keep de-privileging. `unshare -Ur` costs microseconds
    # and means a bug in the workload starts from a mapped, unprivileged uid
    # in an empty network namespace rather than from the guest's real root.
    out = sbx.exec(
        "cd /work && unshare -Urn -- python3 task.py",
        timeout_seconds=300,
    )

    if out.exit_code != 0:
        raise RuntimeError(out.stderr[-4000:])

    print(out.stdout)
finally:
    # The cheapest layer, and the only one with no bypass: the machine is gone.
    sbx.kill()

# Note what the guest-side unshare is and isn't doing. It reduces what a bug in
# task.py immediately gets. If task.py goes looking for a kernel bug, it finds
# the GUEST kernel -- a kernel that belongs to this job, for the next 600
# seconds, and to nobody else, ever.

Where this leaves you

User namespaces are three ideas. One: a mapping between ids inside a namespace and ids in its parent, so a process can hold uid 0 and a full capability set inside while remaining an ordinary unprivileged uid outside — capabilities honoured only against objects the namespace owns. Two: the mechanics are a one-shot uid_map write, a mandatory setgroups deny before gid_map, and delegated /etc/subuid ranges via setuid helpers when one id isn't enough. Three: the feature is genuinely valuable — rootless containers, unprivileged mount and network namespaces, id-shifted filesystem access — and I'd rather have it than not.

Then the part to carry around. A user namespace remaps identity; it does not add a boundary. There is still one kernel underneath, shared with everyone, doing the enforcing and standing as the target. The syscall surface is no narrower and the reachable-privileged-code surface is wider, which is why enabling it is a security decision in its own right and why it keeps showing up as the opening move in privilege-escalation chains. Use it everywhere you can, as one layer among several. Don't stake untrusted code on it.

If the code is attacker-controlled or model-generated, the defensible boundary is a hypervisor, with the namespace stack as defence in depth inside and beneath it — the way Firecracker composes a jailer and seccomp under KVM. For the adjacent layers: the syscall axis is in /blog/seccomp-explained, the filesystem axis in /blog/landlock-lsm-explained, the resource axis in /blog/cgroups-v2-explained-for-sandboxing, the shared-kernel argument in /blog/why-docker-is-not-a-sandbox, and the full ranking in /blog/code-isolation-hierarchy.

Frequently asked questions

Do user namespaces make containers secure?

They make containers meaningfully less dangerous, which is not the same thing. Mapping a container's uid 0 to an unremarkable host uid means that if something does escape, it escapes as a boring unprivileged user rather than as real root — a genuine reduction in what an escape immediately wins. But the container's processes still issue their syscalls to the same host kernel that every other tenant shares, and that kernel is both the enforcer of the namespace and the thing you are trying to be protected from. A memory-corruption bug reached through any permitted syscall compromises the enforcer, and at that point the id mapping is irrelevant. Use user namespaces as a hardening layer on every container you run; don't treat them as the boundary for hostile code.

Are unprivileged user namespaces a security risk?

Yes, and that is why several distributions gate them. The feature's value is that unprivileged code can now legally drive kernel machinery that previously required root — the mount subsystem, network configuration, netfilter, various id-mapping paths. That is the same sentence as its risk, read in the other direction: the set of kernel code paths reachable by your least-trusted process gets strictly larger. Historically, unprivileged user namespaces have been a recurring opening move in Linux local-privilege-escalation chains, not usually because of bugs in the namespace code itself but because of bugs in whatever the namespace made reachable. Enabling them is a real policy decision with a real cost, so decide it deliberately rather than flipping a sysctl to make a build tool work.

What is the difference between rootless containers and a microVM?

A rootless container is a normal process whose identity has been remapped and whose view of the system has been partitioned, running directly on the host kernel. A microVM is a separate machine: the workload talks to its own guest kernel, and the only route toward the host is a CPU-enforced VM exit into a small device-emulation surface. The practical difference is blast radius. If a rootless container finds a kernel bug through a syscall it is allowed to make, the host and every co-tenant are in scope; if a microVM guest does the same, it has compromised a kernel that belongs to one job and gets deleted anyway. Rootless containers are excellent for cooperating software and for removing a permanently-root daemon from your architecture; a microVM is what you want when the code is untrusted.

Why do I get EPERM when writing gid_map?

Almost certainly because you did not write the string "deny" to /proc/<pid>/setgroups first. An unprivileged process must disable setgroups(2) in the namespace before it is allowed to write gid_map at all. The requirement exists because dropping a group membership can grant access rather than remove it — a file mode like 0704 denies the group and allows others — so early user namespaces let an unprivileged process shed groups it had no business shedding and turn a namespace into a privilege upgrade. The switch is one-way and must be flipped before the map write. The other common cause is trying to map more than one id: an unprivileged writer may only map its own effective id, and richer ranges need CAP_SETUID in the parent namespace, which in practice means the newuidmap and newgidmap setuid helpers reading an administrator-allocated range from /etc/subuid and /etc/subgid.

Can I run AI-generated code safely in a user namespace?

Not on its own. A user namespace gives that code an unprivileged identity, which is worth having, but it does not remove a single syscall, does not limit memory or CPU, does not restrict which files it can reach beyond ordinary permission checks, and leaves it executing against the same host kernel as everything else on the box. Layer seccomp, an LSM like Landlock, cgroups v2, and a network namespace on top and you have materially lowered the probability of a bad outcome — while changing the blast radius of one by exactly nothing, because every layer in that list is enforced by the kernel under attack. For code that nobody has read, run it in a KVM-backed microVM so it gets its own kernel, and apply the namespace stack inside the guest as defence in depth.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.