Repository navigation
v1.14.0
A hardening release over 1.13.1. The controller no longer runs as root, and Chromium runs with its own sandbox in
browser-node and in every isolated container, so a renderer exploit no longer starts with every syscall the
container allows. BROWSER_SANDBOX=on refuses any session whose browser is not verifiably sandboxed. Read the
upgrade notes first: the controller changes user and hands its data over at the first start, and browser-node runs
under a custom seccomp profile. The controller and browser-node images change; the client, LangChain and MCP bridge
packages change only in version.
Upgrade notes
- A default compose install now runs browser-node under a custom seccomp profile that lets the container create
user namespaces. Chromium's own sandbox needs them, and Docker's default profile refuses them to a container
without CAP_SYS_ADMIN. The profile is moby's default pluschroot,cloneandunshare
(controller/app/browser/seccomp/);docker_ephemeralcontainers get it too. It exposes the kernel's
user-namespace code to code in the container, and in exchange every renderer runs sandboxed. That is the better
trade for a browser that renders arbitrary pages: a renderer exploit no longer starts with every syscall the
container allows. To keep the old setup under compose, set bothBROWSER_SANDBOX=offand
BROWSER_SECCOMP_PROFILE=builtin(docs/deployment.md, "Chromium's sandbox"). On a host whose Docker daemon sets
its ownseccomp-profile, 1.14.0's profile replaces that one, andbuiltinmeans moby's built-in profile, not the
daemon's: pointBROWSER_SECCOMP_PROFILEandISOLATED_BROWSER_SECCOMP_PROFILEat your profile plus the
auto-browser entry to keep it. - The controller now runs as the owner of your data directory, or as uid 10002, not as root. A Linux checkout
whose./databelongs to you runs it as you; Docker Desktop and named volumes run it as uid 10002 (never
browser-node's 10001). It holds no capabilities and runs with no_new_privs. At start, the
root-owned files earlier releases left in the controller's own/datasubtrees are handed to that user, and
data/api-tokenstays readable by its owner.CONTROLLER_RUN_AS=rootrestores the root controller; it is not
recommended, and readiness warns while it is set. With the isolation overlay on Docker Desktop, the controller
joins group 0 to use the Docker socket. BROWSER_SANDBOX=onnow also gates shared mode: every session is refused, and readiness fails, while the
shared browser-node's connect-time state is not a verifiedon, including a browser-node image older than 1.14.0.
Underon,browser.cdp_attachis refused; aCDP_CONNECT_URLChrome is the operator's own and is reported as
attached, not checked.- Rolling back: after running an older release on the same data, delete
data/.auto-browser-ownerbefore
starting 1.14 again, so the files the older release created as root are handed over. A container without
DAC_OVERRIDE or DAC_READ_SEARCH cannot look inside its own unreadable directories: chown./databy hand then. - A capability-less uid 0 still owns the image: under
cap_drop: [SETUID]or[SETGID], or acap_addset
without SETUID or SETGID, a compromised controller could rewrite the image and regain the container's capabilities
on a restart. The entrypoint names them; prefercap_drop: [ALL],user: "10002:10002", orread_only: true
with a tmpfs for/tmp, which screenshot OCR needs (docs/deployment.md). - A controller that cannot switch users (
cap_drop: [ALL],cap_drop: [SETUID]or[SETGID], orcap_add
without SETUID or SETGID) starts as a uid 0 with no capabilities at all, and refuses to start, in one line, when
it cannot enter the API token's directory or read the token the controller would read. A directory it cannot see,
not there yet or under one it cannot search, is left to the controller, which creates it or says why it cannot.
Run it asuser: "10002:10002", or addcap_add: [CHOWN, DAC_READ_SEARCH, SETUID, SETGID](docs/deployment.md).
Data owned by 65534 (root-squashed NFS) runs as 65534.
Security
- Chromium runs with its own sandbox in browser-node and every isolated container (
BROWSER_SANDBOX=auto, the
default). It launched with the sandbox off, so a renderer exploit ran with every syscall the container allowed.
browser-node now checks in/procthat renderers really are sandboxed; where a host refuses the user namespace
(Ubuntu 23.10 and later restrict it with AppArmor),autoruns Chromium unsandboxed with a WARNING that names the
cause,onrefuses to start, andoffkeeps the old behaviour. Readiness reports every browser's state. - The controller no longer runs as root (see the upgrade notes).
- A closed isolated session's profile and downloads are removed. A controller that is not root could not delete
the browser user's files, and the error was ignored, so the session's cookies stayed on disk; they are now emptied
as the browser user, and anything left is a WARNING with its path. - The controller reads the browsers' endpoint files without following links, and no error quotes their
content: a link planted at a Playwright endpoint file could have put a file the controller can read, its API token
included, into its log. An endpoint is a short ws:// or wss:// URL in printable ASCII, or it is refused.
Fixed
- When screenshot PII scrubbing cannot run, the WARNING (and the error, under
PII_SCRUB_SCREENSHOT_STRICT=true)
says what its reason needs. Both said to run the Docker image, which ships tesseract, even when the controller
already ran it and its OCR run failed, for example for want of a writable temporary directory under
read_only: true. A failed OCR run now names that check,OCR_ENABLED=falsenames the setting, and the cause of
the failure is logged at DEBUG.
Added
- A weekly CI check (
scripts/check_chromium_freshness.py) fails when Chrome stable for Linux is a major ahead of the
Chromium the pinned Playwright bundles, when a newer Playwright bundles a newer Chromium, or when moby's default
seccomp profile has moved away from the one the sandbox profile is built from.
Changed
- CI's compose-smoke prints each browser's measured sandbox state, and checks that the controller runs as the owner
of./data.