Skip to content

feat(runner): start the sandbox on stock Ubuntu, or refuse with the AppArmor profile - #435

Merged
kixelated merged 3 commits into
mainfrom
quest/m1/kix/sandbox/apparmor
Nov 3, 2026
Merged

kixelated merged 3 commits into
mainfrom
quest/m1/kix/sandbox/apparmor

Conversation

@kixelated

@kixelated kixelated commented Nov 3, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes Ubuntu AppArmor: a runner started from an ordinary terminal on Ubuntu 23.10+ now either sandboxes jobs or refuses to start, printing the profile that fixes it.

Summary

  • Startup probe (src/runner/sandbox.ts sandboxRefusal, called from runnerAdapters the first time an account needs an adapter on Linux): runs true in the namespaces Claude Code's sandbox and sandbox-runtime use (--unshare-user --unshare-pid --unshare-net --proc /proc …), nested inside a second bwrap. The nesting matters because Claude Code's seccomp helper makes its own nested user namespace inside the sandbox.
  • Refusal: if /proc/sys/kernel/apparmor_restrict_unprivileged_userns is 1 and bwrap is in the Nix store (the runner package's), the error prints a ready-to-run sudo tee /etc/apparmor.d/quest-bwrap … && sudo apparmor_parser -r … with the profile filled in for that exact store path. For a system bwrap, it points to the runner package instead. Without the AppArmor restriction, it says unprivileged user namespaces are needed.
  • Profile: profile quest-bwrap <store path> flags=(unconfined) { userns, }, the form Ubuntu ships for other self-sandboxing programs (and the Claude desktop profile that made moq-dev/quest#212 work).
  • README documents the probe, the profile, and why it has this shape. A test keeps the README's copy identical to bwrapProfile.
  • Removes the quest and its link from the m1 sandbox epic.

Verification

On this host, stock Ubuntu 26.04.1 with kernel.apparmor_restrict_unprivileged_userns = 1. Every run was from an unconfined process started with systemd-run --user, which has the same AppArmor label (unconfined) as a normal login shell. The profiles were loaded only for the test, with apparmor_parser -r from the checkout's .tmp/, and unloaded afterwards.

Profile loaded Probe srt 0.0.81 + apply-seccomp
none refuses: loopback: Failed RTM_NEWADDR, prints profile fails (same)
Ubuntu-style capability-stripping copy (first draft) refuses: No permissions to create a new namespace fails: apply-seccomp: … nested userns is capability-restricted
this PR's profile passes runs; writes outside allowed paths denied; unshare -Ur inside the job refused
  • Without the helper (the Nix srt that opencode uses ships no apply-seccomp), unshare -Ur inside the job is still refused, because bwrap's --disable-userns applies (No space left on device).
  • The profile parses with Ubuntu 24.04's apparmor_parser 4.0.1 (-Q -K, in an ubuntu:24.04 container). I couldn't check 23.10: it is end-of-life and its apt repositories are gone.
  • just check and just test pass.
  • Not run: a real quest runner taking a Claude Code job end to end, which needs a kix login. Claude Code 2.1.305 embeds the same apply-seccomp (its binary contains the helper's messages), and the probe reproduces that helper's requirement.

Choices made without asking

  • Exact store path, not a glob. Under this profile bwrap and its children may create capable user namespaces. With a glob like /nix/store/*-bubblewrap-*/bin/bwrap, anyone able to build with Nix could add a matching store path and get around Ubuntu's restriction. The cost: a runner update that changes bubblewrap refuses to start until the newly printed profile is installed. The alternative is a glob for convenience.
  • Unconfined userns profile, not Ubuntu's capability-stripping bwrap-userns-restrict design. The stripping version breaks Claude Code's seccomp helper (see the table), and the helper can't be granted capabilities alone, because Claude Code extracts it from its own binary at no stable path. Job commands still can't make user namespaces: the helper's seccomp filter refuses them, or bwrap's --disable-userns does.
  • No profile printed for a system bwrap. A second profile attaching /usr/bin/bwrap beside Ubuntu's bwrap-userns-restrict left neither attached (tested: bwrap then failed outright), so the refusal points to the runner package instead.
  • Probe for every account kind, not only Claude Code (the Plan said Claude Code): opencode's sandbox-runtime uses the same bwrap. The nested check is stricter than opencode needs today, since its Nix srt runs no seccomp helper, but it is the same requirement once srt ships the helper.

Follow-ups

  • The Nix sandbox-runtime package ships no apply-seccomp, so opencode jobs run without srt's Unix-socket blocking (srt logs apply-seccomp binary not available - unix socket blocking disabled). Worth a quest to package the helper, or to set seccomp.applyPath.

Review

  • Reviewer: Codex (quest review), gpt-6.1-sol, medium effort.
  • Round 1 on 8e9a0484: one finding, [P1] "the stripping profile breaks the seccomp helper, and the probe only runs true". Fixed: profile switched to unconfined userns, probe nested, reproduced before and after (table above).
  • Round 2 on 9b0c69635e9c07efdb47128321f081fa03117ad9: no findings.

(Written by Claude Opus 5.5)

🤖 Generated with Claude Code

kixelated and others added 3 commits November 2, 2026 19:43
On Ubuntu 23.10 and later, AppArmor keeps unprivileged user namespaces to
programs whose profile grants them, and Ubuntu's bwrap profile covers only
/usr/bin/bwrap, so the runner's Nix-pinned bwrap failed every job when run
from a normal login shell. Before taking jobs, the runner now runs a
trivial command in the sandbox's namespaces and refuses with the AppArmor
profile to install, for that exact store path, if it fails. The profile
copies Ubuntu's bwrap-userns-restrict: bubblewrap gets namespaces while the
commands it runs keep no capabilities. The README documents it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…profile

Claude Code's seccomp helper makes a nested user namespace inside the
sandbox for the capabilities it needs, so a profile copying Ubuntu's
bwrap-userns-restrict, which strips capabilities from bwrap's children,
passed the startup probe but broke every command. The profile is now the
unconfined userns form Ubuntu gives other self-sandboxing programs, the
probe nests a second bwrap so it catches the stripped case, and a system
bwrap gets no profile, since a second one for /usr/bin/bwrap disables
Ubuntu's own and leaves neither attached.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated kixelated changed the title Start the sandbox on stock Ubuntu feat(runner): start the sandbox on stock Ubuntu, or refuse with the AppArmor profile Nov 3, 2026
@kixelated
kixelated marked this pull request as ready for review November 3, 2026 04:06
@coderabbitai

coderabbitai Bot commented Nov 3, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 51 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: d3dc5c63-ee1d-45ad-af72-ab1976d61e10
Commits

Reviewing files that changed from the base of the PR and between acb8682 and 9b0c696.

Files selected for processing: 6
  • README.md
  • quest/m1/kix/sandbox/README.md
  • quest/m1/kix/sandbox/apparmor.md
  • src/runner/cli.ts
  • src/runner/sandbox.ts
  • test/runner.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@kixelated
kixelated added this pull request to the merge queue Nov 3, 2026
Merged via the queue into main with commit f1cfe6c Nov 3, 2026
19 checks passed
@kixelated
kixelated deleted the quest/m1/kix/sandbox/apparmor branch November 3, 2026 16:55
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