Build1 publisher3 min readPublished
Chromium's user-namespace sandbox survives the hardening that kills its setuid helper
PDFik dropped --no-sandbox from its Playwright workers and kept rendering customer-submitted HTML in non-root, capability-stripped pods. Its own security auditor later wrote that the flag was mandatory.
The Engineer · Build desk

What happened
- The flag had been deleted in an earlier hardening round, and those same pods had been rendering untrusted pages in production with the sandbox on and the end-to-end suite green.
- By Vitalii's account the auditor repeated tutorial folklore that is half right, because only one of Chromium's two sandbox mechanisms genuinely cannot work in that container.
- The audit still changed the spec: the pod now pins the RuntimeDefault seccomp profile, where Kubernetes leaves the container unconfined if the field is absent.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure Each copied --no-sandbox line puts renderer code execution inside the worker process, so a routine browser bug reaches the job runner's environment variables, tokens and network position.
- constraint The configuration only works where the host kernel permits unprivileged user namespaces, so fleets on kernels with the sysctl off or AppArmor restrictions in force cannot lift it as-is.
- decision A team reviewing a headless Chromium image now has two conditions it can test in the running container before accepting the flag as a requirement of the platform.
- precedent A pod-spec review produced a finding that would have switched a working defense back off. Reviewers of these specs need to check the runtime.
The legacy sandbox is a small root-owned helper binary that elevates to root just long enough to construct the isolation, then drops everything. In a hardened container that path is dead on arrival by design [12]. A pod that sets `allowPrivilegeEscalation: false` turns on the kernel's `no_new_privs` bit. Nothing the process executes may gain privileges it does not already have. The bit exists to neutralize setuid tricks [13]. So the helper cannot run, and that much of the folklore is right.
The modern sandbox needs no privileges when the environment permits it. An ordinary process asks the kernel for a new user namespace, where it is root inside while remaining a nobody outside. Chromium uses that namespace-root to strip its renderers of the filesystem, the network and everything else [14]. The renderers are the processes that execute whatever the page supplies: HTML parsing, JavaScript, images, fonts [9]. With the sandbox in place, code execution in a renderer lands in a process with no filesystem, no network sockets of its own and no real user identity. It still needs a second, unrelated bug to get anywhere [10].
Two switches decide whether that path is open, and both have to be on [19]. The kernel has to allow unprivileged user namespaces. Inside the container, `cat /proc/sys/user/max_user_namespaces` returning `0` is the kernel refusing. Debian historically gated this behind a sysctl; recent Ubuntu restricts it through AppArmor [15]. The seccomp profile has to allow the namespace syscalls. Docker's historic default profile blocked creating user namespaces, and that block is what produces the `Failed to move to new namespace ... Operation not permitted` message [16]. On a node where `max_user_namespaces` reads `0`, deleting `--no-sandbox` gets you a browser that will not start [15].
PDFik's auditor read the pod spec and wrote as a finding of fact that Chromium could not even start without `--no-sandbox` in a configuration that locked down [4]. The flag had been removed in an earlier hardening round, and the same pods had been rendering untrusted pages in production the whole time, sandbox on, end-to-end suite green [5]. "I like this story because nobody in it is careless," Vitalii wrote [6]. By his account the auditor pattern-matched the folklore every tutorial repeats, and the only mistake was not knowing there are two sandboxes [7].
The audit did change the spec. The pod now pins a seccomp profile explicitly, `RuntimeDefault`, because Kubernetes leaves the container unconfined when the field is absent [8]. That field is one of the two switches the user-namespace sandbox depends on [16]. Anyone copying the hardening should check the profile they pin against the namespace syscalls before assuming the browser still starts.
All of this is one team's account of one deployment. Queue-driven workers render customer-submitted URLs and HTML through headless Chromium and Playwright, non-root, all Linux capabilities dropped, read-only root filesystem [2][3]. Vitalii is the founder of PDFik, a hosted URL/HTML-to-PDF API [1]. The flag he argues against is still the accepted answer in countless Dockerfiles, quickstarts and Stack Overflow answers [18], and the error text that sends people there suggests it in as many words: "If you want to live dangerously and need an immediate workaround, you can try using --no-sandbox" [17].
What to watch
- Whether PDFik publishes the pod spec's seccompProfile block and the max_user_namespaces value its nodes report.
- Whether Playwright and Puppeteer container quickstarts drop --no-sandbox now that the unprivileged user-namespace path is documented as working.
- Whether further AppArmor restrictions on unprivileged user namespaces in Ubuntu break sandboxed Chromium for containers on those hosts.