Skip to content

feat(policy): let a binaries entry match only the connection-owning executable, not its descendants #4943

Open

Description

@bmbouter

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:

  1. Import providers/claude-code.yaml and attach it to a sandbox running Claude Code.
  2. Have Claude run curl -s https://api.anthropic.com/v1/messages -H "x-api-key: $ANTHROPIC_API_KEY" ... from its Bash tool.
  3. The request is allowed, with the ancestor /usr/bin/claude as 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/claude matches the entry; a process it starts does not.

  • Consider defaulting provider-credentialed endpoints to leaf, or have openshell provider profile lint warn 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 binaries entry 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 curl run 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.mdx and best-practices.mdx describe both match modes.

Alternatives Considered

  • Request middleware on the credentialed endpoint. A middleware cannot tell which process sent a request today: originating_process is still unset in v0.1.4 and on main (feat(supervisor): populate RequestContext.originating_process for 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 binaries list 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

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions