Skip to content

fix(sandbox): charge control slots only after bearer authentication - #3742

Open
drew wants to merge 3 commits into
mainfrom
fix/sandbox-control-unauthenticated-slots
Open

drew wants to merge 3 commits into
mainfrom
fix/sandbox-control-unauthenticated-slots

Conversation

@drew

@drew drew commented Sep 27, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

The sandbox control listener presents a server certificate only and authenticates each RPC with the EdDSA session bearer. A connection that merely completed TLS was still charged one of the 128 control slots and could hold it indefinitely. Because the Docker and Podman control socket is reachable by same-UID workload processes (Landlock does not govern connect() on a filesystem socket), a hostile workload could exhaust the capacity the supervisor needs to reconnect, which after 30 seconds terminates the workload. This PR charges slots only after authentication, bounds and times out connections awaiting a bearer, and tightens the Unix peer check.

Related Issue

No issue required: obvious localized hardening of existing sandbox control-channel admission; no user-visible behavior or API change.

Changes

  • Acquire the control slot on the first authenticated RPC of a connection and return RESOURCE_EXHAUSTED when none is available, instead of charging it after TLS.
  • Arm a 10-second deadline on every connection until a bearer is accepted. A valid bearer replaces it with the session expiry.
  • Limit unauthenticated gRPC transports to 128 permits, held until a connection authenticates or closes. When full, close the oldest waiting peer and hold the new connection's handshake permit until a pool permit is released or its original 10-second deadline expires.
  • Make the gRPC transport report EOF when the connection shutdown fires. Graceful HTTP/2 shutdown alone did not tear down a peer that completed TLS but never sent a preface.
  • Admit same-PID-namespace Unix peers only when they are the sandbox itself or one of its ancestors. The previous descendant-only rule was bypassable by an orphan reparented to PID 1 or a subreaper when the sandbox is not PID 1 (for example a Docker daemon with init: true).
  • Correct three comments that described the channel as mutual TLS and document the trust model in architecture/sandbox.md.

Related observations (not changed here)

Reviewing the sandbox/agent split also surfaced design-level points worth a separate discussion. None are addressed by this PR:

  • Network rules grant access to any descendant of a listed binary (documented). Since the agent entrypoint is always listed for its own API host, every process the agent spawns inherits that grant, so binary scoping only constrains hosts the entrypoint itself cannot reach.
  • Binary identity names the executable, not the code it runs. Interpreters and the dynamic loader are documented cases; environment-driven library loading into an allowed binary is another, and Landlock grants execute on writable policy paths.
  • Same-UID procfs access to dumpable workload children is permitted by the seccomp filter and the Landlock baseline.

Testing

  • mise run pre-commit passes
  • Unit tests added/updated
  • mise run test passes
  • mise run e2e:docker passes

New tests in the boundary server Linux module: a raw TLS client with no bearer holds no slot and is closed at the deadline; gRPC with a missing or malformed bearer holds no slot while a valid bearer charges exactly one slot and leaves the unauthenticated pool; an authenticated RPC is refused when slots are exhausted; a full unauthenticated pool closes its oldest connection and waits for the permit before admitting a new peer; a connection waiting for that permit keeps the original deadline. Ran the full boundary_server module (35 tests), mise run pre-commit, mise run test, and mise run e2e:docker on Linux ARM64. The earlier sandbox and Docker-driver Clippy checks in a rust:1.95-bookworm container also passed.

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

🤖 Generated with Claude Code

The sandbox control listener requires no TLS client certificate; peers
prove themselves with the EdDSA session bearer on each RPC. A connection
that completed TLS was nevertheless charged one of the 128 control slots
and held it indefinitely, so a same-UID workload process reaching the
Unix socket could exhaust the capacity the supervisor needs to reconnect.

- Acquire the control slot on the first authenticated RPC of a connection
  and return RESOURCE_EXHAUSTED when none is available.
- Arm a 10-second deadline on every connection until a bearer is accepted,
  and make the gRPC transport report EOF on shutdown so a peer that never
  sent an HTTP/2 preface is actually torn down.
- Admit same-namespace Unix peers only when they are the sandbox or one
  of its ancestors, so an orphan reparented to PID 1 or a subreaper is
  rejected even when the sandbox is not PID 1.
- Correct comments that described the channel as mutual TLS and document
  the trust model in the sandbox architecture doc.

Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Signed-off-by: Drew Newberry <anewberry@nvidia.com>

This branch has not been deployed

No deployments
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