Unprivileged User-Namespace Restrictions Colliding with AI-Agent Sandboxes
Executive Summary
Since Ubuntu 23.10, and by default in Ubuntu 24.04 LTS, an AppArmor-mediated kernel restriction called kernel.apparmor_restrict_unprivileged_userns has quietly broken a wide swath of Linux sandboxing tooling: bubblewrap (bwrap), Chrome/Chromium's --sandbox, Electron apps, Flatpak, Steam, and anything built on raw unshare(CLONE_NEWUSER). The restriction is a genuinely good idea — unprivileged user namespaces have been a reliable ingredient in local kernel privilege-escalation chains — but its opt-in-by-profile design means unprivileged applications are denied unless an attached AppArmor profile explicitly grants the userns permission.
This collision has become newly urgent in 2026 because AI coding-agent CLIs such as OpenAI's Codex CLI and Anthropic's Claude Code sandboxed Bash tool now use bubblewrap-based Linux isolation for model-issued shell commands. On a bare Ubuntu host, an exact-path AppArmor policy can restore that primitive without switching off the host-wide restriction, but availability and activation are release-specific. Current Noble updates provide an experimental bwrap-userns-restrict policy through the optional apparmor-profiles package; it is stored under /usr/share/apparmor/extra-profiles/ and is disabled by default. Its two-profile design gives bwrap the capabilities needed for setup, then forces executed children into a stacked profile that denies capabilities. Replacing it with a one-line unconfined userns grant would instead make /usr/bin/bwrap a trampoline around the mitigation. Inside Docker or Kubernetes, the container runtime's seccomp and AppArmor policies introduce separate gates. A host per-application policy is not automatically a safe container policy, and CAP_SYS_ADMIN alone is both broad and sometimes insufficient. This article maps those distinct mechanisms and gives operators a deployment-specific decision matrix rather than one universal fix.
The Mechanism: What apparmor_restrict_unprivileged_userns Actually Restricts
Unprivileged user namespaces (unshare(CLONE_NEWUSER) without root) let a normal process mint a namespace in which it appears to be root, gaining an in-namespace set of capabilities it can use for tasks like building a chroot-style mount tree, remounting /proc, or setting up further nested namespaces — all without a setuid helper. It's the mechanism that lets Chrome sandbox its renderer processes and lets bubblewrap build Flatpak's confinement without root, and it's been in mainline Linux since kernel 3.8. It has also been, per Ubuntu's own tally cited in their announcement, an ingredient in roughly 44% of the Linux kernel privilege-escalation exploits Google's security team surveyed, because the namespace machinery exposes a large amount of rarely-exercised kernel code (netfilter, filesystem mount parsing, etc.) to unprivileged callers (Ubuntu blog: Restricted unprivileged user namespaces are coming to Ubuntu 23.10).
Rather than the blunt on/off sysctl that Debian (kernel.unprivileged_userns_clone) and some hardened kernels have long carried, Canonical implemented a finer-grained gate through AppArmor. With kernel.apparmor_restrict_unprivileged_userns=1 (the Ubuntu 24.04 default), AppArmor restricts user-namespace creation for unconfined applications; an application that legitimately needs it must run under a profile whose policy explicitly grants userns,. Preventing an unprivileged unconfined task from using aa-exec to enter a more favorable profile requires a separate hardening control, kernel.apparmor_restrict_unprivileged_unconfined=1; Canonical documents enabling it explicitly and says it will become a default only in future releases. The important distinction is therefore not "confined processes are denied while unconfined ones remain allowed" — the default is the reverse of that description — but whether the active policy intentionally grants user-namespace creation and whether the separate profile-transition hardening is enabled (Ubuntu specification; Ubuntu explanation).
Concretely, the /etc/apparmor.d/chrome profile that Chromium's documentation identifies for Google Chrome stable is itself a non-confining compatibility grant:
abi <abi/5.0>,
include <tunables/global>
profile chrome /opt/google/chrome/chrome flags=(unconfined) {
userns,
@{exec_path} mr,
include if exists <local/chrome>
}
The current upstream rule includes @{exec_path} mr, so the executable's attachment path is readable and mappable even when Chrome is launched from another confined process. AppArmor added an automated attachment-path check after documenting why an omitted permission can appear to work from an unconfined parent yet fail from a confined one (AppArmor MR !1637).
Noble's current bwrap policy is deliberately not the same unconfined compatibility pattern. The optional apparmor-profiles package installs it at /usr/share/apparmor/extra-profiles/bwrap-userns-restrict, outside the automatically loaded /etc/apparmor.d/ set. An administrator who chooses it must explicitly install the optional package, copy the packaged policy into the loaded policy directory, and load it in enforce mode:
sudo apt install apparmor-profiles
sudo install -m 0644 \
/usr/share/apparmor/extra-profiles/bwrap-userns-restrict \
/etc/apparmor.d/bwrap-userns-restrict
sudo apparmor_parser -r /etc/apparmor.d/bwrap-userns-restrict
The security-relevant shape below is an abbreviated explanation, not a replacement policy to paste. Use the packaged file unchanged: its parent grants bwrap the setup powers it needs, while every executed child takes a stacked unpriv_bwrap profile whose explicit rule denies capability use by an arbitrary payload inside its new user namespace.
profile bwrap /usr/bin/bwrap flags=(attach_disconnected) {
allow capability,
allow userns,
allow mount,
allow pivot_root,
allow px /** -> bwrap//&unpriv_bwrap,
}
profile unpriv_bwrap flags=(attach_disconnected) {
allow userns,
allow mount,
allow pivot_root,
allow pix /** -> &unpriv_bwrap,
audit deny capability,
}
The Chrome rule remains a non-confining compatibility grant: flags=(unconfined) means AppArmor otherwise imposes no restrictions on that binary. The bwrap policy instead limits the trampoline that made Canonical initially avoid a default bwrap grant: because /usr/bin/bwrap can launch arbitrary binaries, a simple unconfined userns profile would let any local user start an arbitrary payload with user-namespace capabilities and substantially reopen the kernel attack surface the sysctl is meant to reduce. Noble's packaged parent/child transition is the material control that avoids that outcome (packaged Noble-updates profile; optional package file list).
The package labels these extra profiles experimental and leaves them disabled because they may break real workloads. That warning is concrete for bwrap: a Noble update that installed the policy directly under /etc/apparmor.d/ was reverted after various Flatpak applications could no longer save files; a later update placed it in the optional profile package instead (Noble AppArmor package changelog). Test affected Flatpak workflows and the agent sandbox before enabling it broadly, and keep the loaded copy aligned with package updates. This is a bare-host policy, not a container recipe. It must not be copied into a container runtime's Localhost slot and described as an extra layer: Docker and Kubernetes select one AppArmor profile for the container, so a partial host policy would replace, not augment, the runtime default.
It's also worth noting the restriction is not airtight. Qualys's Threat Research Unit disclosed three practical bypasses in March 2025 — using aa-exec to relaunch under an already-permissive profile like chrome or flatpak when the separate apparmor_restrict_unprivileged_unconfined hardening is not enabled, spawning the default-installed busybox (whose profile grants userns), and LD_PRELOAD-injecting a shell into a permissive-profiled binary such as Nautilus — each letting a local unprivileged user obtain a full-capability user namespace anyway (Qualys: Three bypasses of Ubuntu's unprivileged user namespace restrictions; LWN: Bypassing Ubuntu's user-namespace restrictions; follow-up bypass work at afflicted.sh). Ubuntu's response was that these don't constitute a security vulnerability per se — the feature was never meant to be a hard boundary against a determined local attacker, only to raise the bar against remote/opportunistic exploit chains — but it underlines that this is defense-in-depth, not a capability model, and shouldn't be treated as a substitute for real isolation when agent-generated code is involved.
Real-World Breakage Catalog
The rollout produced a long tail of "why did this suddenly stop working" reports across the desktop and developer-tooling ecosystem:
- Chromium/Electron. Chromium's own security documentation states plainly: "Our primary sandbox no longer works on developer builds on some Linux distributions, namely Ubuntu, due to a security feature that restricts access to a powerful kernel feature, user namespaces" (Chromium docs: AppArmor User Namespace Restrictions vs. Chromium Developer Builds). Google Chrome stable installed at
/opt/google/chrome/chromeis matched by the static/etc/apparmor.d/chromeprofile; Ubuntu's packaged Chromium instead ships as a snap whose AppArmor profile is generated at runtime. Locally built or downloaded Chromium binaries and Electron apps installed at other paths do not inherit either packaging mechanism, so they can fail to launch. A user without root can explicitly pass--no-sandbox, but Chromium warns that this disables critical security features; its documentation does not claim an automatic fallback. Chromium's public bug tracker has the canonical report at issues.chromium.org/issues/333313925, and Electron tracked it as affecting every Electron app on Ubuntu 24.04+ in electron/electron#41066 ("All versions of Electron fail to open on Ubuntu 24.04+ due tokernel.apparmor_restrict_unprivileged_userns=1by default"). Downstream, apps like Joplin (laurent22/joplin#9137) and various AppImage-packaged tools hit the identical SIGTRAP/crash signature. - Bubblewrap directly. On affected bare Ubuntu hosts, bwrap commonly fails while setting up the user namespace or UID map; Russell Coker documented the Ubuntu 24.04 break in detail (etbe.coker.com.au), and the same host-policy problem appears in projects that vendor bwrap for build isolation, including Chainguard's
melange(chainguard-dev/melange#1508) and VS Code's sandboxing work (microsoft/vscode#316046). But strings such asPermission deniedandOperation not permittedare not diagnoses: inside a container, seccomp can rejectpivot_rootwhile the container's AppArmor profile separately rejects mount propagation, producing similarly generic errors for different policy decisions. - Flatpak and Steam. Noble demonstrates why "profile available" is not the same as "safe to enable everywhere": an update that automatically loaded
bwrap-userns-restrictwas reverted because various Flatpak applications could no longer save files, and the policy now ships as a disabled experimental extra. Downstream packaging can drift too: Valve's Ubuntu 24.04 Steam AppArmor profile shipped withflags=(complain)instead offlags=(unconfined), which meant it still blocked userns creation despite intent, producing "Steam now requires user namespaces to be enabled" for users who had done nothing wrong (ValveSoftware/steam-for-linux#12620). - AI coding-agent CLIs, directly. OpenAI's Codex CLI can use a vendored copy of bubblewrap for its Linux sandbox. When Codex began preferring that internal path over
/usr/bin/bwrap, it fell outside path-scoped AppArmor profiles on hosts that had one. openai/codex#14963 addressed that regression by preferring the system binary when present, but/usr/bin/bwrapbenefits from Noble's policy only when the optional package is installed and its experimental profile is explicitly loaded. Container support required a different change: openai/codex#17547 installs setuid bwrap in a dedicated secure devcontainer and explicitly relaxes Docker capabilities, seccomp, and AppArmor so Codex can construct its inner sandbox. Claude Code's official sandbox documentation likewise separates bare Ubuntu AppArmor setup from nested-container failures, and offersenableWeakerNestedSandboxonly when the outer container already supplies the required isolation boundary (Claude Code docs: sandboxing).
The shared pattern is the symptom, not necessarily the cause: Permission denied, Operation not permitted, a crash, or disabled sandboxing can come from Ubuntu's user-namespace restriction, a container seccomp filter, the container's selected AppArmor profile, or more than one of those at once. Diagnose the failed syscall and the relevant seccomp/AppArmor audit evidence before choosing a mitigation.
The Container Twist: Why a Bare-Host Fix Is Not a Container Recipe
The bare-host compatibility fix assumes you control the host's AppArmor policy and the executable path that policy names. A container adds runtime-selected seccomp and AppArmor policy around the whole container, so the operator has to reason about a different policy stack. Three distinctions matter:
The container gets one selected AppArmor policy, not an additive per-binary exception. Docker normally attaches its generated docker-default profile to the container; --security-opt apparmor=<name> selects another loaded profile instead (Docker AppArmor documentation). Noble's packaged bwrap policy is a host-side, executable-specific parent/child design, not a complete container policy. Selecting a partial compatibility policy as the container's custom profile would replace the runtime default; it would not add bwrap's rules on top of docker-default.
Docker's defaults can fail at separate construction steps. Codex's container work records Docker seccomp rejecting pivot_root even with CAP_SYS_ADMIN, while docker-default separately rejects bwrap's mount-propagation setup. Its tested secure-devcontainer recipe therefore uses setuid bwrap; adds SYS_ADMIN, SYS_CHROOT, SETUID, SETGID, and SYS_PTRACE; and sets both seccomp=unconfined and apparmor=unconfined (openai/codex#17547). That is an explicit relaxation of the outer container, not evidence that CAP_SYS_ADMIN alone or a one-line AppArmor grant is sufficient. A narrower custom seccomp/AppArmor design may be possible, but it must model and test the complete inner-sandbox syscall and mount behavior.
Kubernetes exposes the same selection explicitly. The structured securityContext.appArmorProfile field became available in Kubernetes 1.30 and stable in 1.31. It selects RuntimeDefault, Localhost, or Unconfined; a container-level setting overrides the pod-level setting, so each container still runs under one selected profile. A Localhost profile must already be loaded on the node, and kubelet rejects the Pod when a specified profile is missing (Kubernetes AppArmor tutorial; Kubernetes security-context documentation). A safe Localhost option therefore has to be a complete, enforcing container profile that deliberately includes every required rule; the bare-host unconfined compatibility shim is not suitable. Kubernetes Pod user namespaces are a separate feature: a Pod opts in with .spec.hostUsers: false; alpha support began in 1.25 for stateless Pods, 1.28 lifted the stateless-only restriction, 1.30 promoted the feature to beta, and it is stable in 1.36 (Kubernetes 1.30 history). It remaps the Pod's UIDs and limits its capabilities outside that namespace, but does not by itself authorize processes to construct an additional nested bwrap sandbox (Kubernetes user-namespace documentation).
CAP_SYS_ADMIN remains a broad catch-all covering mount and many unrelated kernel operations, and --privileged is broader still. Those costs matter for a container executing model-generated commands. But the comparison is not "broad capability versus one narrow AppArmor bit" in every deployment: bwrap also needs mount, namespace, UID/GID, and seccomp behavior that the outer runtime may block independently. The least-privilege answer is deployment-specific — retain the outer runtime policy unless there is a tested reason to replace it, and account honestly for every relaxation needed to make the inner sandbox work.
Why This Matters Specifically for AI Coding Agents
Coding-agent CLIs are a particularly sensitive consumer of userns: they execute an open-ended stream of model-generated shell commands — rm, curl, git, arbitrary interpreters — so a missing or silently weakened sandbox changes the trust boundary around untrusted actions. Codex's bwrap-based Linux sandbox and Claude Code's sandboxed Bash tool both use namespace isolation, but their supported host and nested-container paths are not interchangeable (Codex bare-host fix; Claude Code sandbox documentation).
That dependency is straightforward on a compatible bare host and more complicated when nested inside another sandbox. An operator must decide which layer is the real isolation boundary, verify that the inner sandbox actually initializes, and make any fallback visible. A successful process start is not enough: the agent may have fallen back to a weaker mode, or a later namespace/mount step may still fail.
The resulting guidance is conditional. On bare Noble, prefer the distro-path binary only after verifying that the current optional apparmor-profiles package is installed and its exact packaged two-profile policy is deliberately loaded in enforce mode. Do not substitute an unconfined one-line grant; stage the policy against Flatpak and the agent sandbox, and keep the loaded copy aligned with package updates. The alternatives are to leave bwrap blocked or explicitly accept the wider exposure from disabling the host sysctl. In Docker, follow a vendor-tested nested-sandbox profile or keep the outer container as the declared boundary; do not infer sufficiency from one successful unshare call. In Kubernetes, a Localhost profile must be a complete enforcing policy for the container and must be distributed to every eligible node. When the outer container is intentionally the boundary, Claude Code's enableWeakerNestedSandbox is an explicit weaker mode: it bind-mounts the container's existing /proc instead of creating a fresh one, exposing process information the stronger mode would hide. In every case, fail closed or emit an actionable warning rather than silently dropping isolation.
Mitigation Decision Matrix
| Approach | What it does | Blast radius if agent sandbox is compromised | Operational cost | When it's appropriate |
|---|---|---|---|---|
Sysctl off (kernel.apparmor_restrict_unprivileged_userns=0) | Disables the restriction host- or node-wide for every process | High — re-opens userns to all local unprivileged processes, not just the agent; restores the pre-2023.10 kernel attack surface Ubuntu shipped this to close | Lowest (one sysctl, or a sysctl.d drop-in) | Single-tenant dev VM/sandboxed cloud instance you already trust broadly; never on a shared or multi-tenant host |
| Noble optional bwrap profile | Installs apparmor-profiles, then explicitly loads the packaged two-profile bwrap-userns-restrict policy for /usr/bin/bwrap | Keeps the host-wide denial for other unconfined applications and denies capability use in bwrap's stacked child profile; bwrap itself receives broad setup permissions, and replacing the child transition with an unconfined shim defeats that protection | Medium — experimental/default-disabled policy, explicit activation, package-to-loaded-copy drift, and Flatpak plus agent-sandbox regression tests | Controlled bare Noble host where the exact packaged policy has been staged and its bwrap/Flatpak behavior is acceptable; not a generic shared-host or container recipe |
| Docker nested sandbox, vendor-tested profile | Makes the outer runtime permissive enough to construct the inner sandbox; Codex's documented example uses setuid bwrap, several capabilities, and unconfined seccomp/AppArmor | Broadens the outer container substantially; security now depends more heavily on the inner sandbox and surrounding environment | Medium to high; exact runtime flags and smoke tests are part of the contract | Dedicated, disposable development container where that explicit trade-off is acceptable; not a generic least-privilege recipe |
Kubernetes enforcing Localhost profile | Selects a complete node-loaded AppArmor policy for the container, including the rules needed by the inner sandbox | Can retain AppArmor enforcement, but only to the extent the custom policy is complete and tested; it replaces RuntimeDefault | High — policy design, node distribution, scheduling compatibility, and versioned tests | Clusters prepared to own a full container AppArmor policy; missing node profiles must fail admission/startup visibly |
Pod user namespace (.spec.hostUsers: false) | Remaps Pod UIDs/GIDs and limits namespace-scoped capabilities outside the Pod | Improves the outer Pod boundary but does not grant or prove nested bwrap support | Medium; runtime and volume limitations apply | Useful outer hardening, considered separately from the inner-sandbox mechanism |
| Weaker or disabled inner sandbox | Relies on the outer container/VM boundary; Claude Code's weaker nested mode reuses the outer /proc | Entirely dependent on outer isolation and exposes resources the stronger inner mode would hide | Low engineering cost, high need for honest boundary documentation | One disposable container/VM per trust domain, with no cross-tenant secrets and an explicit weaker-mode warning |
Forward Outlook
Ubuntu's policy is one response to a long-standing kernel attack-surface problem; Debian's coarser unprivileged_userns_clone control is another (see the 2016 LWN discussion of controlling access to user namespaces). That diversity is reason to document an agent sandbox in terms of the primitive it needs and the boundary it promises, then give separate, tested instructions for each supported host and container environment. AppArmor syntax is one implementation, not the portable contract.
Qualys's 2025 bypass disclosures are also a reminder of the feature's stated role: it raises the cost of reaching vulnerable kernel code but is not a complete sandbox or capability boundary. Agent operators still need an outer isolation boundary that survives arbitrary code execution, regardless of whether this particular userns mediation check is enabled.
For agent-runtime vendors, the practical work is to detect which construction step failed, surface the responsible policy layer, and test the promised isolation end to end. Prefer the system bwrap on Noble only when the optional packaged policy is installed, explicitly enforced, and validated with the host's actual bwrap consumers; availability under /usr/share/apparmor/extra-profiles/ does not mean it is active. Preserve the packaged parent/stacked-child capability boundary rather than generating an unconfined shortcut. Treat Docker and Kubernetes as separate deployment targets with explicit outer-boundary trade-offs, and never present a host compatibility policy as an additive Kubernetes layer. A loud weaker-mode warning or a closed failure is safer than a successful startup that quietly changed the security model.

