Skip to content

fix(sandbox): post-merge audit — exit codes, reserved dsts, root-uid, egress tests#103

Merged
dangtony98 merged 2 commits intomainfrom
fix/sandbox-container-audit
Apr 23, 2026
Merged

fix(sandbox): post-merge audit — exit codes, reserved dsts, root-uid, egress tests#103
dangtony98 merged 2 commits intomainfrom
fix/sandbox-container-audit

Conversation

@dangtony98
Copy link
Copy Markdown
Contributor

Summary

Post-merge audit of the container-mode sandbox (PR #99 + follow-up --share-agent-dir work). Four must-fix items from the audit:

  • Exit-code propagation: container's real exit status now propagates to the parent via a new ExitCodeError sentinel unwrapped in Execute(). Previously every non-zero exit collapsed to 1, which broke CI use cases like vault run -- pytest. Defers still run — the error returns normally through Cobra before Execute() unwraps and os.Exit(Code)s.
  • Root-uid guard for --share-agent-dir: reject uid == 0 on Linux. The usermod/groupmod remap would hand the in-container claude user uid 0 alongside NET_ADMIN/NET_RAW/SETUID/SETGID/KILL caps, and no-new-privileges doesn't disarm ambient caps on root.
  • Expanded reservedContainerDsts: /, /etc (subtree), ContainerClaudeConfig (added by --share-agent-dir but never reserved), and both /usr/local/sbin/{init-firewall,entrypoint}.sh. Without ContainerClaudeConfig on the list, a user --mount could override the bind-mounted ~/.claude.json.
  • Egress-bypass integration tests: TestIntegration_EgressBlocked_Bypasses covers the channels a compromised agent would actually try: IPv6 literal, UDP, ICMP, curl --noproxy '*', and an env-stripped HTTPS_PROXY bypass. Shared runInFirewalledContainer helper also collapses the existing end-to-end test's duplicated docker argv. iputils-ping baked into the image so the ICMP probe doesn't apt-get install per test run (asset hash bumped).

Test plan

  • make test green (all unit tests)
  • go vet -tags docker_integration ./... clean
  • go test -tags docker_integration ./internal/sandbox/ -run Integration -v on Linux + macOS (reviewer, please run — requires Docker)
  • Manual: ./agent-vault run --sandbox=container -- sh -c 'exit 42' returns 42 (not 1)
  • Manual: ./agent-vault run --sandbox=container --mount /tmp/x:/home/claude/.claude.json -- true rejects
  • Manual on Linux: sudo ./agent-vault run --sandbox=container --share-agent-dir -- true rejects with the new error

🤖 Generated with Claude Code

… egress tests

- Propagate the container's real exit status through an ExitCodeError
  sentinel. Previously every non-zero container exit collapsed to the
  parent returning 1, breaking CI use cases like `vault run -- pytest`.
  Defers still run because the error returns up through Cobra before
  Execute() unwraps and os.Exit(Code)s.

- Reject `uid == 0` on Linux with --share-agent-dir. The usermod/groupmod
  remap would hand the in-container claude user uid 0 alongside the
  NET_ADMIN/NET_RAW/SETUID/SETGID/KILL caps, and no-new-privileges
  doesn't disarm ambient caps on root.

- Expand reservedContainerDsts: /, /etc (subtree), ContainerClaudeConfig
  (added by --share-agent-dir but never reserved), and both
  /usr/local/sbin/{init-firewall,entrypoint}.sh. Without
  ContainerClaudeConfig on the list, a user --mount could override the
  bind-mounted ~/.claude.json.

- Add TestIntegration_EgressBlocked_Bypasses covering the channels a
  compromised agent would actually try: IPv6 literal, UDP, ICMP,
  curl --noproxy '*', and an env-stripped HTTPS_PROXY bypass. Shared
  runInFirewalledContainer helper collapses the old end-to-end
  test's duplicated argv too.

- Bake iputils-ping into the image so the ICMP probe doesn't apt-get
  install it per test run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread internal/sandbox/docker.go
Comment thread cmd/run_container.go
- validateContainerDst was asymmetric: it caught `dst == reserved` and
  `dst inside reserved`, but not `reserved inside dst`. A user could
  `--mount /tmp/evil:/usr/local/sbin` to silently shadow both
  entrypoint.sh and init-firewall.sh, so ENTRYPOINT would execute
  attacker code as PID 1 / UID 0 with NET_ADMIN/NET_RAW/SETUID/SETGID/
  KILL before any iptables rule got installed. Same gap covered the
  new ContainerClaudeConfig reservation (bypassable via /home/claude).
  Add the symmetric prefix check; extend the test table with the
  parent paths (/usr/local/sbin, /usr/local, /usr, /home/claude, /home).

- cobra's SilenceErrors and SilenceUsage are independent gates — setting
  only the former still dumps the full usage block on every non-zero
  container exit, turning `vault run -- pytest` into a wall of help
  text on test failure. Set both on the exit-code path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@dangtony98 dangtony98 merged commit 2b8e020 into main Apr 23, 2026
4 checks passed
@dangtony98 dangtony98 deleted the fix/sandbox-container-audit branch April 23, 2026 05:32
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