feat: add AZKABAN_UNMASK_PROC for nested sandboxes #67

Merged
claudiomaradonna merged 3 commits from feat/unmask-proc into master 2026-11-09 14:16:28 +01:00
Collaborator

Why

Since #65, Claude Code's sandbox starts in a Linux client container,
but only with sandbox.enableWeakerNestedSandbox. Podman masks parts of
the container's /proc, so the kernel refuses the fresh /proc that
the regular sandbox mounts. The weaker sandbox binds the container's
/proc instead, which lets its commands see every process in the
container. The diagnosis of #64 proposed an opt-in control to lift the
masks, and that work left it out. Lifting them by default, as #64
suggested, was set aside: it would expose the masked paths to every
launch of every client.

What changed

AZKABAN_UNMASK_PROC=1 in an identity's env or env.<client>, or
--unmask-proc for a single launch, adds --security-opt unmask=/proc/*
to the Podman launch, ahead of the image. Without it, launches stay as
they were. Any value other than 0 or 1 stops the launch before Podman
runs. On macOS, container run has no option for this, so the launch
goes on without the unmask and prints one line saying so.

The smoke gate now also runs each image with the unmask. It requires a
bwrap jail that mounts its own /proc, whose PID 1 differs from the
container's shell, with a nested user namespace inside it. Nested
sandboxes, Launcher controls, the README's options table and Security
model, and the macOS differences describe the control and what it
exposes.

How to verify

On a Linux host with rootless Podman, from the branch:

  1. ln -s "$PWD/bin/azkaban" /tmp/azk/claude, then run
    /tmp/azk/claude --unmask-proc --dry-run .. The output shows
    --security-opt unmask=/proc/\* after --http-proxy=false. On
    master, --unmask-proc lands after the image, as a Claude argument.
  2. Launch with --unmask-proc, with sandbox.enabled on and without
    sandbox.enableWeakerNestedSandbox. Have Claude Code run
    ls -d /proc/[0-9]*. It lists only the sandbox's processes, with no
    sandbox error.
  3. make test-e2e passes and includes the new probe.

Verify (make lint && .agents/hooks/shellcheck.sh repo && make test)
passes on the tip: 352 tests, 0 failures. The new and extended
launcher, help, and smoke unit tests all fail on master. macOS is
covered by the stubbed Darwin tests only, since no Mac was at hand.

Considerations

With the unmask on, every process in the container reads the unmasked
paths with the host user's permissions. /proc/keys, for example,
lists the metadata of the host user's kernel keys, though not their
contents. Nothing becomes writable, because host root owns
/proc/sys and its siblings. The README's Security model lists the
unmask among the settings to review.

## Why Since #65, Claude Code's sandbox starts in a Linux client container, but only with `sandbox.enableWeakerNestedSandbox`. Podman masks parts of the container's `/proc`, so the kernel refuses the fresh `/proc` that the regular sandbox mounts. The weaker sandbox binds the container's `/proc` instead, which lets its commands see every process in the container. The diagnosis of #64 proposed an opt-in control to lift the masks, and that work left it out. Lifting them by default, as #64 suggested, was set aside: it would expose the masked paths to every launch of every client. ## What changed `AZKABAN_UNMASK_PROC=1` in an identity's `env` or `env.<client>`, or `--unmask-proc` for a single launch, adds `--security-opt unmask=/proc/*` to the Podman launch, ahead of the image. Without it, launches stay as they were. Any value other than 0 or 1 stops the launch before Podman runs. On macOS, `container run` has no option for this, so the launch goes on without the unmask and prints one line saying so. The smoke gate now also runs each image with the unmask. It requires a `bwrap` jail that mounts its own `/proc`, whose PID 1 differs from the container's shell, with a nested user namespace inside it. Nested sandboxes, Launcher controls, the README's options table and Security model, and the macOS differences describe the control and what it exposes. ## How to verify On a Linux host with rootless Podman, from the branch: 1. `ln -s "$PWD/bin/azkaban" /tmp/azk/claude`, then run `/tmp/azk/claude --unmask-proc --dry-run .`. The output shows `--security-opt unmask=/proc/\*` after `--http-proxy=false`. On master, `--unmask-proc` lands after the image, as a Claude argument. 2. Launch with `--unmask-proc`, with `sandbox.enabled` on and without `sandbox.enableWeakerNestedSandbox`. Have Claude Code run `ls -d /proc/[0-9]*`. It lists only the sandbox's processes, with no sandbox error. 3. `make test-e2e` passes and includes the new probe. Verify (`make lint && .agents/hooks/shellcheck.sh repo && make test`) passes on the tip: 352 tests, 0 failures. The new and extended launcher, help, and smoke unit tests all fail on master. macOS is covered by the stubbed Darwin tests only, since no Mac was at hand. ## Considerations With the unmask on, every process in the container reads the unmasked paths with the host user's permissions. `/proc/keys`, for example, lists the metadata of the host user's kernel keys, though not their contents. Nothing becomes writable, because host root owns `/proc/sys` and its siblings. The README's Security model lists the unmask among the settings to review.
A nested sandbox that mounts its own /proc, as Claude Code's regular
sandbox does, cannot start in a Linux client container, because the
kernel refuses a fresh procfs mount while Podman's default masks cover
parts of the container's /proc. AZKABAN_UNMASK_PROC=1, or
--unmask-proc for one launch, adds --security-opt unmask=/proc/* to
the Podman launch; it stays off unless set.

Apple container's run takes no option to lift /proc masks, so a macOS
launch that asks for the unmask goes on without it and says so on
standard error.
AZKABAN_UNMASK_PROC exists so a nested sandbox can mount a fresh /proc,
as Claude Code's regular sandbox does. The gate now runs each image as
the launcher runs it with the control and requires a bwrap jail there
to mount its own /proc, whose PID 1 is not the container's shell, and
to nest a user namespace, so a base that loses this fails the release
gate rather than a user's sandbox.
docs: document --unmask-proc for nested sandboxes
All checks were successful
publish-images / publish (push) Successful in 37m39s
d433a6e537
Nested sandboxes now names the control as the way for Claude Code's
regular sandbox to start, and what the unmask lets every process in the
container read, keeping enableWeakerNestedSandbox for launches without
it. The launcher controls, the README's options, the macOS differences,
and the development note on the repository's sandbox list it.
claudiomaradonna deleted branch feat/unmask-proc 2026-11-09 14:16:32 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
claudiomaradonna/azkaban!67
No description provided.