Skip to content

Isolate the agent bash tool with Landlock on Linux#60

Merged
Anton-Horn merged 1 commit into
mainfrom
feat/landlock-bash-sandbox
Jun 9, 2026
Merged

Isolate the agent bash tool with Landlock on Linux#60
Anton-Horn merged 1 commit into
mainfrom
feat/landlock-bash-sandbox

Conversation

@Anton-Horn

Copy link
Copy Markdown
Contributor

Why

bubblewrap (the Linux backend of @anthropic-ai/sandbox-runtime) can't engage on the hardened OCD deploy — the container is capless root (CapEff=0) and the host blocks unprivileged user namespaces — so the agent's bash silently fell back to running unsandboxed. A prompt-injected or buggy command could read/write sibling projects under the projects root. (PRs #58/#59 confirmed bubblewrap is a dead end here.)

Landlock is the one FS-sandbox primitive that fits: it needs no capabilities and no userns, only PR_SET_NO_NEW_PRIVS — which the container already sets. Networking is untouched, so the zero CLI ↔ loopback proxy keeps working (the thing that forced us off bubblewrap's --unshare-net).

Verified on the prod box (ocd ssh zero-server)

  • Kernel 6.8, capless root, NoNewPrivs=1Landlock ABI v4 usable (not seccomp-blocked: create_ruleset/add_rule/restrict_self all succeed).
  • The actual compiled helper runs a real bash that reads/writes its own project ✓ and is denied read + write + even ls of a sibling project ✓ — while staying a functional shell.

What changed

  • server/landlock-exec/zero-landlock.c — self-contained C helper (UAPI structs inline, no kernel-header dep). Applies a deny-by-default Landlock ruleset from --rw/--ro/--rwfile flags, then execvps the command. Fail-closed; --check mode for probing.
  • server/Dockerfile — compiles it to /usr/local/bin/zero-landlock, creates /var/empty.
  • project-sandbox/index.ts — on Linux, probes zero-landlock --check at session_start and routes bash through it (--rw <projectDir> --rw /tmp --ro <system+pkg roots> --rwfile /dev/* -- bash -c <cmd>). The projects root is never granted, so siblings are denied by allowlist default. macOS keeps sandbox-exec; if Landlock is unavailable it falls back to unsandboxed with a notify. Covers parent turns and subagents (shared factory).

Related gaps closed while here

  • Bash env secret scrub — strip JWT_SECRET, CREDENTIALS_KEY, OPENROUTER_API_KEY, BRAVE_SEARCH_API_KEY, TELEGRAM_WEBHOOK_SECRET (+ a secret-shaped name pattern) from the bash child env. The zero CLI reaches models/search via the in-process proxy, so it never needs them. The per-turn proxy token is re-added after the scrub.
  • Relocate .chrome-state.json — browser storageState (cookies/localStorage/IndexedDB) moves out of the project dir to a sibling root (chromeStateFileFor), since Landlock grants the whole project dir rw and can't do per-file deny. Migrated on next browser open so existing sessions aren't logged out. Removes the now-redundant in-process deny machinery; keeps the snapshot exclude for un-migrated stragglers.

Notes / scope

  • tsc --noEmit clean for project source (pre-existing s3lite node_modules errors unrelated).
  • On a Linux dev box without the compiled helper, bash gracefully degrades to unsandboxed (binary only built in the image). macOS dev is unaffected.
  • scripts/landlock-probe.py is included as a standalone diagnostic.

🤖 Generated with Claude Code

bubblewrap (sandbox-runtime's Linux backend) can't engage on the hardened
OCD deploy — capless root + blocked unprivileged userns — so bash silently
ran unsandboxed and a prompt-injected command could read/write sibling
projects. Landlock fits the constraints: it needs no caps and no userns,
only PR_SET_NO_NEW_PRIVS (already set on the container). Verified on the
prod box: kernel 6.8, Landlock ABI v4 usable (not seccomp-blocked), and a
real contained bash reads/writes its own project but is denied read/write/
list on siblings.

- server/landlock-exec/zero-landlock.c: self-contained C helper (UAPI
  structs inline, no kernel-header dep). Applies a deny-by-default Landlock
  ruleset from --rw/--ro/--rwfile flags then execs the command; fail-closed.
- server/Dockerfile: compile it to /usr/local/bin/zero-landlock; mkdir /var/empty.
- project-sandbox: on Linux, probe `zero-landlock --check` at session_start
  and route bash through it (projects root never granted -> siblings denied
  by default); macOS keeps sandbox-exec; unavailable -> unsandboxed fallback.
  Network untouched, so the zero CLI -> loopback proxy keeps working.

Also closes two related gaps surfaced while doing this:

- Scrub the server's own secrets (JWT_SECRET, CREDENTIALS_KEY,
  OPENROUTER_API_KEY, BRAVE_SEARCH_API_KEY, TELEGRAM_WEBHOOK_SECRET, plus a
  secret-shaped name pattern) from the bash child env. The zero CLI reaches
  models/search via the in-process proxy, so it never needs them.
- Relocate browser storageState (.chrome-state.json) out of the project dir
  to a sibling root (chromeStateFileFor), since Landlock grants the whole
  project dir rw and can't do per-file deny. Migrated on next browser open so
  existing sessions aren't logged out. Drops the now-redundant in-process
  deny machinery; keeps the snapshot exclude for un-migrated stragglers.
@Anton-Horn Anton-Horn merged commit 3e9c155 into main Jun 9, 2026
2 checks passed
@Anton-Horn Anton-Horn deleted the feat/landlock-bash-sandbox branch June 9, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant