Repository navigation
feat(policy): let a binaries entry match only the connection-owning executable, not its descendants #4943
Description
User Story
I run OpenShell 0.1.4 on a home server to sandbox a fleet of always-on agents, using the Podman compute driver and provider-credentialed model endpoints.
I need a model endpoint's credential to be usable only by the agent CLI the policy names (Claude Code), not by every process it starts. I directly encountered the gap: a curl that Claude Code's Bash tool ran against a Vertex AI rawPredict path was allowed, and the only binary the policy entry for that endpoint names is claude.
This means the agent's own tools, and anything a prompt-injected shell command starts, can use the model credential.
Problem Statement
binary_allowed in sandbox-policy.rego (#L154-L172 @ v0.1.4; the same lines on main) matches a listed path against the leaf executable, against any ancestor ("ancestor exact path (e.g., claude spawns node)"), and, for globs, against the leaf or any ancestor. This is documented: "A rule also applies to processes that a listed binary starts" (docs/how-it-works/policies/network-rules.mdx), and docs/security/best-practices.mdx says policy "can authorize either the connection-owning executable or one of its executable ancestors."
NetworkBinary has only path (sandbox.proto#L352-L356 @ v0.1.4), so an operator cannot opt out of ancestor matching. The Windows MXC path already moved to socket-owner semantics in #3491 ("an allowed Windows entry process no longer lends its binary-scoped network permissions to arbitrary descendants"), so the two platforms now differ.
Impact / Why This Matters
For provider-credentialed endpoints, every descendant of an agent CLI gets the agent's credential. With the shipped providers/claude-code.yaml (binaries: [/usr/bin/claude, /usr/local/bin/claude]), a curl or python3 started by Claude Code's Bash tool can reach api.anthropic.com, and the proxy injects the real API key for it. The same holds for Vertex profiles and the gateway-refreshed Google token.
To see it:
- Import
providers/claude-code.yamland attach it to a sandbox running Claude Code. - Have Claude run
curl -s https://api.anthropic.com/v1/messages -H "x-api-key: $ANTHROPIC_API_KEY" ...from its Bash tool. - The request is allowed, with the ancestor
/usr/bin/claudeas the matching binary, and the proxy substitutes the real key for the placeholder.
Today an operator has no setting that stops this: the only way to keep the credential from the agent's tools is to not attach the provider at all.
Proposed Design
-
An optional per-entry match mode on
binaries, available in both sandbox policies and provider profiles. Notional:binaries: - path: /usr/bin/claude match: leaf # leaf | leaf_or_ancestor (default, today's behaviour)
With
match: leaf, only a connection whose owning executable is/usr/bin/claudematches the entry; a process it starts does not. -
Consider defaulting provider-credentialed endpoints to
leaf, or haveopenshell provider profile lintwarn when a credentialed profile relies on ancestor matching. -
OCSF network events record whether the leaf or an ancestor authorized the connection, and which one, so operators can measure the impact before tightening.
Scope: leaf-only matching is necessary but not sufficient. As #1496 showed, claude is itself an interpreter (BUN_OPTIONS="--preload ..."), so a descendant can re-exec the listed binary with injected code. A full fix pairs this with environment controls on exec, or with credential scoping.
Acceptance Criteria
- A
binariesentry in a sandbox policy or provider profile can be marked leaf-only. - With a leaf-only entry, a process started by the listed binary (for example
curlrun from Claude Code's Bash tool) is denied on that endpoint, and no credential is injected for it; the listed binary itself still connects with the credential. - Policies and profiles that do not set the field behave exactly as today.
- The network allow event shows whether the leaf or an ancestor matched, with that binary's path.
-
network-rules.mdxandbest-practices.mdxdescribe both match modes.
Alternatives Considered
- Request middleware on the credentialed endpoint. A middleware cannot tell which process sent a request today:
originating_processis still unset in v0.1.4 and on main (feat(supervisor): populateRequestContext.originating_processfor supervisor middleware #4269). Even once it is set, it would only re-check a decision the policy has already made by ancestor, on every credentialed endpoint, which is the policy's job. - Provider profiles. A profile's
binarieslist is matched the same way, with ancestors, and credential selection itself is keyed only on host, port and path, so no provider option scopes a credential to one executable. - Not attaching the provider and giving the agent no model credential. The agent then cannot reach its model.
- One sandbox per tool. The agent's tools run as its own child processes, so they cannot be split into another sandbox.
Agent Investigation
- Credential selection is keyed only on host, port and path (
crates/openshell-core/src/provider_credentials.rs:resolver_and_body_classifier_for_endpoint, #L347 @ main, andstatic_credential_endpoint_matches, #L1156-L1165 @ main). - Code unchanged in v0.1.5 and main 1d880c9.
- Related: refactor(sandbox): remove binary path allowlisting entirely #1496 (binary allowlisting critique), fix(network): scope Windows egress by socket owner #3491 (Windows socket-owner fix), fix(sandbox): stop using cmdline paths for binary policy matching #345 (cmdline paths removed, ancestor matching kept), feat(policy): revalidate running subagents when ancestor authority changes #3808 (revalidating subagents when ancestor authority changes; a related view of authority inherited through the process tree), feat(supervisor): populate
RequestContext.originating_processfor supervisor middleware #4269 (originating_processfor middleware),examples/policy-advisor/README.md(documents inheritance).
Note: this content was drafted by Claude, an AI assistant, working on behalf of Brian Bouterse and in collaboration with him. It is AI generated and human reviewed.
Checklist
- I've reviewed existing issues and the published docs
- This is a design proposal, not a "please build this" request
Activity