· Clane AI · 3 min read
Keeping Chrome's sandbox on in Docker
Almost every guide to Chromium in a container says --no-sandbox. Why that is, what it gives up, and the three-syscall seccomp profile that keeps the sandbox on.
Search for "Chromium in Docker" and nearly every answer ends with the same flag: --no-sandbox. Until this week our
image did too. It now runs Chromium with its sandbox on, inside an ordinary Docker container, without extra
capabilities and without turning seccomp off. This post explains the problem and the fix, which is small enough to
copy.
What the sandbox does
Chromium splits a browser into processes. The ones that parse and run web content, the renderers, are the ones an attacker targets, so Chromium locks them down. On Linux that is two layers:
- Namespaces. Renderers start in their own user, PID and network namespaces, so a compromised renderer cannot see other processes or open network connections of its own.
- seccomp-bpf. Each renderer also gets a syscall filter that allows only what rendering needs.
--no-sandbox turns both off. A bug in the renderer then runs with whatever the browser process can do: read the
profile directory (cookies, saved sessions), reach the network, see the other processes in the container. For a
browser that holds people's logged-in sessions, that is the wrong trade.
There is also a visible cost. Chromium shows an infobar on every window: "You are using an unsupported command-line flag: --no-sandbox. Stability and security will suffer." Our users watch their browser in a live view, so they saw it too.
Why containers break it
To create those namespaces, Chromium calls clone and unshare with namespace flags, and later setns. Docker's
default seccomp profile allows those calls only when the container has CAP_SYS_ADMIN. Without it they fail, the
sandbox cannot start, and Chromium refuses to run unless you pass --no-sandbox.
The usual ways around it are all broad:
| Option | What it costs |
|---|---|
--no-sandbox | No sandbox for any renderer. |
--cap-add SYS_ADMIN | A capability that covers mounts, namespaces and much more, for the whole container. |
--security-opt seccomp=unconfined | No syscall filter for the container at all. |
The fix: three syscalls
We took Docker's default seccomp profile and added one rule: allow clone, unshare and setns without
CAP_SYS_ADMIN. Everything else stays as Docker ships it, including the rule that answers clone3 with ENOSYS, so
the C library falls back to plain clone, which is now allowed.
{
"names": ["clone", "unshare", "setns"],
"action": "SCMP_ACT_ALLOW",
"comment": "Chromium's namespace sandbox (user, PID and network namespaces) without CAP_SYS_ADMIN"
}The profile is docker/seccomp-chromium.json in the
open-source repository. Use it with docker run:
docker run -d --name webpilot -p 127.0.0.1:8931:8931 --shm-size=1g -v webpilot-data:/data \
--security-opt seccomp=docker/seccomp-chromium.json \
-e CBU_VAULT_KEY=$(openssl rand -hex 32) ghcr.io/clane-ai/webpilot-server:latestor in Compose:
services:
gateway:
image: ghcr.io/clane-ai/webpilot-server:latest
shm_size: 1g
security_opt:
- seccomp=../docker/seccomp-chromium.jsonThe path is read by the Docker daemon on the host, not inside the container. On a platform that runs Compose for you
(we deploy with Dokploy), put the file somewhere the platform's daemon can read and point security_opt there.
Fail loudly, not silently
A missing profile should not take the service down, but it should not hide either. The image's entrypoint now probes
the sandbox before it starts the gateway: it launches Chromium headless, with the sandbox, on about:blank.
- If that works, it logs
Chromium sandbox: onand starts normally. - If it fails, it logs a warning with the fix, adds
--no-sandbox, and starts anyway. The probe's output is kept in/tmp/sandbox-probe.log.
If the probe fails even with the profile, check that the host allows unprivileged user namespaces; some distributions restrict them by default.
What changed for users
Nothing to do on the hosted service: webpilot.si runs with the profile. Self-hosters get it with the Compose files in
the repository, and the README's docker run line now includes it. And the infobar is gone.