diff --git a/docs/about/overview.mdx b/docs/about/overview.mdx index 34b8b0cecb..63ada5a910 100644 --- a/docs/about/overview.mdx +++ b/docs/about/overview.mdx @@ -42,7 +42,7 @@ OpenShell applies defense in depth across the following policy domains. | Process | Blocks privilege escalation and dangerous syscalls. | Locked at sandbox creation. | | Provider credentials | Resolves opaque credential placeholders only at profile-authorized endpoints. | Attachments, rotation, and revocation update at runtime; new environment variables require a new process. | -For details, refer to [Customize Sandbox Policies](/how-it-works/policies/overview) and [Default Policy](/how-it-works/policies/default-policy). +For details, refer to [Sandbox Policies](/how-it-works/policies/overview) and [Default Policy](/how-it-works/policies/default-policy). ## Common Use Cases @@ -61,4 +61,4 @@ Explore these topics to go deeper: - To understand the runtime architecture, refer to [Architecture](/about/architecture). - To prepare an image and launch an agent, refer to [Run Your First Agent](/about/run-your-first-agent). -- To learn how OpenShell enforces policy controls across protection layers, refer to [Customize Sandbox Policies](/how-it-works/policies/overview). +- To learn how OpenShell enforces policy controls across protection layers, refer to [Sandbox Policies](/how-it-works/policies/overview). diff --git a/docs/extensibility/isolation-backends.mdx b/docs/extensibility/isolation-backends.mdx index fc1a33b4ee..6b89be1b4d 100644 --- a/docs/extensibility/isolation-backends.mdx +++ b/docs/extensibility/isolation-backends.mdx @@ -43,6 +43,6 @@ crate. `OpenShellRuntimeBackend` is implemented in | Compute driver | Placement and fencing. The driver creates runtime resources, installs the outer egress fence, and supplies a verified descriptor. | | Isolation backend | Common behavior. The backend turns the shared Rust calls into a protected session with the sandbox runtime. | -For how `openshell-sandbox` enforces the boundary, refer to -[Sandbox](/about/architecture/sandbox). For driver-specific workload behavior, -refer to [Runtimes](/how-it-works/sandboxes/runtimes). +For how `openshell-sandbox` enforces the boundary, refer to [Inside the Sandbox +Boundary](/about/architecture#inside-the-sandbox-boundary). For driver-specific +workload behavior, refer to [Runtimes](/how-it-works/sandboxes/runtimes). diff --git a/docs/how-it-works/inference.mdx b/docs/how-it-works/inference.mdx index f01c2cf36a..72c361e654 100644 --- a/docs/how-it-works/inference.mdx +++ b/docs/how-it-works/inference.mdx @@ -34,17 +34,18 @@ curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/providers/nvi ``` In `nvidia-native.yaml`, change `id` to `nvidia-native`, give the profile a -distinct `display_name`, and set `binaries` to the paths that may call the API. -Keep the credential and endpoint definitions you intend to grant. For a Python -workload, the edited fields can look like: +distinct `display_name`, and set `binaries` to the executables that may call the +API, as described in [Binary +Matching](/how-it-works/policies/network-rules#binary-matching). Keep the +credential and endpoint definitions you intend to grant. For a Python workload, +the edited fields can look like: ```yaml id: nvidia-native display_name: NVIDIA Native API binaries: - - /usr/bin/python3 - - /usr/bin/python3.13 - - /usr/local/bin/python + - /usr/bin/python3.* + - /usr/local/bin/python3.* - /sandbox/.venv/** ``` @@ -316,5 +317,5 @@ headers, model selection, request shape, streaming, and timeout behavior. - [Profiles](/how-it-works/providers/profiles) - [Providers](/how-it-works/providers/overview) -- [Customize Sandbox Policies](/how-it-works/policies/overview) +- [Sandbox Policies](/how-it-works/policies/overview) - [Google](/how-it-works/providers/google#vertex-ai) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index b6f7acd0af..3190219c64 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -1,20 +1,88 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Use Policy Advisor" -sidebar-title: "Advisor" -description: "Let sandboxed agents propose narrow policy changes through policy.local while keeping developer approval in the loop." +title: "Policy Advisor" +sidebar-title: "Policy Advisor" +description: "Let an agent in a sandbox propose the network rule it needs when OpenShell blocks a request, and control which proposals take effect." keywords: "Generative AI, Cybersecurity, Policy Advisor, Policy, Sandbox, policy.local, Agent Policy" -position: 7 +position: 5 --- -Policy advisor lets a running sandboxed agent ask for a narrow network policy change after OpenShell denies a request. The agent submits a draft through `policy.local`, a developer approves or rejects it from outside the sandbox, and approved network policy hot-reloads into the same sandbox. - -Policy advisor preserves OpenShell's default-deny posture. The structured rule is the approval contract, and the agent's rationale is supporting context. By default every accepted proposal lands in the draft inbox for human review. Opt-in [auto mode](#approval-modes) approves a proposal without a reviewer only when its [prover delta](#what-auto-approval-checks) is empty and the current draft rule produces no security notes. A prover finding or security note keeps the proposal pending for human review. - -## Enable Policy Advisor - -Policy advisor is disabled by default. Enable it globally when you want every sandbox on the selected gateway to expose the agent proposal surface: +The policy advisor lets an agent in a sandbox propose a new network rule when +OpenShell blocks one of its network requests, and you decide which proposals +take effect. It handles network access only. A proposal can add a network rule, +but it cannot remove rules or change filesystem, Landlock, or process settings. + +Without the policy advisor, an agent whose request is blocked usually cannot +finish its task until someone changes the policy by hand. With it, the agent can +request the access it needs. Because network rules can change while a sandbox +runs, an approved rule takes effect without a restart. + +The policy advisor is off by default. When you enable it, OpenShell gives the +agent instructions and a local HTTP API for submitting proposals. A proposal +never changes the policy on its own. By default, every proposal waits for your +review. You can opt in to automatic approval, which approves a proposal when +OpenShell's risk checks find nothing to flag. + +## How the Policy Advisor Works + +A proposal goes through these steps: + +1. A process in the sandbox makes a network request that the policy denies. When + OpenShell denies an inspected HTTP request, its response explains what was + blocked and tells the agent how to request access. The agent can also look up + recent denials itself. +2. The agent reads the policy advisor guide in the sandbox, checks the current + policy, and submits a proposal. A proposal is a network rule scoped to the + host, port, and binary that need access and, for HTTP APIs, the method and + path. +3. OpenShell validates the proposal and checks it for added risk in two ways. + The policy prover runs a proposal risk check, which compares what the + sandbox could reach with and without the rule. OpenShell also flags + destinations that are often risky, such as private network addresses and + wildcard hosts. OpenShell then adds the proposal and the results of both + checks to the sandbox's pending proposals. Refer to [Proposal Risk + Check](#proposal-risk-check). +4. You approve or reject the proposal. With automatic approval turned on, + OpenShell approves a proposal that passes its risk checks without waiting for + you. +5. The agent waits for the decision. After approval, the sandbox loads the new + rule and the agent retries the request. After a rejection, the agent receives + your reason and can submit a narrower proposal. + +OpenShell also drafts proposals on its own from connections that it blocks, in +every sandbox, whether or not the policy advisor is enabled. This includes +connections to hostnames that no rule names. Each draft allows one binary to +reach one host and port. Drafts go through the same checks and review as +proposals from the agent, and automatic approval applies to them too. The +blocked request stays denied, so retry it after OpenShell approves the draft and +loads the new rule. + +When the policy advisor is enabled, OpenShell adds the following to the sandbox +so the agent can find and use it: + +- A guide at `/etc/openshell/skills/policy_advisor.md` that explains how to + write and submit a proposal. OpenShell also adds short pointers to the guide + where agent tools look for instructions: an agent skill at + `/etc/openshell/skills/policy-advisor/SKILL.md` and, if the image does not + already have one, `/AGENTS.md`. +- The `policy.local` API, which the agent uses to read the current policy and + recent denials, submit proposals, and wait for decisions. OpenShell serves it + at `http://policy.local`, which is reachable only from inside the sandbox and + needs no network rule or proxy setting. Refer to [Agent API](#agent-api). +- Instructions in the response to each denied HTTP request on an inspected + endpoint, which point the agent to the guide and the API. + +## Enable the Policy Advisor + +The `agent_policy_proposals_enabled` setting turns the policy advisor on or off. +You can set it for every sandbox on the gateway or for a single sandbox. Enable +it before you create a sandbox when you can. A running sandbox picks up the +change without a restart, but many agents read instruction files such as +`/AGENTS.md` only when they start, and an agent may already have given up on +requests that were denied before you enabled the policy advisor. + +Enable the policy advisor for every sandbox on the gateway: ```shell openshell settings set --global \ @@ -23,7 +91,8 @@ openshell settings set --global \ --yes ``` -You can also enable it for one sandbox, unless the key is managed globally: +To enable it for one sandbox instead, set the key on that sandbox. This works +only when the key has no gateway-wide value: ```shell openshell settings set \ @@ -31,13 +100,10 @@ openshell settings set \ --value true ``` -Check the effective setting for a sandbox: - -```shell -openshell settings get -``` - -The output shows whether `agent_policy_proposals_enabled` is `global`, `sandbox`, or `unset`. A global value overrides sandbox-scoped values. To return control to sandbox-scoped settings, delete the global key: +To check the setting for a sandbox, run `openshell settings get `. +The output shows whether the value comes from the gateway, the sandbox, or +neither. To let individual sandboxes control the setting again, delete the +gateway-wide value: ```shell openshell settings delete --global \ @@ -45,114 +111,104 @@ openshell settings delete --global \ --yes ``` -Set the value before creating a sandbox when you want the first denied request to include policy advisor guidance. Running sandboxes poll settings and can enable the surface after startup, but startup enablement gives the agent the clearest first-denial path. - -## Approval Modes - -Every proposal, mechanistic or agent-authored, is routed through the [policy prover](#what-auto-approval-checks). The gateway also recalculates security notes from the current draft rule before auto-approval. The `proposal_approval_mode` setting decides whether proposals that pass both checks require human review. - -| Mode | When unset / `manual` | `auto` | -|---|---|---| -| Empty prover delta and no security notes | Lands in the draft inbox for human review. | Approved automatically. The sandbox hot-reloads the new rule and the agent retries. | -| Any prover finding or security note | Lands in the draft inbox. | Remains pending for human review. | - -`manual` is the default. Auto mode is an explicit opt-in; OpenShell's default-deny posture is preserved unless you choose otherwise. +## Review Proposals -Enable auto mode at gateway scope when you want every sandbox on this gateway to auto-approve eligible proposals: +Proposals that are not approved automatically wait for you to approve or reject +them. List a sandbox's pending proposals with the rule each would add, the +agent's reason, and the results of the risk checks: ```shell -openshell settings set --global \ - --key proposal_approval_mode \ - --value auto \ - --yes +openshell rule get --status pending ``` -Enable it for one sandbox when no global value is set: +Approve a proposal when its rule grants only the access you intend. Use the ID +from the proposal's `Chunk` line: ```shell -openshell settings set \ - --key proposal_approval_mode \ - --value auto +openshell rule approve --chunk-id ``` -The shorthand at create time writes the sandbox-scoped setting for you: +Otherwise, reject it with a reason. OpenShell sends the reason and the risk +check findings to the agent, which can submit a narrower proposal: ```shell -openshell sandbox create --approval-mode auto +openshell rule reject \ + --chunk-id \ + --reason "Scope this to docs/ paths only." ``` -Only `manual` and `auto` are accepted; typos like `autom` are rejected at configure time. Stale or unknown values found in storage are still treated as `manual` at runtime as a defense-in-depth measure. - -**Precedence.** Gateway scope wins over sandbox scope. A reviewer can pin `manual` for a fleet by setting it globally; per-sandbox overrides only apply when no global value is set. - -**Audit trail.** Every auto-approval emits a `CONFIG:APPROVED` event with `auto=true`, `source=`, `prover_delta=empty`, and `resolved_from=` so operators can reconstruct why a given approval ran without human review. +If the sandbox's policy or providers change after a proposal is submitted, +OpenShell rechecks the proposal and asks you to review it again before you can +approve it. While a global policy is active, OpenShell cannot approve proposals, +including automatically. You can also review proposals in the terminal UI with +`openshell term`, but it rejects proposals without a reason. -## How It Works +## Approve Proposals Automatically -When policy advisor is enabled, the sandbox supervisor turns on three agent-facing surfaces: +By default, every proposal waits for your review. In automatic mode, OpenShell +approves a proposal without review when both of these are true: -- It installs `/etc/openshell/skills/policy_advisor.md` inside the sandbox. -- It also installs `/etc/openshell/skills/policy-advisor/SKILL.md` as a short Codex/generic-agent pointer, and writes a root `/AGENTS.md` pointer only when the image does not already provide one. -- It serves `http://policy.local` from inside the sandbox. -- It adds `agent_guidance` and `next_steps` to L7 `policy_denied` response bodies so the agent can find the skill and local API. +- The proposal risk check finds none of the risks described in [Proposal Risk + Check](#proposal-risk-check). +- OpenShell has not flagged the proposal's destination. OpenShell flags hosts + written as private IP addresses, wildcard hosts, `allowed_ips` entries that + include private ranges or have no host, ports above 49152, and well-known + database and cache ports, such as 5432 and 6379. Each flag appears on the + proposal's `Security` line. -The loop has seven steps: +Every other proposal still waits for your review. These checks do not treat +access to a new public host as a risk when no provider credential applies +there. Automatic mode approves such proposals, including OpenShell's own drafts +from blocked connections, so turn it on only if you accept that binaries in the +sandbox can gain access to public hosts without your review. -1. A sandboxed process attempts a network request that policy denies. -2. For inspected REST traffic, OpenShell returns a structured `403` body with fields such as `layer`, `host`, `port`, `binary`, `method`, `path`, `rule_missing`, `agent_guidance`, and `next_steps`. -3. The agent reads the policy advisor skill, inspects the current policy, and optionally reads recent denial log lines. -4. The agent submits one or more `addRule` proposals to `http://policy.local/v1/proposals`. -5. The gateway turns the proposal into the exact effective-policy candidate it would apply. It preserves any existing L7 or provider-owned endpoint contract, adds the proposed binary as a sandbox overlay, validates the full merge, and runs the [policy prover](#what-auto-approval-checks) against that candidate. -6. The gateway stores the candidate, its prover result, any application error, and a review token tied to the live policy, provider rules, and credential metadata. Provider rules remain immutable inputs. -7. Before approval, the gateway cheaply recomputes the candidate token from live inputs. An unchanged token reuses the stored prover result. A changed token leaves the proposal pending with a refreshed candidate and requires a fresh review. Under `auto` mode, an unchanged candidate is approved only when the prover delta and security notes are empty. Under `manual` mode, every valid proposal lands in the draft inbox. -8. The agent waits on `/v1/proposals/{chunk_id}/wait` until a decision is available. Approved proposals hot-reload into the sandbox; rejected proposals return `rejection_reason` and `validation_result` so the agent can revise. +Turn on automatic mode for every sandbox on the gateway: -```mermaid -flowchart TD - A["Denied request creates a narrow proposal"] --> B["Gateway builds the exact effective-policy candidate"] - B --> C["Validate merge, L7 contract, providers, and credentials"] - C -->|Invalid| D["Keep pending and show application error"] - C -->|Valid| E["Run prover once and store candidate plus review token"] - E --> F["Reviewer approves using that token"] - F --> G["Recompute token from live inputs"] - G -->|Unchanged| H["Reuse stored prover result and apply candidate"] - G -->|Changed| I["Refresh candidate and token; require fresh review"] - I --> F +```shell +openshell settings set --global \ + --key proposal_approval_mode \ + --value auto \ + --yes ``` -When a proposal is approved, `/wait` reports `policy_reloaded: true` only after the local sandbox policy covers the approved rule. At that point the agent can retry the original denied action once. If a proposal is rejected, `/wait` returns `rejection_reason` and `validation_result` so the agent can revise or stop. `validation_result` carries the categorical prover findings — `link_local_reach`, `l7_bypass_credentialed`, `credential_reach_expansion`, `capability_expansion` — so the agent can narrow the next attempt to the specific concern the prover flagged. - -## What Gets Proposed - -OpenShell has two proposal paths: - -| Path | Source | Typical rule shape | -|---|---|---| -| Mechanistic mapper | Aggregated denial summaries from the sandbox. | Groups by host, port, and binary. If L7 request samples are available, it can draft REST method and path rules. Otherwise it drafts an L4 endpoint. | -| Agent-authored proposal | The in-sandbox agent, using `policy.local`. | Usually a REST `addRule` with exact host, port, binary, method, and path from the structured denial. It can also omit `protocol` for endpoint-only access through the explicit proxy. | +To turn it on for one sandbox, set the key on that sandbox, or pass +`--approval-mode auto` when you create it: -For a new hostname absent from policy, policy DNS does not query an upstream resolver. It gives the process a synthetic address with a 30-second observation record that cannot reach the destination. The resulting TCP attempt records the actual hostname, port, and verified binary for a mechanistic proposal, even when the agent proposal surface is disabled. For example, `curl http://pypi.org/` can create a pending `pypi.org:80` rule for `/usr/bin/curl`. The first request is still denied. After approval and policy reload, retry the request so DNS can resolve the now-authorized endpoint. Each unknown lookup still appears in the OCSF log as a `policy_dns_ineligible` DNS denial. Policy DNS refuses reserved loopback, metadata, and host-gateway names outright, and refuses every unknown name while the sandbox policy is quarantined. Observations can use at most a quarter of each synthetic address pool, up to 128 unique unknown names per address family for the supervisor's lifetime. After that, policy DNS refuses further unknown names, and they do not produce proposals until the supervisor restarts. - -### How proposal provenance works +```shell +openshell sandbox create --name --approval-mode auto +``` -OpenShell tracks whether an endpoint and binary came from policy advisor. This is internal provenance; it is not a policy YAML field that authors set. Think of each marker as answering “who introduced this identity?” rather than “what traffic does this allow?” +The accepted values are `manual`, the default, and `auto`. A gateway-wide value +overrides sandbox values, so an administrator can require manual review for +every sandbox by setting `manual` on the gateway. -| Marker | `false` | `true` | When both declarations meet | -|---|---|---|---| -| Endpoint provenance | A user or provider explicitly declared the endpoint. | Policy advisor proposed the endpoint. | The values do not conflict by themselves. The explicit declaration wins if the endpoints merge. | -| Binary provenance | A user or provider explicitly declared the binary path. | Policy advisor proposed the binary path. | The explicit declaration wins if the same path merges. | +### Proposal Risk Check -For example, a GitHub provider can explicitly declare `api.github.com:443` for read operations. An agent can then propose `PUT /repos/NVIDIA/OpenShell/contents/docs/**` for the same endpoint. The provider endpoint has provenance `false`; the proposal endpoint has provenance `true`. OpenShell allows that overlap, keeps the provider rule immutable, and stores an approved write rule in the sandbox policy layer. +The policy advisor uses the [policy prover](/how-it-works/policies/prover) to +run a proposal risk check on every proposal. Unlike the boundary check that the +`openshell-prover` CLI runs, it does not compare a policy with a boundary that +you write. It compares what the sandbox can reach with and without the proposed +rule. The check accounts for the credentials of attached providers and for +binaries whose traffic OpenShell cannot inspect. It reports a finding when the +rule would give a binary new access of one of these kinds: -Provenance does not hide a real endpoint conflict. The same two declarations still fail validation if they disagree on connection or request-processing behavior that must have one value, such as TLS mode, `allowed_ips`, an equally specific L7 protocol or parser contract, credential binding, or enforcement mode. Authorization fields such as compatible allow and deny rules can combine. +| Finding | The proposed rule would let a binary | +|---|---| +| `link_local_reach` | Reach a link-local address (`169.254.0.0/16` or `fe80::/10`) or a cloud metadata hostname. | +| `l7_bypass_credentialed` | Send traffic that OpenShell cannot inspect, as `git`, `ssh`, or `nc` do, to a host where a provider credential is available. | +| `credential_reach_expansion` | Use a provider credential at a host and port that it could not reach before. | +| `capability_expansion` | Use a new HTTP method at a host and port where it already uses a provider credential. | -Exact-host SSRF trust requires an exact endpoint and the matching binary identity to be explicit in the same rule. An advisor-only endpoint or binary does not create that stronger trust. For example: +A proposal with any prover finding or flagged destination needs your review, +even in automatic mode. When you reject a proposal, the agent receives the +findings with your reason, so it can narrow its next attempt. -- A provider rule that explicitly declares both `/usr/bin/gh` and `api.github.com:443` already establishes exact-host trust for that pair. A later advisor proposal does not create or broaden that trust. -- If the provider declares `api.github.com:443` for `/usr/bin/gh`, but the advisor proposes the endpoint for `/usr/bin/curl`, `curl` does not inherit the provider's binary identity. Its proposed rule remains subject to the normal SSRF checks. -- If an advisor proposes `internal-api.example:443` and it resolves to a private address, approval alone is not enough. A developer must explicitly authorize the intended address range with `allowed_ips`. +## What Agents Can Propose -For REST APIs, prefer L7 rules over broad L4 access. A good proposal allows one method and the smallest safe path: +An agent proposes endpoints and binaries. An endpoint can include an access +preset, method and path allow rules, deny rules, `allowed_ips`, and +`allow_encoded_slash`. A good proposal allows one method on the narrowest path +the task needs: ```json { @@ -191,99 +247,71 @@ For REST APIs, prefer L7 rules over broad L4 access. A good proposal allows one } ``` -The current `policy.local` JSON shape covers explicit-proxy endpoints and REST method or path rules. Agent-authored proposals cannot set `protocol: tcp` or `tls: skip`, because those modes bypass application-authority inspection. When a task requires native TCP or a raw TLS tunnel, a developer must add the rule through the normal policy-authoring workflow. Omitting `protocol` remains supported and retains the explicit proxy's default TLS termination and HTTP authority checks. Use [Customize Sandbox Policies](/how-it-works/policies/overview) or [Policy Schema Reference](/how-it-works/policies/schema) for policy fields that are not part of the agent-authored proposal surface, such as WebSocket credential rewrite, GraphQL operation matching, endpoint path scoping, and provider-owned policy bundles. - -Policy advisor proposals do not add `allowed_ips` automatically. If an advisor-proposed hostname resolves to an internal or private address, OpenShell's SSRF protections still block the connection until a developer explicitly adds the required `allowed_ips` entry. - -Private RFC 1918, CGNAT, IPv6 ULA, and other special-use destinations classified as internal produce advisory security notes when they appear as literal endpoint IPs or in `allowed_ips`. CIDR intersections are included, and hostless `allowed_ips` rules receive an additional warning because they can match any hostname resolving into the configured range. - -Always-blocked destinations are not advisory. Loopback, link-local, and unspecified IPs or CIDRs, plus `localhost` and known metadata endpoint hostnames, are excluded from security notes. Submit and edit can store such a draft, but approval fails when merge validation prevents it from entering the active policy. Runtime SSRF protections continue to enforce the same boundary. - -## What Auto-Approval Checks - -Auto-approval requires all three conditions: the effective mode is `auto`, the prover delta is empty, and recalculating security notes from the current stored rule produces none. - -The policy prover runs against mechanistic and agent-authored proposals alike and asks four formal questions about the proposed change. Each "yes" is one categorical finding. Any finding blocks auto-approval. An empty delta is necessary but not sufficient. - -| Category | Triggered when | -|---|---| -| `link_local_reach` | A rule reaches `169.254.0.0/16`, `fe80::/10`, or a known metadata hostname. | -| `l7_bypass_credentialed` | A binary using a wire protocol the L7 proxy cannot inspect (`git-remote-https`, `ssh`, `nc`) gains reach to a host where a credential is in scope. | -| `credential_reach_expansion` | A binary gains credentialed reach to a `(host, port)` it could not reach before. | -| `capability_expansion` | On a `(binary, host, port)` that already had credentialed reach, the proposal adds a new HTTP method. The finding cites the specific method. | - -Findings are categorical. There is no severity tier. The reviewer reads the category and the structured evidence to decide. - -Before approval, the gateway rebuilds the candidate token from the live base policy, immutable provider rules, and non-secret credential metadata. When that token is unchanged, it reuses the persisted prover result instead of rerunning the prover. When it changes, the gateway evaluates and persists the refreshed candidate, leaves the chunk pending, and requires the reviewer to inspect and approve the new token. Edits and deduplicated resubmissions follow the same path. Merge, policy-shape, provider-composition, credential, or prover failures are shown as application errors and cannot be approved. Security notes flag concerns such as internal or private destinations and `allowed_ips`, wildcard hosts, hostless `allowed_ips`, ephemeral ports, and well-known database or service ports. Any prover finding or security note keeps the chunk pending in auto mode. - -The full reasoning model lives in [`crates/openshell-prover/README.md`](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-prover/README.md). Provider profiles composed in via [Profiles](/how-it-works/providers/profiles) are part of the effective policy the prover reasons over. - -## Review Proposals - -Review pending chunks from the host: - -```shell -openshell rule get --status pending -``` - -Under `auto` mode, proposals with a prover finding or any recalculated security note remain pending for human review. Proposals that pass both checks are visible under `--status approved` with the auto-approval audit fields described in [Approval Modes](#approval-modes). Under `manual` mode, every accepted proposal shows up as pending regardless of the prover verdict or security notes. - -The output shows the chunk ID, status, rationale, binary, endpoint summary, prover result, application error (if any), and candidate hash. For L7 proposals, the endpoint summary includes the protocol, method, and path: - -```text -Endpoints: api.github.com:443 [L7 rest, allow PUT /repos/NVIDIA/OpenShell/contents/docs/**] -``` - -Approve only when the structured rule matches the access you intend to grant: - -```shell -openshell rule approve --chunk-id -``` - -The CLI fetches the current review token and submits it with the approval. If policy, provider, or credential inputs changed after the proposal was displayed, the gateway leaves it pending and asks you to review the refreshed candidate. Run `rule get` again before retrying. Bulk approval binds each selected chunk to its own review token in the same way. - -Reject with guidance when the rule is too broad or points at the wrong target: - -```shell -openshell rule reject \ - --chunk-id \ - --reason "Scope this to docs/ paths only." -``` - -The rejection reason is returned to the agent through `policy.local`. The agent can use it to draft a narrower proposal. - -Reviewers see the same guidance in the terminal UI. Run `openshell term`, open the sandbox's draft inbox, and select a rejected chunk to open its detail popup. The stored reason appears on a `Guidance:` line, and the list row shows a shortened copy of it. Rejection reasons have no length limit, so long guidance wraps and the popup body scrolls with `j`/`k`, `PageUp`/`PageDown`, and `g`/`G`; the approve and close controls stay pinned below the body, and the bottom border shows the scroll position. +Agents cannot propose `protocol: tcp` or `tls: skip`, because OpenShell cannot +inspect that traffic. The API also has no fields for GraphQL operation rules, +MCP tool rules, query matchers, credential settings, or endpoint path selectors, +and it ignores them if an agent sends them. Review the rule that +`openshell rule get` shows, not the agent's description of it. Add rules that +need those fields yourself, as described in [Manage Sandbox +Policies](/how-it-works/policies/manage-policies). + +If a proposed hostname resolves to a private address, OpenShell still blocks the +connection after approval until you add an `allowed_ips` entry or declare the +endpoint in the policy yourself. This applies even when a provider rule already +lists that hostname for a different binary. Loopback, link-local, and cloud +metadata addresses are always blocked, and you cannot approve a proposal that +targets them. + +OpenShell adds an approved proposal to the sandbox's own policy as a separate +rule and never changes rules that providers contribute. If another rule covers +the same host and port, a request is allowed when either rule allows it, and +deny rules in either rule still apply. OpenShell refuses a proposal that +disagrees with an overlapping endpoint on a setting that must have a single +value, such as `tls` or `allowed_ips`. ## Agent API -`policy.local` is available only inside the sandbox and uses plain HTTP. It -resolves through sandbox-local DNS and does not need a proxy environment variable -or a network policy rule: +The agent uses these endpoints at `http://policy.local`: | Endpoint | Purpose | |---|---| -| `GET /v1/policy/current` | Returns the current effective sandbox policy as YAML. | -| `GET /v1/denials?last=10` | Returns recent denied OCSF shorthand log lines, newest first. Query strings are redacted before lines are returned to the agent. | -| `POST /v1/proposals` | Submits `addRule` operations. The response includes `accepted_chunk_ids` and `rejection_reasons`. | -| `GET /v1/proposals/{chunk_id}` | Returns one proposal's current `pending`, `approved`, or `rejected` status. | -| `GET /v1/proposals/{chunk_id}/wait?timeout=300` | Holds one HTTP request open until the proposal is approved, rejected, or the timeout expires. | +| `GET /v1/policy/current` | Returns the sandbox's effective policy as YAML. | +| `GET /v1/denials?last=10` | Returns recent denials as log lines, newest first, with query strings redacted. `last` defaults to 10 and can be up to 100. | +| `POST /v1/proposals` | Submits proposals. The response lists the IDs of accepted proposals and the reasons for any that were refused. | +| `GET /v1/proposals/{chunk_id}` | Returns a proposal's status: `pending`, `approved`, or `rejected`. | +| `GET /v1/proposals/{chunk_id}/wait?timeout=300` | Waits until the proposal is approved or rejected, or until the timeout expires. | -If policy advisor is disabled, every route returns `404 feature_disabled`, the skill is not installed for new sandboxes, and L7 deny bodies do not advertise `policy.local` routes or include `agent_guidance`. +After an approval, the `/wait` response reports `policy_reloaded: true` once the +sandbox has loaded the new rule. OpenShell closes connections opened under the +previous rules, so the agent's retry uses the new rule. -## What to Expect +When the policy advisor is disabled, every route returns `404 feature_disabled`, +new sandboxes do not receive the guide, and denial responses do not mention +`policy.local`. OpenShell still drafts proposals from blocked connections. -Approved network rules hot-reload without restarting the sandbox. HTTP L7 keep-alive connections are closed at the reload boundary so the next parsed request uses the new policy. Raw streams remain connection-scoped, as described in [Customize Sandbox Policies](/how-it-works/policies/overview#policy-structure). +## Logs -Policy advisor emits audit events into the sandbox log. Use these lines to trace the full loop: +To follow a proposal in the sandbox log, run: ```shell openshell logs --since 10m ``` -Look for `HTTP:* DENIED`, `CONFIG:PROPOSED`, `CONFIG:APPROVED` or `CONFIG:REJECTED`, `CONFIG:LOADED`, and the final allowed request if the agent retries successfully. Auto-approved chunks emit `CONFIG:APPROVED` with `auto=true`, `source=`, `prover_delta=empty`, and `resolved_from=`. +Look for the denied request (`HTTP: ... DENIED`) or blocked connection +(`NET:OPEN ... DENIED`), then `CONFIG:PROPOSED`, `CONFIG:APPROVED` or +`CONFIG:REJECTED`, `CONFIG:LOADED`, and the retried request. `CONFIG:PROPOSED` +appears only for proposals from the agent, and `CONFIG:REJECTED` appears only +if the agent was waiting for the decision. An automatic approval's +`CONFIG:APPROVED` entry includes `auto:true`, the proposal's `source` +(`agent_authored`, or `mechanistic` for OpenShell's own drafts), and +`resolved_from`, which shows where the approval mode setting came from. ## Next Steps -- Use [Customize Sandbox Policies](/how-it-works/policies/overview) for manual policy updates and L7 rule syntax. -- Use [Policy Schema Reference](/how-it-works/policies/schema) for full YAML field details. -- Use [Logging](/observability/logging) to interpret OCSF shorthand log entries. +- Use [Manage Sandbox Policies](/how-it-works/policies/manage-policies) to + change policies yourself. +- Use [Network Rules](/how-it-works/policies/network-rules) and the + [Policy Schema Reference](/how-it-works/policies/schema) for rules that agents + cannot propose. +- Use the [Policy Prover](/how-it-works/policies/prover) to check a + complete policy against a boundary you define. diff --git a/docs/how-it-works/policies/default-policy.mdx b/docs/how-it-works/policies/default-policy.mdx index d670c80e2e..b682c14437 100644 --- a/docs/how-it-works/policies/default-policy.mdx +++ b/docs/how-it-works/policies/default-policy.mdx @@ -1,19 +1,34 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Default Policy Reference" +title: "Default Policy and Baseline Paths" sidebar-title: "Default Policy" -description: "Breakdown of the built-in default policy applied when you create an OpenShell sandbox without a custom policy." -keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Policy" -position: 2 +description: "The restrictive fallback policy used when a sandbox has no explicit or embedded policy, and the baseline paths OpenShell adds to sandbox policies." +keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Policy, Landlock, Filesystem" +position: 6 --- -When you create a sandbox without `--policy`, OpenShell applies a restrictive built-in fallback. The policy comes from the OpenShell runtime and does not depend on the selected workload image. +This reference describes the restrictive policy OpenShell uses when a sandbox +has no other policy, and the filesystem paths that OpenShell adds to sandbox +policies at runtime. -## Filesystem Access +## When the Default Applies -The fallback includes the sandbox working directory and grants read-only access to standard runtime paths: +OpenShell uses the restrictive default only when no global policy is active, the +sandbox has no saved policy, and its image contains no policy. Creating a +sandbox without `--policy` does not by itself mean the default is active, +because the image or `OPENSHELL_SANDBOX_POLICY` can supply one. An invalid image +policy keeps the workload from starting until you repair it. It does not select +the default. Refer to [Where the Active Policy Comes +From](/how-it-works/policies/overview#where-the-active-policy-comes-from) for +the complete selection order. +## Default Filesystem Access + +The default policy includes the sandbox working directory as read-write and +grants read-only access to these paths: + +- `/bin` - `/usr` - `/lib` - `/proc` @@ -21,14 +36,87 @@ The fallback includes the sandbox working directory and grants read-only access - `/etc` - `/var/log` -It grants read-write access to `/tmp` and `/dev/null`. Landlock enforcement uses `best_effort` compatibility so OpenShell can use the strongest ABI available on the host while retaining its mandatory baseline protections. +It grants read-write access to `/tmp` and `/dev/null`. Landlock user-policy +compatibility is `best_effort`. + +## Default Network Access + +The default policy defines no network rules or middleware, so all outbound +network access is denied. Attached providers can still add network rules to the +effective policy. To see them, compare the base and effective policies, as +described in [Inspect the Selected Policy](#inspect-the-selected-policy). + +## Default Process Identity + +The default policy leaves process identity to the compute driver. Docker and +Podman run the workload as the image's `USER` when it names a non-root user, and +as UID and GID 1000 when the image declares no user. They reject an image whose +user is root unless the policy you pass when you create the sandbox sets a +non-root `run_as_user`. Kubernetes and VM sandboxes run as the identity +configured for their driver. + +## Baseline Filesystem Paths + +Sandbox processes that use the network need system paths for shared libraries, +DNS resolution, and CA certificates. When the effective policy contains at least +one network rule, including a provider-contributed rule, OpenShell adds these +baseline paths to the sandbox's filesystem policy at startup: + +| Access | Paths | +|---|---| +| Read-only | `/usr`, `/lib`, `/etc`, `/app`, `/var/log`, `/proc`, `/dev/urandom` | +| Read-write | `/tmp`, `/dev/null` | + +OpenShell adds a baseline path only when it is available and your policy does +not already list it, so list the paths your workload needs, such as `/app`, in +your policy. OpenShell never changes the access of a path you list, so a +baseline read-write path that you list as read-only stays read-only. If the +policy has no `filesystem_policy` section, OpenShell creates one with +`include_workdir: true`. + +The sandbox saves the enriched filesystem policy as a new revision, so the +added paths appear in `openshell policy get --base`. OpenShell can reject a +replacement policy that removes filesystem paths, so keep the added paths when +you replace the complete policy. + +The runtime also grants the workload read-only access to the sandbox's TLS CA +certificates under `/run/openshell-supervisor-ca`. This grant is not saved in +the sandbox's policy. + +### GPU Sandboxes + +On the Docker and VM compute drivers, a sandbox that requests a GPU receives +additional paths when the corresponding GPU device is present: + +| Access | Paths | +|---|---| +| Read-only | `/run/nvidia-persistenced`, `/usr/lib/wsl` | +| Read-write | `/dev/nvidiactl`, `/dev/nvidia-uvm`, `/dev/nvidia-uvm-tools`, `/dev/nvidia-modeset`, `/dev/dxg`, numbered `/dev/nvidia` device nodes, and `/proc` | + +CUDA writes thread names under `/proc` during initialization, so GPU enrichment +moves `/proc` from read-only to read-write. OpenShell adds each path only when +it exists in the workload. These paths apply at runtime and are not saved in +the sandbox's policy. + +### Protected Paths -## Network Access +On Docker, Podman, Kubernetes, and VM sandboxes, a mandatory Landlock baseline +protects the private `/.openshell` directory and requires Landlock ABI v3. Your +filesystem policy is applied on top of that baseline and can narrow access, but +it cannot expose `/.openshell`. The `best_effort` compatibility setting does not +disable this protection or allow a kernel without ABI v3. -The fallback defines no network policies or provider-derived endpoints, so outbound network access is denied. Attach a provider or apply a custom policy that names the required endpoints and executable paths before running a networked agent. +## Inspect the Selected Policy -## Process Identity +To see which policy a sandbox uses, print its base and effective policies. +While a global policy is active, both commands show the global policy: -The fallback leaves process identity selection to the compute driver. Docker and Podman honor a non-root OCI `USER`; when an image declares no user, they use numeric UID and GID `1000`. Kubernetes and MicroVM drivers apply their configured non-root identities. +```shell +openshell policy get --base +openshell policy get --full +``` -Use `openshell policy get --full` to inspect the effective policy. Refer to [Customize Sandbox Policies](/how-it-works/policies/overview) to replace the fallback. +Neither view includes the paths that OpenShell grants only at runtime, such as +the CA certificate and GPU paths. To confirm what a running sandbox enforces, +check the revision status with `openshell policy list ` and test +requests. diff --git a/docs/how-it-works/policies/manage-policies.mdx b/docs/how-it-works/policies/manage-policies.mdx new file mode 100644 index 0000000000..681f7f0f29 --- /dev/null +++ b/docs/how-it-works/policies/manage-policies.mdx @@ -0,0 +1,390 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +title: "Manage Sandbox Policies" +sidebar-title: "Manage Policies" +description: "Use the OpenShell CLI to set, inspect, change, verify, and roll back sandbox policies, apply a global policy, and troubleshoot policy problems." +keywords: "Generative AI, Cybersecurity, Policy, Sandbox, CLI, Hot Reload, Revision" +position: 3 +--- + +This page shows how to use the OpenShell CLI to manage a sandbox's policy, from +setting a policy when you create a sandbox to changing it while the sandbox +runs, confirming that the change took effect, and rolling it back. Run +`openshell policy --help` for every command and option. + +## Create a Sandbox with a Policy + +Pass a policy file when you create a sandbox. Filesystem, Landlock, and process +settings take effect only when the sandbox starts, so set them here: + +```shell +openshell sandbox create --name my-sandbox --policy ./policy.yaml +``` + +The rest of this page refers to this sandbox as `my-sandbox`. If you omit the +sandbox name from an `openshell policy` command, the CLI uses the sandbox you +most recently created, connected to, or ran a command in, and prints its name. + +To use the same file for every sandbox you create, set +`OPENSHELL_SANDBOX_POLICY` instead of passing `--policy`: + +```shell +export OPENSHELL_SANDBOX_POLICY="$PWD/policy.yaml" +openshell sandbox create --name my-sandbox +``` + +Without either, the sandbox uses a policy from its image or the restrictive +[default policy](/how-it-works/policies/default-policy). + +## Ship a Policy in an Image + +To include a policy in a sandbox image, add it at `/etc/openshell/policy.yaml`: + +```dockerfile +COPY policy.yaml /etc/openshell/policy.yaml +``` + +OpenShell uses the image policy only when the sandbox has no saved policy, so a +`--policy` file or `OPENSHELL_SANDBOX_POLICY` takes precedence. If the image +policy is invalid, the sandbox does not start until you [replace it with a valid +policy](#a-new-sandbox-stays-in-provisioning). OpenShell +does not fall back to the default policy. + +## Inspect the Current Policy + +A sandbox's policy has two views. The base policy is the policy you set for the +sandbox. The effective policy is the base policy plus any rules that attached +providers add, and the sandbox enforces it once the latest revision loads. While +a global policy is active, both views show the global policy. `--base` shows the +base policy, and `--full` shows the effective policy: + +```shell +openshell policy get my-sandbox --base +openshell policy get my-sandbox --full +``` + +Edit from the base policy, because provider rules belong to their providers, not +to the sandbox's own policy. Use the effective policy to see everything the +sandbox can reach, for example to find out why a request is allowed. + +To save the effective policy as a YAML file, for example to review it or to +check it with the [policy prover](/how-it-works/policies/prover): + +```shell +openshell sandbox get my-sandbox --policy-only > effective-policy.yaml +``` + +To see each revision of the policy and whether the sandbox loaded it: + +```shell +openshell policy list my-sandbox +``` + +## Add or Remove Network Access + +`openshell policy update` adds or removes network rules on a running sandbox. It +changes only the `network_policies` section, so you do not need a complete +policy file. + +For example, to let `curl` in the sandbox make read-only requests to the GitHub +API: + +```shell +openshell policy update my-sandbox \ + --rule-name github_readonly \ + --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:enforce \ + --wait +``` + +The endpoint value lists the host, port, access preset, protocol, and +enforcement mode. `read-only` allows `GET`, `HEAD`, and `OPTIONS` requests, +`rest` turns on HTTP request inspection, and `enforce` blocks every other +request. The `--binary` value must be the real path of the executable inside the +sandbox, as described in [Binary +Matching](/how-it-works/policies/network-rules#binary-matching). + +To allow another kind of request on an existing rule, add an allow rule. Name +the rule with `--rule-name`, and list every binary in the rule with `--binary`. +The command fails if the list does not match the rule's binaries, so a new +permission cannot reach a binary you did not name: + +```shell +openshell policy update my-sandbox \ + --rule-name github_readonly \ + --binary /usr/bin/curl \ + --add-allow 'api.github.com:443:POST:/repos/*/*/issues' \ + --wait +``` + +Because this endpoint uses the `read-only` preset, OpenShell first replaces the +preset with equivalent explicit rules, then adds yours. `--rule-name` refers to +the rule's key under `network_policies`, which can differ from its `name` field. +Use `--add-deny` the same way to block a request. To remove the rule: + +```shell +openshell policy update my-sandbox --remove-rule github_readonly --wait +``` + +To preview a change without applying it, replace `--wait` with `--dry-run`. A +preview does not guarantee that the sandbox will accept the change. +`--remove-endpoint` removes a destination from every rule in the base policy +that lists it, and deletes rules that are left without endpoints. Rules that +providers add keep the destination. + +## Replace the Complete Policy + +`openshell policy set` replaces the whole policy. Use it for changes that +`policy update` cannot make, such as middleware, GraphQL, MCP, or JSON-RPC +rules, `tls: skip`, credential bindings, or request signing. The new file +replaces every section, so start from the current base policy. + + + +Print the base policy: + +```shell +openshell policy get my-sandbox --base +``` + +The command prints revision details followed by the policy. Save only the policy +YAML as `policy.yaml`, then edit the sections you want to change. Keep +everything else, including filesystem paths that OpenShell added when the +sandbox started. + +Submit the edited file and wait for the result: + +```shell +openshell policy set my-sandbox --policy policy.yaml --wait +``` + + + +Save the base policy as JSON: + +```shell +set -o pipefail +openshell policy get my-sandbox --base --output json \ + | jq -e '.policy' > base-policy.json +``` + +Edit `base-policy.json`, then submit it and wait for the result: + +```shell +openshell policy set my-sandbox --policy base-policy.json --wait +``` + + + + +The next section describes how each part of the new policy takes effect. + +## How Changes Take Effect + +When you apply a change with `openshell policy update` or +`openshell policy set`, the network sections of the policy take effect in the +running sandbox. The startup sections do not: + +| Change | Effect on a running sandbox | Required action | +|---|---|---| +| Network rules | The sandbox loads the new rules. Connections opened under the previous rules close. | Apply the change, then retry the request. | +| Middleware in `network_middlewares` | You can add, remove, or reconfigure built-in middleware and external services already registered with the gateway. | Apply the change with `openshell policy set`. | +| External middleware registration | Policy changes cannot register a service or change its gateway connection settings. | Update the gateway configuration and restart the gateway. | +| Added filesystem paths | The saved policy can change, but the running workload keeps its existing filesystem permissions. | Recreate the sandbox. | +| Removed filesystem paths, or changed `include_workdir`, Landlock, or process settings | OpenShell rejects the change after the workload starts. | Recreate the sandbox. | + +When new network rules take effect, OpenShell closes connections that were +opened under the previous rules, including HTTP keep-alive connections, tunnels, +WebSocket connections, and long-lived streams. Clients must reconnect, and their +next requests are checked against the new rules. + +OpenShell validates every change twice before it takes effect: + +```mermaid +flowchart TD + A["Submit a policy change"] --> B{"Gateway validates
the change"} + B -->|Invalid| C["Change rejected
Current policy stays active"] + B -->|Valid| D["Gateway saves a new revision"] + D --> E{"Sandbox validates
and loads the revision"} + E -->|Loads| F["New rules active
Old connections close"] + E -->|Fails| G["Failure mode applies
Block traffic or keep the last valid policy"] +``` + +The sandbox validates the revision again because it also accounts for provider +rules and other changes that arrive at the same time. If the sandbox cannot load +a revision, the `policy_validation_failure_mode` option in the [gateway +configuration](/how-it-works/gateways/configuration#full-example) decides what +happens. With `fail_closed`, the default, the sandbox blocks network traffic +until you submit a valid policy. With `retain_last_valid`, the last valid policy +stays active. [A Change Fails to Load](#a-change-fails-to-load) explains how to +repair a rejected change. + +## Change Filesystem and Process Settings + +Filesystem, Landlock, and process settings take effect only when a sandbox +starts, so changing them requires a new sandbox. Save the base policy as +described in [Replace the Complete Policy](#replace-the-complete-policy), and +edit these settings in the file. + +When the policy allows network access, OpenShell adds the system paths that +sandbox processes need, so list only the paths your workload requires. [Baseline +Filesystem +Paths](/how-it-works/policies/default-policy#baseline-filesystem-paths) lists +those paths. + + +Deleting a sandbox stops its processes and removes its state. Copy out anything +you need before you delete it. + + +Delete the sandbox and create a new one with the edited file: + +```shell +openshell sandbox delete my-sandbox +openshell sandbox create --name my-sandbox --policy ./policy.yaml +``` + +If the delete command reports that cleanup is pending, wait until +`openshell sandbox get my-sandbox` no longer finds the sandbox before you create +the new one. + +## Verify a Change + +Without `--wait`, a successful command means only that the gateway accepted the +change. With `--wait`, the CLI also waits for the sandbox to report a result. It +exits with status `1` if the sandbox rejects the revision, and with status `124` +if the wait times out. A change whose wait timed out might still load later, so +check the revision status before you submit it again. + +A successful exit does not always mean your change is active. It can also mean +that the policy was unchanged, or that a newer change replaced yours before it +loaded. Confirm the result: + +```shell +openshell policy list my-sandbox +openshell policy get my-sandbox --full +``` + +The latest revision should show `Loaded`, and the effective policy should +contain your change. + +Then test one request that should be allowed and one that should be blocked. +Confirm that a denial comes from OpenShell, as a `policy_denied` response or a +sandbox log entry, rather than from the destination service or a missing client +tool. + +## Roll Back to an Earlier Revision + +To restore an earlier policy, find the revision in +`openshell policy list my-sandbox` and print its base policy. For example, for +revision 2: + +```shell +openshell policy get my-sandbox --rev 2 --base +``` + +Save the policy YAML from the output as `previous-base.yaml`, review it, and +submit it as a new revision: + +```shell +openshell policy set my-sandbox --policy previous-base.yaml --wait +``` + +Rolling back restores only the policy. It does not restore provider profiles, +attachments, credentials, or a global policy, and the limits on +filesystem and process changes still apply. + +## Apply a Global Policy + +A gateway administrator can apply one policy to every sandbox on the gateway. +The global policy replaces each sandbox's policy and blocks sandbox policy +changes until you delete it. These commands require the platform administrator +role: + +| Task | Command | +|---|---| +| Inspect the current global policy. | `openshell policy get --global --full` | +| Apply a reviewed complete policy. | `openshell policy set --global --policy ./global-policy.yaml` | +| View global policy history. | `openshell policy list --global` | +| Remove the global policy and restore normal policy selection. | `openshell policy delete --global` | + +The set and delete commands ask for confirmation, because each one changes the +network access of every sandbox on the gateway. Running sandboxes load a new +global policy at their next configuration check, within about 10 seconds. +`--wait` is not available for global policies, so test traffic in a sandbox to +confirm a change. After you delete the global policy, `policy get --global` +still shows the last one, with the status `Superseded`. Deleting the global +policy fails if a sandbox's own policy and provider rules would be invalid once +restored. + +## Troubleshoot + +### A Request Is Denied + +OpenShell logs each denied connection with the destination, binary, and reason, +and each denied request with its method, path, and reason. Check the sandbox log +first: + +```shell +openshell logs my-sandbox --since 10m --source sandbox +``` + +Do not filter this log with `--level warn`, which hides policy events. Compare +the logged binary and destination with the effective policy, and use the error +code in the response to find the cause: + +| Error | Cause | What to check | +|---|---|---| +| `policy_denied` | No rule allows the connection, or a request rule blocks the request. | A rule lists the logged binary, host, and port, and its request rules allow the method and path. Scripts appear as their interpreter, such as `/usr/bin/python3.12`. | +| `request_authority_mismatch` | The HTTP request's host or port differs from the connection's destination. | The client's `Host` header, including any non-default port, matches the connection. | +| `credential_endpoint_mismatch` | A network rule allowed the request, but the provider credential is not bound to this destination. | The provider's profile endpoints or credential binding. Do not widen the network rule. | +| `credential_placeholder_in_request_body` | The request body contains an invalid or revoked credential placeholder. | Remove the stale placeholder or restore the provider. | + +### A Change Fails to Load + +`openshell policy list my-sandbox` shows a revision that the sandbox rejected as +`Failed`, with a shortened error. Run +`openshell policy get my-sandbox --rev ` to see the full error. What happens +to network access depends on the failure mode described in [How Changes Take +Effect](#how-changes-take-effect). With the default, `fail_closed`, the sandbox +blocks network traffic until a valid policy loads. Fix the error and submit the +policy again, or [roll back to an earlier +revision](#roll-back-to-an-earlier-revision). + +### A New Sandbox Stays in Provisioning + +`openshell sandbox create` rejects an invalid policy file. If a new sandbox's +configuration fails when it starts, for example because its image policy is +invalid or its providers conflict, the sandbox stays in `Provisioning` and its +workload does not start. Its `ConfigurationReady` condition shows the reason +`ConfigurationInvalid`. Check the diagnostic, and for an invalid image policy, +check the sandbox log for the specific error: + +```shell +openshell sandbox get my-sandbox --output json +``` + +You have 300 seconds to fix the configuration. Replace the policy with +`openshell policy set`, or fix the provider configuration. Until the workload +first starts, you can also change filesystem, Landlock, and process settings. +Each change to the policy, providers, or settings restarts the 300 seconds. + +If the time runs out, the sandbox moves to `Error` with the reason +`ProvisioningTimedOut`. Fixing the configuration does not restart it. After you +fix it, start the sandbox again, which begins a new 300-second window: + +```shell +openshell sandbox start my-sandbox +``` + +## Next Steps + +- Use [Network Rules](/how-it-works/policies/network-rules) for example rules + you can adapt to common services and protocols. +- Use the [Policy Prover](/how-it-works/policies/prover) to check that a policy + grants no more access than a boundary you define. +- Use the [Policy Advisor](/how-it-works/policies/advisor) to let an agent + propose the network rules it needs for your review. +- Use the [Policy Schema Reference](/how-it-works/policies/schema) for every + field, default, and constraint. diff --git a/docs/how-it-works/policies/network-rules.mdx b/docs/how-it-works/policies/network-rules.mdx new file mode 100644 index 0000000000..5b3b366458 --- /dev/null +++ b/docs/how-it-works/policies/network-rules.mdx @@ -0,0 +1,615 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +title: "Network Rules" +sidebar-title: "Network Rules" +description: "Understand how OpenShell evaluates network rules, and adapt examples for REST APIs, package registries, internal services, WebSocket, GraphQL, MCP, and native TCP." +keywords: "Generative AI, Cybersecurity, Policy, Network, REST, TCP, WebSocket, GraphQL, MCP, PyPI, npm" +position: 2 +--- + +Network rules control which destinations each binary in a sandbox can reach, and +which requests it can send. They make up most of a typical policy. This page +explains how OpenShell evaluates network rules, then gives example rules for +common services and protocols that you can adapt. + +## How Network Rules Work + +OpenShell checks every outbound connection from a sandbox against the +`network_policies` section of its policy, and denies any connection that no rule +allows. + +### Rule Structure + +Each entry in `network_policies` is a rule. The entry's key is the rule's name, +and the rule contains two lists. `endpoints` lists the destinations the rule +allows, and `binaries` lists the executables that can connect to them. A rule +allows every listed binary to reach every listed endpoint. + +For example, this rule is named `example_api` and lists two binaries and two +endpoints: + +```yaml +network_policies: + example_api: + endpoints: + - host: api.example.com + port: 443 + - host: uploads.example.com + port: 443 + binaries: + - path: /usr/bin/curl + - path: /usr/bin/wget +``` + +The rule allows these four combinations: + +| Binary | Endpoint | +|---|---| +| `/usr/bin/curl` | `api.example.com:443` | +| `/usr/bin/curl` | `uploads.example.com:443` | +| `/usr/bin/wget` | `api.example.com:443` | +| `/usr/bin/wget` | `uploads.example.com:443` | + +If `wget` should reach only `api.example.com`, put that binary and endpoint in a +separate rule. + +### Binary Matching + +When you write a network rule, list the path of the executable that opens the +connection. This is not always the command you run. For example, `pip` is a +Python script, so the process that connects to PyPI is the Python interpreter, +and a rule for `pip install` must list the interpreter, not `/usr/bin/pip`. + +OpenShell identifies each process by the real path of its executable, as the +kernel reports it, so list real paths rather than symlinks. For example, on +Ubuntu 24.04, `/usr/bin/python3` is a symlink to `/usr/bin/python3.12`. To find +the real path, run `readlink -f` on the path inside the sandbox. When OpenShell +denies a connection, the sandbox log shows the path it identified. [Allow PyPI +Downloads](#allow-pypi-downloads) and [Allow npm Installs](#allow-npm-installs) +show rules that list interpreters. + +A rule also applies to processes that a listed binary starts. For example, if a +rule lists an agent's executable, tools that the agent launches can use the rule +too. A rule with an empty `binaries` list matches no binary and allows nothing. + +OpenShell records a hash of each executable the first time it takes part in a +connection, and denies later connections if the file at that path changes. + +### Connection and Request Checks + +OpenShell checks network traffic in two stages: + +1. When a binary opens a connection, OpenShell checks the destination host and + port and the binary against your rules. If no rule matches, OpenShell denies + the connection. +2. If the matching endpoint sets a request protocol, such as `rest`, OpenShell + also reads each request sent over the connection and checks it against the + endpoint's request rules. + For example, with `protocol: rest`, a rule can allow `GET` requests to an API + while blocking `POST` and `DELETE` requests. + +The second stage is called request inspection, and an endpoint that uses it is +an inspected endpoint. The `protocol` field selects what OpenShell inspects: + +| `protocol` | What OpenShell checks in each request | Example | +|---|---|---| +| `rest` | HTTP method, path, and query parameters. | [REST](#allow-read-only-api-access) | +| `websocket` | The WebSocket upgrade request and each text message the client sends. | [WebSocket](#allow-websocket-messages) | +| `graphql` | GraphQL operation type, operation name, and top-level fields. | [GraphQL](#allow-graphql-operations) | +| `mcp` | MCP method and tool name. | [MCP](#allow-mcp-tools) | +| `json-rpc` | JSON-RPC method name. | [JSON-RPC rules](/how-it-works/policies/schema#json-rpc-rules) | +| `tcp` | No request rules. Use it for clients that speak a protocol other than HTTP, such as databases. | [Native TCP](#allow-native-tcp) | +| Omitted | No request rules. | -- | + +When `protocol` is omitted, OpenShell allows any method and path. Unless the +endpoint sets `tls: skip`, it still terminates TLS and rejects HTTP requests +that are addressed to another host or have malformed paths, such as paths that +contain `%2F`. It relays traffic that is neither TLS nor HTTP without inspecting +it. For a protocol in which the server sends first, such as SMTP, set +`tls: skip`. + +Use an inspected endpoint when the request, not only the destination, +determines the risk, such as allowing reads but not writes on an API. + +### Enforcement + +The `enforcement` field on an inspected endpoint decides what happens when a +request breaks the endpoint's request rules: + +- `enforce` blocks the request. An HTTP client receives an OpenShell + `policy_denied` response. +- `audit` allows the request and logs the violation. This is the default. + +Audit mode applies only to the endpoint's allow and deny rules. OpenShell still +rejects requests that it cannot process safely, such as malformed requests and +requests addressed to another host. + +Use `audit` to see what a new rule would block before you enforce it. Apply the +rule with `enforcement: audit`, run your workload, then look for violation +events in the sandbox log: + +```shell +openshell logs my-sandbox --since 5m --source sandbox +``` + +Do not filter this log with `--level warn`, which hides policy events. When the +log shows only the violations you expect, switch the endpoint to `enforce`, +because an endpoint in audit mode does not block requests that break its rules. + +### Overlapping Rules + +Network rules are not an ordered firewall list. Several rules can match the same +connection or request, and every matching rule adds the access it allows. A +matching deny rule takes precedence over any allow, regardless of where either +appears in the file. If one matching rule inspects requests and another does +not, OpenShell inspects the connection, and the rule without `protocol` adds no +request access. Endpoints that can match the same host and port must also agree +on settings such as `tls` and `allowed_ips`, or OpenShell rejects the policy. + +For example, suppose one rule allows `GET /repos/**` on a host and another rule +for the same host, port, and binary denies `GET /repos/private/**`. OpenShell +denies requests under `/repos/private/`. When you restrict access, review every +rule that could allow the request, including rules that providers contribute. +[Allow Specific Methods and Paths](#allow-specific-methods-and-paths) shows a +deny rule. + +### Network Access and Credentials + +A network rule that allows traffic to a destination does not allow OpenShell to +send provider credentials there. OpenShell supplies a provider's credentials +only to the destinations that the provider's profile or an explicit credential +binding allows. When a request fails a credential check, correct the provider +binding instead of widening the network rule. Refer to [Provider +Profiles](/how-it-works/providers/profiles) for profile endpoints, and to +[Credential Fields](/how-it-works/policies/schema#credential-fields) for +explicit bindings. + +## Examples + +The following rules cover common services and protocols. Replace the example +hosts, paths, and binaries with your own values, and make sure the sandbox +contains the client executable. + +When a single `openshell policy update` command can create a rule, the example +shows it. Otherwise, add the YAML under the `network_policies` section of a +complete policy file and apply it with `openshell policy set`, as described in +[Replace the Complete +Policy](/how-it-works/policies/manage-policies#replace-the-complete-policy). +After you apply a rule, [verify the +change](/how-it-works/policies/manage-policies#verify-a-change) before testing +traffic. + +### Allow Read-Only API Access + +Use this rule when a binary needs to read from an HTTP API but must not change +anything. The `read-only` preset permits `GET`, `HEAD`, and `OPTIONS`. + + + + +```shell +openshell policy update my-sandbox \ + --rule-name github_readonly \ + --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:enforce \ + --wait +``` + + + + +```yaml +github_readonly: + endpoints: + - host: api.github.com + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: /usr/bin/curl +``` + + + + +Verify an allowed `GET` and a denied `POST`: + +```shell +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/curl --silent --show-error --fail \ + https://api.github.com/zen +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/curl --silent --show-error \ + --request POST https://api.github.com/zen +``` + +The `POST` must return an OpenShell `policy_denied` response. `read-only` is an +HTTP method preset, not a guarantee that an upstream `GET` has no side effect. + +### Allow Specific Methods and Paths + +Use explicit `rules` instead of an access preset when a binary needs specific +methods or paths, and `deny_rules` to block exceptions within them. This rule +lets the GitHub CLI make any API request for one repository, except requests to +its webhooks, which could send repository events to another destination. Replace +`` and ``, and keep only the executable paths used by your image: + +```yaml +github_repository_api: + endpoints: + - host: api.github.com + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: + method: "*" + path: "/repos///**" + deny_rules: + - method: "*" + path: "/repos///hooks" + - method: "*" + path: "/repos///hooks/**" + binaries: + - path: /usr/bin/gh + - path: /usr/local/bin/gh +``` + +In request paths, `*` matches within one path segment, and `**` written as a +whole segment matches across segments, so `/repos///**` covers every +path under the repository but not `/repos//` itself. The deny rules +list the webhooks path and everything under it separately for the same reason. +The allow rule grants every HTTP method under the repository path, including +writes, so narrow it if the agent needs only specific operations. It does not +cover Git transport or GraphQL requests. To add a deny rule to an existing rule +without a complete policy file, use `openshell policy update` with `--add-deny`, +as described in [Add or Remove Network +Access](/how-it-works/policies/manage-policies#add-or-remove-network-access). + +Apply the rule with a GitHub provider attached. Its profile must permit +credential use at `api.github.com`, and the token must authorize the repository +operations. Verify that `gh api repos///issues` succeeds and that +`gh api repos///hooks` returns an OpenShell denial. For a complete +Git push example, see [Grant GitHub Push Access to a Sandboxed +Agent](/tutorials/github-push-access). + +### Allow PyPI Downloads + +Package managers download packages with `GET` requests, so a read-only REST rule +allows installs while blocking uploads. `pip` is a Python script, so the rule +lists the real path of the Python interpreter, and any Python program in the +sandbox can use it. This rule lets `pip` and `uv` install packages from PyPI: + +```yaml +pypi: + endpoints: + - host: pypi.org + port: 443 + protocol: rest + enforcement: enforce + access: read-only + - host: files.pythonhosted.org + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: /usr/bin/python3.12 + - path: /usr/local/bin/uv +``` + +`/usr/bin/python3.12` is the interpreter on Ubuntu 24.04. To find yours, run +`readlink -f /usr/bin/python3` inside the sandbox. If your image uses another +interpreter, such as a uv-managed Python, list its real path or a glob that +matches it, such as `/sandbox/.uv/python/*/bin/python3*`. For a private package +index, replace the hosts with your index's hosts. + +For a sandbox with `pip` installed, verify that a download succeeds: + +```shell +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/python3 -m pip download --no-deps --dest /tmp/pypi-check requests +``` + +### Allow npm Installs + +Like `pip`, `npm` is a script, so the rule lists the Node.js interpreter. This +rule lets `npm` install packages from the public npm registry. It lists +`/usr/bin/node`, but official Node.js images install `node` at +`/usr/local/bin/node`, so adjust the path for your image: + +```yaml +npm_registry: + endpoints: + - host: registry.npmjs.org + port: 443 + protocol: rest + enforcement: enforce + access: read-only + allow_encoded_slash: true + binaries: + - path: /usr/bin/node +``` + +npm requests scoped packages, such as `@types/node`, with an encoded slash in +the path. OpenShell rejects `%2F` in request paths unless the endpoint sets +`allow_encoded_slash: true`. npm also sends its security audit as a `POST` +request, which the read-only preset denies. Run `npm install --no-audit`, or +replace `access: read-only` with explicit rules that also allow the audit's +`POST` requests. An endpoint cannot combine `access` and `rules`. + +Verify that a scoped package lookup succeeds: + +```shell +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/npm view @types/node version +``` + +### Restrict Destination Addresses + +OpenShell blocks connections to private network addresses by default to prevent +server-side request forgery (SSRF). An endpoint with an exact hostname can still +reach the private addresses that its hostname resolves to, unless the endpoint +comes from an approved [policy advisor](/how-it-works/policies/advisor) +proposal. Loopback, link-local, and unspecified addresses, including the cloud +metadata address `169.254.169.254`, are always blocked. + +Use `allowed_ips` to limit the addresses an endpoint can reach, or to let a +wildcard host such as `*.internal.example` reach private addresses. This rule +allows read-only access to an internal API only at addresses in `10.20.0.0/16`: + + + + +```shell +openshell policy update my-sandbox \ + --rule-name internal_api \ + --binary /usr/bin/curl \ + --add-endpoint 'api.internal.example:443:read-only:rest:enforce:allowed-ip=10.20.0.0/16' \ + --wait +``` + + + + +```yaml +internal_api: + endpoints: + - host: api.internal.example + port: 443 + protocol: rest + enforcement: enforce + access: read-only + allowed_ips: + - 10.20.0.0/16 + binaries: + - path: /usr/bin/curl +``` + + + + +When an endpoint sets `allowed_ips`, every address its hostname resolves to must +fall within the list, including public addresses. Account for DNS rotation and +service failover before pinning addresses. All endpoints that share a host and +port must use the same `allowed_ips` list. + +Verify that a request to the service succeeds. If it is denied, the sandbox log +shows the resolved address and reason. Review both the hostname and the address +instead of broadening the range when an address changes. + +### Allow WebSocket Messages + +Use `protocol: websocket` to control WebSocket connections and the messages +that clients send over them. This template requires `/usr/bin/node` and a +WebSocket service you control. It permits the `/v1/realtime` upgrade and client +text messages on that upgraded path, while denying `/v1/admin/**`: + +```yaml +realtime: + endpoints: + - host: realtime.example.com + port: 443 + protocol: websocket + enforcement: enforce + rules: + - allow: + method: GET + path: /v1/realtime + - allow: + method: WEBSOCKET_TEXT + path: /v1/realtime + deny_rules: + - method: "*" + path: /v1/admin/** + binaries: + - path: /usr/bin/node +``` + +Use your Node client to test a successful upgrade and text message on +`/v1/realtime`, and a denied upgrade or message on `/v1/admin/**`. The path on a +`WEBSOCKET_TEXT` rule is the original upgrade path, not text-frame content. + +OpenShell inspects complete client text messages. It does not inspect binary +frames or messages from the server. If the endpoint carries provider +credentials, OpenShell closes the connection with close code `1008` when the +client sends a binary frame, because it cannot check the frame for credentials. +Set `allow_uninspected_credentials: true` to relay binary frames anyway. Set +`websocket_credential_rewrite: true` only when client text messages contain +OpenShell credential placeholders that must be resolved. + +When new network rules take effect, OpenShell closes open WebSocket connections, +so the client must reconnect. The client might receive close code `1012`, or the +connection might close without a close frame. + +### Allow GraphQL Operations + +Use `protocol: graphql` to allow or deny GraphQL operations. APIs such as +GitHub serve REST and GraphQL on the same host. A REST rule sees every GraphQL +request as `POST /graphql`, so it cannot tell a query from a mutation. Give each +API its own endpoint in one rule, and use the endpoint `path` field to send +`/graphql` requests to the GraphQL rules. This rule allows read-only REST +requests, GraphQL queries, and the `createIssue` mutation: + +```yaml +github_api: + endpoints: + - host: api.github.com + port: 443 + protocol: rest + enforcement: enforce + access: read-only + - host: api.github.com + port: 443 + path: /graphql + protocol: graphql + enforcement: enforce + rules: + - allow: + operation_type: query + - allow: + operation_type: mutation + fields: [createIssue] + binaries: + - path: /usr/bin/gh +``` + +OpenShell selects the endpoint whose `path` most specifically matches the +request, so requests to `/graphql` use the GraphQL rules and all other paths use +the REST rules. An endpoint without `path` matches all paths. Endpoints that +share a host and port must agree on `tls` and `allowed_ips`. + +For allow rules, every top-level field in an operation must match. A malformed +or disallowed operation denies an entire batched request. GraphQL field names +are application-specific, so review them against the service's schema before +you rely on them. For deny rules, persisted queries, and GraphQL over WebSocket, +refer to [GraphQL Rules](/how-it-works/policies/schema#graphql-rules). + +With a GitHub provider attached, verify a REST read with `gh api zen` and a +query with `gh api graphql -f query='{ viewer { login } }'`, then confirm that a +mutation other than `createIssue` returns an OpenShell denial. + +### Allow MCP Tools + +Use `protocol: mcp` for sandbox-to-server MCP Streamable HTTP requests. This +template requires an MCP client that runs on the Python interpreter at +`/usr/bin/python3.12`, and a Streamable HTTP server you control. It allows +initialization, tool discovery, and `read_status`, while denying +`delete_resource`: + +```yaml +mcp_server: + endpoints: + - host: mcp.example.com + port: 443 + path: /mcp + protocol: mcp + enforcement: enforce + rules: + - allow: + method: initialize + - allow: + method: notifications/initialized + - allow: + method: tools/list + - allow: + method: tools/call + tool: read_status + deny_rules: + - method: tools/call + tool: delete_resource + binaries: + - path: /usr/bin/python3.12 +``` + +Verify initialization and `read_status` before confirming that `delete_resource` +returns a policy denial. Tool argument matching is not supported, so an allowed +tool can receive any arguments accepted by the server. + +Omitting `mcp.versions` allows only the `2025-11-25` revision. To support an +older server, list the exact revisions it needs, as described in [MCP Version +Selection](/how-it-works/policies/schema#mcp-version-selection). Server +responses and SSE messages are relayed without MCP policy parsing. Do not put an +MCP endpoint on the same host and port as an endpoint that uses a different +protocol. `openshell policy update` rejects this combination. + +### Allow Native TCP + +Use `protocol: tcp` for a client that speaks a protocol other than HTTP, such +as a database client. OpenShell checks the hostname, port, and executable but +applies no request rules, so prefer an inspected protocol whenever the client +supports one. + +On Debian and Ubuntu, `/usr/bin/psql` is a wrapper script that starts +`/usr/lib/postgresql//bin/psql`, so this rule lists that path with a +glob: + + + + +```shell +openshell policy update my-sandbox \ + --rule-name postgres \ + --binary '/usr/lib/postgresql/*/bin/psql' \ + --add-endpoint db.internal.example:5432::tcp \ + --wait +``` + + + + +```yaml +postgres: + endpoints: + - host: db.internal.example + port: 5432 + protocol: tcp + binaries: + - path: /usr/lib/postgresql/*/bin/psql +``` + + + + +Do not add `access`, `enforcement`, request rules, or credential-rewrite fields +to a TCP endpoint. Verify that `psql` reaches the server, then verify that +another binary or port is denied at the connection boundary. Use a client +command that fails before application authentication when the rule is absent. A +database authentication error can show that the TCP connection reached the +server, but it does not prove that database credentials are correct. + +OpenShell prepares DNS and TCP handling when the sandbox starts, so you can add +a TCP endpoint to a running sandbox without recreating it. + +Set `tls: skip` so that OpenShell relays the connection without examining it +when the client starts TLS as soon as it connects, when the client must complete +TLS itself, for example to present a client certificate, or when the server +sends first, as with SMTP, IMAP, and MySQL. Clients that send first and +negotiate TLS within their protocol, such as `psql`, do not need it. Refer to +[Inspection Fields](/how-it-works/policies/schema#inspection-fields). + +Prefer exact hostnames. A wildcard authorizes DNS queries for every matching +name, which can expose a DNS-label exfiltration channel. An allowed hostname on +shared infrastructure can also reach other tenants or virtual services behind +the same connection, because OpenShell cannot check which service a non-HTTP +payload addresses. + +OpenShell answers DNS queries for the hosts in your rules with placeholder +addresses whose TTL is at most 30 seconds. This applies to every endpoint, not +only `protocol: tcp`. The sandbox applies no DNS search domains, so request each +hostname exactly as your rules name it. Clients must resolve the hostname again +before reconnecting. A client that caches an address indefinitely can fail +after it expires, even while the endpoint remains allowed. OpenShell returns an +empty answer to AAAA queries, so dual-stack clients must fall back to the A +record. + +## Next Steps + +- Use [Manage Sandbox Policies](/how-it-works/policies/manage-policies) to + apply, verify, and roll back policy changes. +- Use the [Policy Schema Reference](/how-it-works/policies/schema) for protocol + defaults, field constraints, and matcher semantics. +- Use [Troubleshoot](/how-it-works/policies/manage-policies#troubleshoot) when a + request is denied unexpectedly. diff --git a/docs/how-it-works/policies/overview.mdx b/docs/how-it-works/policies/overview.mdx index 9b9d2a45e1..d5800bf0ac 100644 --- a/docs/how-it-works/policies/overview.mdx +++ b/docs/how-it-works/policies/overview.mdx @@ -1,974 +1,104 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Customize Sandbox Policies" +title: "Sandbox Policies" sidebar-title: "Overview" -description: "Apply, iterate, and debug sandbox network policies with hot-reload on running OpenShell sandboxes." +description: "Understand what sandbox policies control and where a sandbox's policy comes from." keywords: "Generative AI, Cybersecurity, Policy, Network Policy, Sandbox, Security, Hot Reload" -position: 6 +position: 1 --- -Use this page to apply and iterate policy changes on running sandboxes. For a full field-by-field YAML definition, use the [Policy Schema Reference](/how-it-works/policies/schema). - -## Policy Structure - -A policy has static sections `filesystem_policy`, `landlock`, and `process` that are locked at sandbox creation, and dynamic `network_policies` and `network_middlewares` sections that are hot-reloadable on a running sandbox. - -```yaml wordWrap showLineNumbers={false} -version: 1 - -# Static: locked at sandbox creation. Paths the agent can read vs read/write. -filesystem_policy: - include_workdir: true - read_only: [/usr, /lib, /etc] - read_write: [/tmp] - -# Static: Landlock LSM kernel enforcement. best_effort uses highest ABI the host supports. -landlock: - compatibility: best_effort - -# Static, optional: override the identity selected by the compute driver. -# process: -# run_as_user: "1500" -# run_as_group: "1500" - -# Dynamic: hot-reloadable. Named blocks of endpoints + binaries allowed to reach them. -network_policies: - my_api: - name: my-api - endpoints: - - host: api.example.com - port: 443 - protocol: rest - enforcement: enforce - access: full - binaries: - - path: /usr/bin/curl - -# Dynamic: ordered middleware selected independently by admitted host. -network_middlewares: - regex-redactor: - name: Redact API tokens - middleware: openshell/regex - order: 10 - config: - mode: redact - on_error: fail_closed - endpoints: - include: ["api.example.com"] - exclude: [] - -``` - -Static sections are locked at sandbox creation. Changing them requires destroying and recreating the sandbox. -Dynamic sections can be updated on a running sandbox with `openshell policy update` for incremental merges or `openshell policy set` for full replacement, and take effect without restarting. -When a hot reload changes rules, the supervisor publishes a new policy generation and closes connections pinned to the previous generation. This includes HTTP keep-alive tunnels, `tls: skip`, non-HTTP payloads, HTTP upgrades such as WebSocket, and long-lived response streams such as SSE. Most clients reconnect automatically, and the next connection or request is evaluated against the current policy. A parsed WebSocket relay closes with code `1012` when its attached policy generation becomes stale. Use `protocol: websocket` when policy should stay attached to the RFC 6455 upgrade and client text messages after the allowed upgrade. Provider-credentialed endpoints reject L4-only and `tls: skip` modes by default. On a credentialed WebSocket upgrade, OpenShell keeps the connection on the parsed relay and rejects binary frames unless the endpoint explicitly sets `allow_uninspected_credentials: true`. Add `websocket_credential_rewrite: true` when the relay should rewrite credential placeholders in client-to-server WebSocket text messages. Add `request_body_credential_rewrite: true` only on inspected REST endpoints that need OpenShell to rewrite placeholders in supported text request bodies. - -| Section | Type | Description | -|---|---|---| -| `filesystem_policy` | Static | Controls which directories the agent can access on disk. Paths are split into `read_only` and `read_write` lists. Any path not listed in either list is inaccessible. Set `include_workdir: true` to automatically add the agent's working directory to `read_write`. [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforces these restrictions at the kernel level. | -| `landlock` | Static | Configures Landlock LSM enforcement behavior. Set `compatibility` to `best_effort` (skip individual inaccessible paths while applying remaining rules) or `hard_requirement` (fail if any path is inaccessible or the required kernel ABI is unavailable). Refer to the [Policy Schema Reference](/how-it-works/policies/schema#landlock) for the full behavior table. | -| `process` | Static | Optionally overrides the OS-level identity for the agent process. Explicit values must be `sandbox` or numeric UID/GID values from `1` through `4294967294`; root and the invalid identity sentinel are rejected. Docker and Podman may use named identities through per-field OCI `USER` fallback; Kubernetes uses its platform-selected numeric identity. The agent also runs with seccomp filters that block dangerous system calls. | -| `network_policies` | Dynamic | Controls outbound traffic from the sandbox, including native model-provider endpoints. Each block has a name, a list of endpoints (host, port, protocol, and optional rules), and a list of binaries allowed to use those endpoints.
Every outbound connection passes through the network supervisor, which queries the [policy engine](/about/architecture#how-a-network-request-travels) with the destination and calling binary. A connection is allowed only when both match an entry in the same policy block. Attached provider profiles can contribute endpoint and binary entries to the effective policy.
For endpoints with `protocol: rest`, the proxy auto-detects TLS and terminates it so each HTTP request can be checked against that endpoint's `rules` (method and path). For endpoints with `protocol: websocket`, the proxy validates the RFC 6455 upgrade and evaluates `GET` rules for the handshake plus either `WEBSOCKET_TEXT` rules for raw client text messages or GraphQL-over-WebSocket messages. Set `websocket_credential_rewrite: true` only when a WebSocket or REST compatibility endpoint must keep placeholder credentials in sandbox-owned text frames and resolve them at the OpenShell relay boundary.
Endpoints with `protocol: tcp` allow ordinary DNS resolution and native TCP connections without inspecting payloads. Endpoints without `protocol` retain L4 passthrough through an explicit proxy.
If no endpoint matches, the connection is denied. | -| `network_middlewares` | Dynamic | Declares keyed HTTP and WebSocket middleware configs. After network and L7 policy admit a request or upgrade, OpenShell matches each config's host selectors independently and runs matching entries by their unique ascending `order` before credential injection. WebSocket-capable entries continue on complete client text messages. | - -When REST body credential rewriting is disabled, OpenShell forwards placeholder -text unchanged if trusted metadata identifies an unknown key or a valid credential -with a current binding, including to the destination itself. For example, an -OpenRouter placeholder printed by `printenv` can remain in an OpenRouter -conversation, just like an unknown documentation example or a GitHub placeholder. -OpenShell preserves the body bytes and does not substitute the secret. Header -credential resolution remains separate: the same placeholder can resolve in an -authentication header while remaining literal text in the body. - -Known invalid or revoked references and unavailable classification -metadata also fail closed. The local HTTP 403 response uses the error code -`credential_placeholder_in_request_body`. Remove the offending reference from -conversation history before retrying; if metadata is unavailable, restore provider -access first. Do not enable body rewriting or `allow_uninspected_credentials` just -to forward conversation text. Malformed placeholder candidates and candidates -longer than 4096 wire bytes are denied. Authentication headers and HTTP trailers -retain their credential restrictions. - -When `openshell-sandbox` and the supervisor run separately, the supervisor mediates DNS by -hostname. DNS sender identity is unavailable because native socket writes can -come from a process that inherited the socket or replaced its executable. -Resolving a name does not authorize a connection: the supervisor still checks -the destination and calling binary when the workload opens TCP traffic. - -## Supervisor Middleware - -Supervisor middleware can inspect, deny, or replace admitted HTTP request bodies and client WebSocket text messages before provider credentials are injected. Middleware selection is independent of the `network_policies` rule that admitted the traffic: each keyed `network_middlewares` entry matches the destination host through `endpoints.include` and `endpoints.exclude`. - -```yaml -network_middlewares: - regex-redactor: - name: Redact API tokens - middleware: openshell/regex - order: 10 - config: - mode: redact - on_error: fail_closed - endpoints: - include: ["*.example.com"] - exclude: ["trusted.example.com"] -``` - -Matching entries run once each by ascending `order`; lower values run first, and duplicate order values are rejected. The default order is `0`, so policies with multiple entries normally set it explicitly. Map keys are structurally unique. An optional `name` provides a human-readable label, defaults to the map key, and does not change the key used as the config identity. Different keys may use the same implementation and run as distinct stages. `exclude` takes precedence over `include`. - -`openshell/regex` is an example built into the supervisor. It applies fixed regular expressions to UTF-8 HTTP request bodies and complete client-to-upstream WebSocket text messages before credential injection. The initial pattern recognizes `sk-` tokens. This is a best-effort text transformation without guarantees that sensitive values will be detected or fully removed; it does not inspect binary or upstream-to-client WebSocket messages. Custom expressions are not configurable yet. Operator-run middleware must be registered by name before a policy can reference it. The gateway validates implementation-owned config before accepting the policy. - -`on_error` defaults to `fail_closed`. Use `fail_open` only when skipping a selected stage that fails is acceptable. For WebSocket streams, a broken fail-open stage is disabled for the rest of that connection and OpenShell emits a state-change finding. A host-matched attachment joins only operation chains advertised by its implementation. An HTTP-only attachment may inspect the WebSocket upgrade GET, but post-upgrade messages pass with informational `binding_not_selected` coverage under either error mode. Binary messages also pass without middleware inspection and produce `unsupported_message_type` coverage for active WebSocket stages. Upstream-to-client messages remain uninspected. Policy validation rejects a fail-closed selector that can cover a `tls: skip` endpoint. An all-`fail_open` match may cover the endpoint; the supervisor bypasses the middleware and emits a detection finding. - -See [Supervisor Middleware](/extensibility/supervisor-middleware) for an introduction, or [Supervisor Middleware Configuration](/extensibility/supervisor-middleware/configure) for registration, ordering, failure behavior, and operations. - -## Baseline Filesystem Paths - -When a sandbox runs in proxy mode (the default), OpenShell automatically adds baseline filesystem paths required for the sandbox child process to function: `/usr`, `/lib`, `/etc`, and `/var/log` (read-only), plus `/tmp` (read-write). When `filesystem.include_workdir` is `true`, OpenShell also adds the resolved working directory as read-write. Paths like `/app` are included in the baseline set but are only added if they exist in the container image. - -For GPU sandboxes, OpenShell also adds existing GPU device nodes as read-write paths. CUDA workloads require write access to procfs for thread metadata, so GPU baseline enrichment moves `/proc` from read-only to read-write when GPU devices are present. - -This filtering prevents a missing baseline path from degrading Landlock enforcement. Without it, a single missing path could cause the entire Landlock ruleset to fail, leaving the sandbox with no filesystem restrictions at all. - -User-specified paths in your policy YAML are not pre-filtered. If you list a path that does not exist: - -- In `best_effort` mode, the path is skipped with a warning and remaining rules are still applied. -- In `hard_requirement` mode, sandbox startup fails immediately. - -This distinction means baseline system paths degrade gracefully while user-specified paths surface configuration errors. - -## Allow Native TCP Connections - -Use `protocol: tcp` when an application needs to resolve a policy-approved hostname and open a normal TCP connection without configuring an HTTP proxy. The endpoint remains L4-only, so OpenShell authorizes the hostname, port, and calling binary but does not inspect application payloads. - -```yaml showLineNumbers={false} -network_policies: - postgres: - name: postgres - endpoints: - - host: db.internal.example - port: 5432 - protocol: tcp - binaries: - - path: /usr/bin/psql -``` - -OpenShell uses policy DNS for hostnames eligible under TCP-carried endpoints, including `protocol: tcp` and inspected HTTP-based protocols such as `protocol: rest`. It validates upstream answers against destination and SSRF controls, returns a supervisor-owned synthetic address, and records the validated real addresses. A direct HTTP or HTTPS connection to that address enters transparent TCP capture; the supervisor then applies the endpoint's L4 or L7 policy before dialing a pinned address. Hostnames absent from policy also receive a contract-free synthetic observation address so a denied connection can produce a mechanistic proposal. Sandbox resolvers apply no DNS search domains, so a workload must request a hostname exactly as the policy names it. Clients that use the explicit HTTP proxy send the target hostname in the proxy request and do not need a policy DNS address for that target. - -Treat that hostname as a connection-routing constraint, not an application-authority boundary. OpenShell does not inspect TLS SNI, HTTP `Host`, or another protocol-level destination inside a `protocol: tcp` stream. If the approved hostname reaches compatible shared infrastructure, a client may be able to select another tenant, virtual host, or service behind the same front door. Use native TCP only when the trust boundary includes every destination that the shared infrastructure can expose; use an inspected protocol when application authority must remain constrained. - -Applications must honor the returned DNS TTL and resolve the hostname again before reconnecting after that TTL expires. A client that caches the synthetic address indefinitely can receive a connection failure after the mapping expires. Synthetic address identities are not reused within a supervisor lifetime, even after the active DNS record expires; the production pool has up to 512 IPv4 addresses and 512 IPv6 addresses, shared across policy-backed names and unknown-host observations. Observations can hold at most a quarter of each pool. Docker and Podman currently advertise only IPv4 egress for this feature, so OpenShell returns an empty successful answer for AAAA queries and lets dual-stack clients use the working A record. - -DNS resolution does not authorize a connection by itself. Unknown names, wrong ports, stale mappings, disallowed destination addresses, and binaries outside the matching policy fail closed. Applications cannot inherit access by connecting directly to a real IP returned by an upstream resolver. - -Do not combine `protocol: tcp` with L7-only fields such as `path`, `enforcement`, `access`, `rules`, `deny_rules`, request rewriting, or credential signing. Docker and Podman sandboxes support policy DNS and transparent TCP capture. Other compute drivers reject policies containing `protocol: tcp` until they provide the required runtime capability. - -Prefer exact hostnames for TCP endpoints. A wildcard authorizes DNS queries for every matching name, which means a compromised process can encode data into matching DNS labels even when the lookup does not return an address. OpenShell records failed eligible lookups for operators, but logging does not remove that exfiltration channel. - -The first TCP endpoint is a deliberate exception to ordinary dynamic network policy updates. A sandbox created without any `protocol: tcp` endpoint does not install the DNS and transparent-capture substrate. A hot reload that introduces the first TCP endpoint is rejected atomically and leaves the previous policy active; recreate the sandbox with a TCP endpoint to install the substrate. A sandbox that started with TCP support can remove and later re-add TCP endpoints through normal policy reloads. - -## Apply a Custom Policy - -Pass a policy YAML file when creating the sandbox: - -```shell -openshell sandbox create --from registry.example.com/your-org/claude-agent:latest --policy ./my-policy.yaml -- claude -``` - -The trailing command is the sandbox's canonical main process. If it exits, the -sandbox enters `Error`; use `sandbox exec` for one-shot commands that should not -define sandbox health. - -To avoid passing `--policy` every time, set a default policy with an environment variable: - -```shell -export OPENSHELL_SANDBOX_POLICY=./my-policy.yaml -openshell sandbox create --from registry.example.com/your-org/claude-agent:latest -- claude -``` - -The CLI uses the policy from `OPENSHELL_SANDBOX_POLICY` whenever `--policy` is not explicitly provided. - -## Iterate on a Running Sandbox - -To change what the sandbox can access, pull the current policy, edit the YAML, and push the update. The workflow is iterative: create the sandbox, monitor logs for denied actions, pull the policy, modify it, push, and verify. - -```mermaid -flowchart TD - A["1. Create sandbox with initial policy"] --> B["2. Monitor logs for denied actions"] - B --> C["3. Pull current policy"] - C --> D["4. Modify the policy YAML"] - D --> E["5. Push updated policy"] - E --> F["6. Verify the new revision loaded"] - F --> B - - style A fill:#76b900,stroke:#000000,color:#000000 - style B fill:#76b900,stroke:#000000,color:#000000 - style C fill:#76b900,stroke:#000000,color:#000000 - style D fill:#ffffff,stroke:#000000,color:#000000 - style E fill:#76b900,stroke:#000000,color:#000000 - style F fill:#76b900,stroke:#000000,color:#000000 - - linkStyle default stroke:#76b900,stroke-width:2px -``` - -The following steps outline the hot-reload policy update workflow. - -1. Create the sandbox with your initial policy by following [Apply a Custom Policy](#apply-a-custom-policy) above (or set `OPENSHELL_SANDBOX_POLICY`). - -2. Monitor denials. Each log entry shows host, port, binary, and reason. Alternatively, use `openshell term` for a live dashboard. - - ```shell - openshell logs --tail --source sandbox - ``` - -3. For additive network changes, use `openshell policy update`. This is the fastest path for adding endpoints, binaries, or REST and WebSocket allow/deny rules without replacing the full policy. The full option and format reference is in [Incremental Policy Updates](#incremental-policy-updates). - - ```shell - openshell policy update \ - --add-endpoint api.github.com:443:read-only:rest:enforce \ - --binary /usr/bin/gh \ - --wait - - openshell policy update \ - --rule-name allow_api_github_com_443 \ - --binary /usr/bin/gh \ - --add-allow 'api.github.com:443:POST:/repos/*/issues' \ - --wait - ``` - - `--add-allow` and `--add-deny` target existing `protocol: rest` or `protocol: websocket` endpoints. They require the rule name and complete affected binary and port scope. Run endpoint creation and L7 appends as separate commands; `--add-endpoint` cannot share a command with `--add-allow` or `--add-deny`. Compatible update flags in one command form one atomic merge batch and persist at most one new revision. - -4. For larger edits, pull the current base policy and edit the YAML directly. The base policy is the user-authored policy without provider-composed `_provider_*` entries, so it is safe to round-trip through `openshell policy set`. Before reusing the file, strip the metadata header above the `---` line. - - ```shell - openshell policy get --base > current-policy.yaml - ``` - - To inspect the effective policy that the sandbox enforces, including provider-composed entries, use `openshell policy get --full`. To inspect a stored sandbox-authored revision instead of the current effective policy, pass `--rev `. - -5. Edit the YAML: add or adjust `network_policies` entries, binaries, `access`, `rules`, or protocol-specific matchers such as GraphQL operation fields, MCP `method` / `tool` rules, and generic JSON-RPC `method` rules. - -6. Push the updated policy when you need a full replacement. Exit codes: 0 = loaded, 1 = validation failed, 124 = timeout. - - ```shell - openshell policy set --policy current-policy.yaml --wait - ``` - -7. Verify the new revision. If status is `loaded`, repeat from step 2 as needed; if `failed`, fix the policy and repeat from step 4. - - ```shell - openshell policy list - ``` - - Add `--output json` or `--output yaml` for automation. Structured policy - history is an envelope with `revisions` and `next_page_token` fields. Each - revision contains the scope, sandbox name when applicable, version, full - hash, status, and available revision timestamps, load error, and provenance. - Pass the returned token to `--page-token` to continue. Use - `openshell policy list --global --output json` for global history. - -### Validation failures - -Before starting a workload, OpenShell validates its effective policy together -with attached provider rules and credential bindings. An image policy can be -valid alone and fail after composition, for example when an L4-only endpoint -overlaps a credentialed provider that requires L7 inspection. - -When startup validation fails, the sandbox stays in `Provisioning` with a -`ConfigurationInvalid` readiness condition. Its workload has not started. Inspect -the condition with `openshell sandbox get `, then submit a complete repaired -policy or detach the conflicting provider: - -```shell -openshell policy set --policy repaired-policy.yaml --wait -openshell sandbox provider detach -``` - -These are alternative repairs; choose the one that matches the intended access. -Startup has a [300-second provisioning repair window](/how-it-works/sandboxes/overview#sandbox-lifecycle). -Effective configuration changes and their first failed load reset the window; -repeated failures do not. If it expires, the gateway records `ProvisioningTimedOut` -and reclaims compute. Repair the configuration, wait for cleanup, then run -`openshell sandbox start ` to retry. Before expiry, repair resumes the same -provisioning attempt. -The supervisor starts the workload after the repaired configuration passes -validation. You can replace static policy fields while the first startup is -blocked. After the first accepted activation, the usual static-field restrictions -apply permanently, including while a restart is pending or rejected. Images without an -embedded policy use the restrictive baseline and gain network access only from -operator-selected configuration. Explicit user/global policy precedence remains -unchanged. Gateway authorization or compatibility errors, exhausted connection -retries, and unavailable sandbox boundary connections terminate startup rather than -waiting for policy repair. - -OpenShell validates a complete candidate policy before activating any part of it. Endpoints may overlap when their connection and request-processing metadata agree. For example, two `api.example.com:443` REST entries can contribute different allow and deny rules when they use the same TLS, destination, credential, parser, and enforcement settings. A plain L4 endpoint may overlap an L7 endpoint because it authorizes the destination without contributing request-processing metadata. A more-specific path endpoint may override request-processing metadata from a broader endpoint, such as a `/graphql` GraphQL endpoint alongside a general REST endpoint for the same host. OpenShell rejects the candidate when overlapping exact or wildcard host selectors can both contribute equally specific endpoint configuration and disagree on those fields. - -Internal policy-advisor provenance does not make otherwise compatible endpoints ambiguous. This lets an advisor proposal extend a provider-covered host without modifying the provider rule. TLS, destination IP constraints, credential handling, protocol, parser, and equally specific enforcement settings must still agree. - -When the gateway knows the affected sandbox scope, it validates the complete -effective candidate before persistence. This covers direct policy replacement, -incremental merges and proposal approvals, provider attachment, and -provider-profile updates that fan out to attached sandboxes. An ambiguity -failure returns `FAILED_PRECONDITION`; OpenShell stores no invalid policy -revision and does not partially apply a profile update. Supervisor validation -remains a defense-in-depth boundary for startup, concurrent changes, and policy -sources outside those mutation paths. - -A gateway preflight rejection leaves the currently active policy unchanged -regardless of failure mode because the candidate is never persisted or -distributed. If a candidate reaches a supervisor and fails runtime validation, -the gateway's `policy_validation_failure_mode` configuration determines the -supervisor posture. Set it under `[openshell.gateway]` in `gateway.toml`. Its -default is `fail_closed`: - -```toml -[openshell.gateway] -policy_validation_failure_mode = "fail_closed" -``` - -In `fail_closed` mode, the supervisor publishes a quarantine generation, denies new egress, and closes connections pinned to the previous generation. The previous policy is not active. A later valid policy exits quarantine automatically. - -Operators that explicitly prioritize availability can retain the previous generation: - -```toml -[openshell.gateway] -policy_validation_failure_mode = "retain_last_valid" -``` - -In `retain_last_valid` mode, the rejected candidate remains inactive and the previous valid generation remains active. During initial startup, a rejected configuration keeps the workload unstarted in either mode. Restart the gateway after changing `gateway.toml`; connected sandbox supervisors receive the configured posture from the restarted gateway. Individual sandboxes cannot override it. - -OCSF configuration and finding events identify the rejected candidate, validation rationale, configured and effective modes, active generation, and whether the previous policy is active. When `retain_last_valid` is configured without a previous valid generation, the effective mode remains `fail_closed`. Connection denials during quarantine include the validation failure as their policy denial rationale. - -## Incremental Policy Updates - -Use `openshell policy update` when you want to merge network policy changes into the current live policy instead of replacing the whole YAML document. This command only updates the dynamic `network_policies` section. - -`openshell policy update` is useful when you want to: - -- add a new endpoint for an existing binary without touching other policy sections. -- add a few REST or WebSocket allow/deny rules after you see a blocked request in the logs. -- remove one endpoint or one named rule without rewriting the rest of the file. -- preview a merged result locally with `--dry-run` before you send it to the gateway. - -Use `openshell policy set` instead when you want to replace the full policy, update static sections, or make broader edits that are easier to express in YAML. Use full YAML for GraphQL, MCP, and JSON-RPC rule shapes. - -### Update Commands - -The incremental update surface is split into endpoint-level operations and method/path rule-level operations for REST and WebSocket endpoints. - -| Flag | What it changes | Typical use | -|---|---|---| -| `--add-endpoint ` | Creates or merges a network rule and endpoint. | Allow a new host and port, optionally with `access`, `protocol`, `enforcement`, endpoint options, and binaries. | -| `--remove-endpoint ` | Removes one host and port match from the current policy. | Drop a stale endpoint or remove one port from a multi-port endpoint. | -| `--remove-rule ` | Deletes a named `network_policies` entry. | Remove a whole rule by name when you no longer need it. | -| `--add-allow ` | Appends method/path allow rules to an existing REST or WebSocket endpoint. | Permit one additional REST method/path or WebSocket `WEBSOCKET_TEXT` path on an API that is already configured. | -| `--add-deny ` | Appends method/path deny rules to an existing REST or WebSocket endpoint. | Block a sensitive REST path or WebSocket text-message path under an endpoint that is otherwise allowed. | -| `--binary ` | Adds binaries for `--add-endpoint`; declares the complete existing binary set for L7 appends. | Repeat for every executable sharing the target rule. | -| `--rule-name ` | Names one new endpoint rule or selects the existing rule for L7 appends. | Required for `--add-allow` and `--add-deny`. | -| `--any-binary` | Explicitly acknowledges an existing rule that authorizes every binary. | Use for L7 appends instead of `--binary` when the target rule has no binary restriction. | -| `--endpoint-path ` | Selects an exact existing endpoint path for L7 appends. | Resolve path ambiguity; pass `''` to select an endpoint without a path scope. | -| `--dry-run` | Shows the merged policy locally and does not call the gateway. | Review the result before persisting it. | -| `--wait` | Polls until the sandbox reports that the new revision loaded. | Confirm the change took effect before continuing. | -| `--timeout ` | Sets the timeout for `--wait`. | Extend the wait window for slower sandboxes. | - -`--wait` and `--dry-run` cannot be used together. - -### Add Endpoint Compared to Allow and Deny - -`--add-endpoint` works at the endpoint and rule level. It creates a new `network_policies` entry when needed, or merges into an existing rule that already covers the same host and port. Use it when you define where traffic can go and which binaries can send it. - -`--add-allow` and `--add-deny` work at the method/path rule level. They do not create binaries, and they do not create a new endpoint. They modify an existing endpoint that already has `protocol: rest` or `protocol: websocket`. - -This is the practical difference: - -- Use `--add-endpoint` to say "allow this binary to reach `api.github.com:443`." -- Use `--add-allow` to say "for that existing REST endpoint, also allow `POST /repos/*/issues`." -- Use `--add-deny` to say "for that existing REST endpoint, explicitly deny `POST /admin/**`." -- Use `--add-allow` to say "for that existing WebSocket endpoint, also allow client text messages on `/v1/realtime/**`." - -Current constraints: - -- `--add-allow` and `--add-deny` work on `protocol: rest` and `protocol: websocket` endpoints. -- GraphQL, MCP, and JSON-RPC fine-grained rules require full policy YAML applied with `openshell policy set`. -- `--add-deny` requires the endpoint to already have an allow base, either an `access` preset or explicit allow `rules`. -- `protocol: sql` is not a practical incremental workflow today. OpenShell does not do full SQL parsing, and SQL enforcement is not meaningfully supported yet. - -### Endpoint Specs - -`--add-endpoint` uses this format: - -```text -host:port[:access[:protocol[:enforcement[:options]]]] -``` - -Each segment has a fixed meaning: - -| Segment | Required | Meaning | -|---|---|---| -| `host` | Yes | Destination hostname. | -| `port` | Yes | Destination port, `1` through `65535`. | -| `access` | No | Access preset for L7 endpoints: `read-only`, `read-write`, or `full`. Incremental updates expand presets into protocol-specific method/path rules for REST and WebSocket endpoints. | -| `protocol` | No | Endpoint mode accepted by `openshell policy update`: `tcp`, `rest`, `websocket`, or `sql`. Use `tcp` for native DNS and TCP without L7 inspection. `sql` is audit-only and not a recommended workflow today. Full policy YAML also supports `graphql`, `mcp`, and `json-rpc`. | -| `enforcement` | No | Enforcement mode for inspected traffic: `enforce` or `audit`. | -| `options` | No | Comma-separated endpoint options. Use `websocket-credential-rewrite` with `protocol: websocket` or REST compatibility endpoints that perform a WebSocket upgrade. Use `request-body-credential-rewrite` only with `protocol: rest`. | - -Examples: - -| Example | Meaning | -|---|---| -| `pypi.org:443` | Add a plain L4 endpoint. The proxy allows the TCP stream and does not inspect HTTP requests. | -| `telemetry.example.com:443::::allow-uninspected-credentials` | Explicitly allow a provider-credentialed L4 endpoint after accepting that OpenShell cannot inspect or rewrite its traffic. | -| `db.internal.example:5432::tcp` | Add an L4 endpoint for native DNS resolution and transparent TCP capture. The empty `access` segment is required before `tcp`. | -| `api.github.com:443:read-only:rest:enforce` | Add a REST endpoint with the `read-only` preset expanded by the policy engine into GET, HEAD, and OPTIONS access. | -| `api.example.com:443:read-write:rest:enforce:request-body-credential-rewrite` | Add a REST endpoint that rewrites credential placeholders in supported text request bodies. | -| `realtime.example.com:443:read-write:websocket:enforce` | Add a WebSocket endpoint with the `read-write` preset expanded by the policy engine into the upgrade `GET` and client `WEBSOCKET_TEXT` access. | -| `realtime.example.com:443:read-write:websocket:enforce:websocket-credential-rewrite` | Add a WebSocket endpoint that rewrites `openshell:resolve:env:*` placeholders in client text frames after an allowed upgrade. | - -If you set `protocol: rest` or `protocol: websocket`, you also need an allow shape. With incremental updates, that means you should provide an `access` preset on `--add-endpoint`, then use `--add-allow` or `--add-deny` to refine method/path rules later. - -Use the `websocket-credential-rewrite` endpoint option with `protocol: websocket` when the sandbox should send credential placeholders in client text frames and have OpenShell resolve them after the allowed upgrade. The option can also be used with `protocol: rest` compatibility endpoints that perform a WebSocket upgrade. It is rejected for plain L4 or `protocol: sql` endpoints. - -Use the `request-body-credential-rewrite` endpoint option with `protocol: rest` when an API expects OpenShell-managed credentials in UTF-8 JSON, form, or text request bodies. OpenShell buffers up to 256 KiB, rewrites recognized credential placeholders, updates `Content-Length`, and rejects unresolved placeholders instead of forwarding them. For chunked requests, the 256 KiB limit counts the complete wire representation, including framing, extensions, and trailers. The option is rejected for WebSocket, GraphQL, SQL, and plain L4 endpoints. - -Use `allow-uninspected-credentials` only when a provider-credentialed endpoint must remain L4-only, use `tls: skip`, or carry uninspectable WebSocket traffic. Without this explicit opt-in, the gateway rejects credentialed L4-only and `tls: skip` endpoints. When REST body rewriting is disabled, unknown placeholder text and valid issued placeholders pass unchanged; invalid or unavailable references fail closed. - -Credential rewrite recognizes the canonical `openshell:resolve:env:KEY` placeholder form and whole-token provider-shaped aliases such as `provider-OPENSHELL-RESOLVE-ENV-API_TOKEN` when the referenced environment key exists in the configured provider credentials. - -Static provider placeholders resolve only when the request host, port, and path -also match an endpoint in the provider profile. A sandbox policy allow does not -expand that binding. A mismatch returns HTTP 403 with -`credential_endpoint_mismatch`. Refer to [Static Credential Endpoint -Binding](/how-it-works/providers/profiles#understand-static-credential-endpoint-binding). - -For example: - -- `db.internal.example:5432::tcp` is valid. -- `api.github.com:443:read-only:rest` is valid. -- `realtime.example.com:443:read-write:websocket` is valid. -- `api.github.com:443::rest` is invalid. It does not mean "allow all traffic." An L7 endpoint with `protocol` but no `access` or `rules` is rejected when the policy loads. - -Endpoint options belong to the individual `--add-endpoint` spec. When you pass multiple `--add-endpoint` flags in one command, every `--binary` value applies to every added endpoint in that command. If different endpoints need different binaries, use separate `policy update` commands. - -If you do not pass `--rule-name`, OpenShell generates one from the host and port, such as `allow_api_github_com_443`. - -### Method/Path Rule Specs - -`--add-allow` and `--add-deny` use this format: - -```text -host:port:METHOD:path_glob -``` - -This string identifies an existing REST or WebSocket endpoint and the request pattern you want to add. - -In shell commands, quote the full `SPEC` when it contains `*` or `**` so your shell passes it literally instead of expanding it as a local file glob. - -| Segment | Meaning | -|---|---| -| `host` | Existing endpoint host. | -| `port` | Complete comma-separated port set of the existing endpoint, such as `443,8443`. | -| `METHOD` | HTTP method for REST endpoints, or `GET` / `WEBSOCKET_TEXT` for WebSocket endpoints. The CLI normalizes it to uppercase. | -| `path_glob` | URL path glob. For WebSocket text messages, this still matches the upgraded request path, not message payload content. It must start with `/`, or be `**`, or start with `**/`. | - -This example: - -```text -api.github.com:443:POST:/repos/*/issues -``` - -means: - -- match the endpoint `api.github.com:443`. -- match HTTP method `POST`. -- match paths like `/repos/acme/issues`. -- also match deeper paths when the surrounding literals align, because `*` may include `/`. - -Path globs follow the same semantics as YAML allow and deny rules: - -- `*` and `**` match zero or more characters and may cross `/` boundaries. -- `?` matches exactly one character. -- bracket classes such as `[0-9]` and negated classes such as `[!0]` are supported. -- `/repos/*/issues` matches any intervening text, including multiple path segments. -- `/repos/**` matches everything under `/repos/`. - -The rule-level commands only modify method and path constraints. They do not change binaries, hostnames, ports, protocol settings, or WebSocket message payload matching. Each change applies to every binary and port on the target endpoint. Supply `--rule-name` and every binary with repeated `--binary`, or use `--any-binary` when the stored rule has an empty binary list. The declared binary and port sets must match the stored scope exactly; OpenShell reports the expected scope when they differ. - -Use `--endpoint-path` to select an endpoint by its stored path when the rule contains more than one endpoint matching the host and ports. `--endpoint-path ''` selects an endpoint without a path selector. This selector is separate from the request path in the allow or deny rule. All L7 additions in one command use the same rule name, binary declaration, and endpoint-path selector. - -### Common Workflows - -Use these patterns as starting points when you decide whether to update an endpoint or append REST/WebSocket rules. - -#### Add a new L4 endpoint - -Use `--add-endpoint` when you need a new host and port and do not need REST inspection. - -```shell -openshell policy update demo \ - --add-endpoint pypi.org:443 \ - --add-endpoint files.pythonhosted.org:443 \ - --binary /usr/bin/pip \ - --binary /usr/local/bin/uv \ - --wait -``` - -This creates or merges endpoint entries and binds them to the listed binaries. It does not create inspected method/path rules. - -#### Create a REST endpoint with a base allow set - -Use `--add-endpoint` first when the endpoint does not exist yet. - -```shell -openshell policy update demo \ - --add-endpoint api.github.com:443:read-only:rest:enforce \ - --binary /usr/bin/gh \ - --wait -``` - -This creates a REST endpoint and sets its base allow behavior through the `read-only` access preset. - -#### Add one more REST allow rule - -Use `--add-allow` after the REST endpoint already exists. - -```shell -openshell policy update demo \ - --add-allow 'api.github.com:443:POST:/repos/*/issues' \ - --rule-name allow_api_github_com_443 \ - --binary /usr/bin/gh \ - --wait -``` - -This keeps the existing endpoint definition and appends one new allow rule. It does not add binaries or change the endpoint host and port. - -#### Add a REST deny rule under an allowed endpoint - -Use `--add-deny` when you want to carve out a blocked subtree under an existing REST endpoint. - -```shell -openshell policy update demo \ - --add-deny 'api.github.com:443:POST:/admin/**' \ - --rule-name allow_api_github_com_443 \ - --binary /usr/bin/gh \ - --wait -``` - -This adds a deny rule to the existing REST endpoint. The endpoint must already have an allow base. - -#### Create a WebSocket endpoint with a base allow set - -Use `--add-endpoint` with `protocol: websocket` when the destination is an RFC 6455 WebSocket API. - -```shell -openshell policy update demo \ - --add-endpoint realtime.example.com:443:read-write:websocket:enforce:websocket-credential-rewrite \ - --binary /usr/bin/node \ - --wait -``` - -This creates a WebSocket endpoint and sets its base allow behavior through the `read-write` access preset. For WebSocket endpoints, `read-write` expands to the upgrade `GET` and client `WEBSOCKET_TEXT` messages on the upgraded request path. The rewrite option lets the sandbox send `openshell:resolve:env:*` placeholders in client text frames; OpenShell resolves them before forwarding to the upstream service. - -#### Add a WebSocket text-message deny rule - -Use `WEBSOCKET_TEXT` when you want to refine client-to-server text-frame policy without matching message payload content. - -```shell -openshell policy update demo \ - --add-deny 'realtime.example.com:443:WEBSOCKET_TEXT:/v1/admin/**' \ - --rule-name allow_realtime_example_com_443 \ - --binary /usr/bin/node \ - --wait -``` - -This adds a deny rule to the existing WebSocket endpoint. The path glob matches the WebSocket upgrade path. - -#### Remove one endpoint or rule - -Use `--remove-endpoint` to remove one host and port pair, or `--remove-rule` to delete the whole named rule. - -```shell -openshell policy update demo --remove-endpoint pypi.org:443 --wait -openshell policy update demo --remove-rule github_repos --wait -``` - -If the target endpoint is part of a multi-port endpoint, `--remove-endpoint` removes only the specified port and keeps the rest. - -### Merge Semantics - -OpenShell applies all update flags from one `openshell policy update` command as one merge batch. The gateway validates the full merged result and persists at most one new policy revision. - -This means: - -- one command is atomic at the revision level. -- multiple flags in one command succeed or fail together. -- concurrent writers do not partially interleave one batch with another. - -When two updates race, the gateway uses optimistic retry. It fetches the latest revision, reapplies the full batch, validates the result again, and retries the write. This preserves the intent of each individual command while still allowing concurrent sandbox policy updates. - -### Preview and Validation - -Use `--dry-run` when you want to inspect the merged YAML before you send it to the gateway. - -```shell -openshell policy update demo \ - --add-allow 'api.github.com:443:GET:/repos/**' \ - --rule-name allow_api_github_com_443 \ - --binary /usr/bin/gh \ - --dry-run -``` - -The CLI validates the argument shapes before it sends the request. The gateway then validates the merged policy against the current live policy and returns clear errors when: - -- a required segment is missing. -- a port is outside `1` through `65535`. -- `--add-allow` or `--add-deny` points at an endpoint that does not exist. -- `--add-allow` or `--add-deny` targets an endpoint that is neither REST nor WebSocket. -- `--add-allow` or `--add-deny` omits the rule name or binary declaration, names incomplete binary or port scope, or still matches several endpoints within the selected rule. -- `--add-deny` targets an endpoint that has no base allow set. -- an update names an existing rule and adds a binary to it without declaring every endpoint and port that rule already authorizes. -- an update names an existing rule and adds or changes an endpoint on it without declaring every binary that rule already authorizes. -- an update widens a rule to any binary without declaring every endpoint that rule already authorizes. -- an update changes an endpoint without declaring every port that endpoint carries. -- an update puts an MCP endpoint on the same host and port as a differently inspected endpoint, or gives one host and port two different MCP inspection contracts, including through separate rules or different paths. - -A rule authorizes every listed binary to reach every listed endpoint and port, so merging a binary and an endpoint into the same rule authorizes that pair too. The gateway rejects the whole batch rather than granting a pair the update did not ask for. The error names the binary scope and the ports involved, and lists the binaries you still need to declare. An empty binary list means any binary, so widening a rule to any binary is subject to the same requirement. - -You can declare the scope across several `--add-endpoint` arguments. The update is complete as long as every binary-to-port pair the merged rule ends up authorizing appears somewhere in the update. - -An endpoint's allow rules, deny rules, and allowed IPs apply to every port that endpoint carries, so an update that changes any of them has to name every one of those ports. Declaring `api.example.com:443` alone on an endpoint that also serves `8443` is rejected, because the change would reach `8443` as well. Declaring every existing binary does not lift this requirement; the two are separate axes of the same product. - -The sandbox picks the parser for a request by most-specific path, but it authorizes the request against every endpoint that matches it. A broad REST endpoint and a narrower GraphQL endpoint on one host and port share the same method-and-path rule vocabulary, so that combination stays supported. MCP does not: its rules address JSON-RPC methods and tool names, so a plain REST rule on an overlapping path could authorize a tool call the MCP endpoint denies. An MCP endpoint therefore cannot share a host and port with a differently inspected endpoint, and two MCP endpoints there must agree on their exact revision allowlist, strict-tool-name, method-profile, and body-limit settings, even under different paths or in separate rules. An update creating either situation is rejected. A policy that already contains one is left alone so unrelated updates still apply, but it should be repaired with full YAML replacement. - -To grant one binary access to only part of an existing rule's endpoints, send it under its own `--rule-name`. The gateway normally folds an update into an existing rule that shares an endpoint, but it keeps your rule name whenever folding would grant authorization you did not declare, so the narrow grant lands as its own rule authorizing exactly what you asked for. The update reports that it kept your rule name and names the rule it would otherwise have folded into. An MCP contract conflict is the exception: one host and port carry a single MCP contract regardless of which rule holds them, so a conflicting update is rejected rather than moved to a separate rule. - -When several rules contain the same host and port, `--rule-name` selects the rule to change. Within that rule, use `--endpoint-path` if several endpoints match. OpenShell rejects missing, ambiguous, or mismatched targets atomically. Provider-owned rules cannot be changed through these operations; update the provider profile instead. - -For example, this appends one permission to an endpoint shared by two binaries on two ports: - -```shell -openshell policy update demo \ - --rule-name internal_api \ - --binary /usr/bin/curl \ - --binary /usr/bin/python3 \ - --add-allow 'api.example.com:443,8443:POST:/admin' \ - --wait -``` - -The command explicitly declares all four binary-to-port combinations affected by the new permission. Omitting either binary or either port rejects the whole update. To grant a permission to a smaller scope, create a separate rule with `--add-endpoint` or edit the policy structure first. - -## Global Policy Override - -Use a global policy when you want one policy payload to apply to every sandbox. - -```shell -openshell policy set --global --policy ./global-policy.yaml -``` - -When a global policy is configured: - -- The global payload is applied in full for all sandboxes. -- Sandbox-level policy updates are rejected until the global policy is removed. - -To restore sandbox-level policy control, delete the global policy setting: - -```shell -openshell policy delete --global -``` - -You can inspect a sandbox's effective settings and policy source with: - -```shell -openshell settings get -``` - -## Debug Denied Requests - -Check `openshell logs --tail --source sandbox` for the denied host, path, and binary. - -For agent-authored draft updates on running sandboxes, enable [Policy Advisor](/how-it-works/policies/advisor). Policy advisor lets the sandboxed agent submit a narrow proposal through `policy.local` while a developer still approves or rejects the structured rule from outside the sandbox. - -When triaging denied requests, check: - -- Destination host and port to confirm which endpoint is missing. -- Calling binary path to confirm which `binaries` entry needs to be added or adjusted. -- HTTP method and path for REST endpoints, or `GET` / `WEBSOCKET_TEXT` and the upgraded request path for WebSocket endpoints, to confirm which `rules` entry needs to be added or adjusted. -- `credential_endpoint_mismatch` in sandbox logs to confirm that policy admitted the request but the attached provider profile did not authorize its credential for that host, port, and path. -- `request_authority_mismatch` in the response or sandbox logs to confirm that the HTTP request authority differs from the authorized tunnel endpoint. For a CONNECT tunnel to `api.example.com:8443`, send `Host: api.example.com:8443`; omitting the non-default port makes the request authority use the transport default and OpenShell rejects it. Absolute-form request targets must use the same host and port. - -Then push the updated policy as described above. - -Do not fix `credential_endpoint_mismatch` by widening sandbox policy. Export the -provider profile with `openshell profile export -o yaml`. -Update the custom provider profile only when the destination is an intended -credential recipient. Refer to [Static Credential Endpoint -Binding](/how-it-works/providers/profiles#understand-static-credential-endpoint-binding) -for the complete authorization model. - -For small changes, prefer `openshell policy update` over rewriting the full YAML: - -```shell -openshell policy update --rule-name allow_api_github_com_443 --binary /usr/bin/gh --add-allow 'api.github.com:443:GET:/repos/**' --wait -``` - -## Examples - -Add these blocks to the `network_policies` section of your sandbox policy. Apply simple endpoints and REST/WebSocket rule additions with `openshell policy update`, or apply any complete YAML block with `openshell policy set --policy --wait`. -Use **Simple endpoint** for host-level allowlists and **Granular rules** for method/path control. - - - -Allow `pip install` and `uv pip install` to reach PyPI: - -```yaml showLineNumbers={false} - pypi: - name: pypi - endpoints: - - host: pypi.org - port: 443 - - host: files.pythonhosted.org - port: 443 - binaries: - - { path: /usr/bin/pip } - - { path: /usr/local/bin/uv } -``` - -Endpoints without `protocol` use explicit-proxy TCP passthrough, where OpenShell allows the stream without inspecting payloads. Use `protocol: tcp` when the application needs ordinary DNS resolution and native TCP connections through transparent capture. Provider-credentialed endpoints cannot use either L4 shape unless `allow_uninspected_credentials: true` records the exception. If an explicit-proxy stream is HTTP and TLS is auto-terminated, the proxy can still rewrite configured credential placeholders and closes keep-alive passthrough tunnels on policy reload before forwarding another request. WebSocket text-frame policy requires an explicit `protocol: websocket` endpoint. WebSocket payload credential rewrite can also be enabled on a `protocol: rest` compatibility endpoint with `websocket_credential_rewrite: true`. REST request body credential rewrite requires an inspected `protocol: rest` endpoint with `request_body_credential_rewrite: true`. - - - -Allow Claude and the GitHub CLI to reach `api.github.com` with separate REST and GraphQL endpoint scopes: read-only REST for general API paths, GraphQL operation inspection on `/graphql`, full REST write access for `alpha-repo`, and create/edit issues only for `bravo-repo`. Replace `` with your GitHub org or username. - - -For an end-to-end walkthrough that combines this policy with a GitHub credential provider and sandbox creation, refer to [GitHub Sandbox](/tutorials/github-push-access). - - - -```yaml showLineNumbers={false} - github_repos: - name: github_repos - endpoints: - - host: api.github.com - port: 443 - path: "/**" - protocol: rest - enforcement: enforce - rules: - - allow: - method: GET - path: "/**" - - allow: - method: HEAD - path: "/**" - - allow: - method: OPTIONS - path: "/**" - - allow: - method: "*" - path: "/repos//alpha-repo/**" - - allow: - method: POST - path: "/repos//bravo-repo/issues" - - allow: - method: PATCH - path: "/repos//bravo-repo/issues/*" - - host: api.github.com - port: 443 - path: "/graphql" - protocol: graphql - enforcement: enforce - rules: - - allow: - operation_type: query - - allow: - operation_type: mutation - fields: [createIssue, updateIssue, addComment] - deny_rules: - - operation_type: mutation - fields: [deleteRepository, deleteRef, updateBranchProtectionRule] - binaries: - - { path: /usr/local/bin/claude } - - { path: /usr/bin/gh } -``` - -Endpoints with `protocol: rest` enable HTTP request inspection and can opt in to supported text request body credential rewrite. Endpoints with `protocol: websocket` validate WebSocket upgrades and inspect client text messages on the upgraded request path. WebSocket endpoints can also classify GraphQL-over-WebSocket operation messages with the same operation rules used by GraphQL-over-HTTP. Endpoints with `protocol: graphql` parse GraphQL-over-HTTP payloads before evaluating rules. Endpoints with `protocol: mcp` parse MCP Streamable HTTP request bodies and evaluate `method`, optional `tool`, and supported params rules. Endpoints with `protocol: json-rpc` parse JSON-RPC-over-HTTP request bodies and evaluate `method` rules. The endpoint-level `path` field lets these protocols share `api.github.com:443` without treating GraphQL payloads as plain REST `POST /graphql` requests. - - - - -### Query parameter matching - -REST rules can also constrain query parameter values: - -```yaml showLineNumbers={false} - download_api: - name: download_api - endpoints: - - host: api.example.com - port: 443 - protocol: rest - enforcement: enforce - rules: - - allow: - method: GET - path: "/api/v1/download" - query: - slug: "skill-*" - version: - any: ["1.*", "2.*"] - binaries: - - { path: /usr/bin/curl } -``` - -`query` matchers are case-sensitive and run on decoded values. If a request has duplicate keys (for example, `tag=a&tag=b`), every value for that key must match the configured glob(s). - -### MCP and JSON-RPC matching - -MCP endpoints use `protocol: mcp`. The proxy parses sandbox-to-server MCP Streamable HTTP request bodies, validates known MCP request and notification params, can evaluate the MCP method against rule `method`, and can match tool calls with the `tool` alias. Unknown extension methods stay addressable as literal method strings. You may omit the entire `mcp` stanza when using its defaults, or omit only `mcp.versions` when setting another MCP option. OpenShell immediately resolves either form to the exact `["2025-11-25"]` allowlist and stores the materialized list in canonical policy data. Adding another supported revision therefore never widens a normalized policy. Defaulting requires the `mcp` or `mcp.versions` key to be absent; explicit `mcp: null`, `versions: null`, and `versions: []` values are invalid. Use an explicit nonempty allowlist only for intentional compatibility or downgrade control. Supported revisions are `2025-03-26`, `2025-06-18`, and `2025-11-25`; the exact support floor is `2025-03-26`, and this is a closed set rather than a date range. Explicit values must be unique and contain no extra whitespace; OpenShell stores them in semantic order. Moving aliases such as `draft` or `latest` are rejected because they could change policy meaning without a policy edit; omission never means all known versions. The sessionless `2026-07-28` revision is not accepted until OpenShell supports its distinct per-request contract. A version identifies a core MCP revision only; there is no policy syntax for separately named SEP overlays. For every MCP HTTP request except a valid standalone `initialize`, OpenShell selects the revision from one `MCP-Protocol-Version` header or, when the header is absent, the MCP specification's `2025-03-26` compatibility fallback. The selected revision must appear in the allowlist. Duplicate, empty, and unsupported header values receive `400 Bad Request`; a supported revision outside the allowlist receives `403 Forbidden`. OpenShell checks the request again after middleware changes it and before forwarding. Generic JSON-RPC endpoints do not use this header and continue to evaluate only `method`. `mcp.allow_all_known_mcp_methods` defaults to `false`, so endpoints require explicit MCP method rules. Set it to `true` to enable the endpoint method profile; in that mode, rules can omit `method`, and tool selectors are normalized to `tools/call` internally. By default, MCP `tools/call` tool names must match `^[A-Za-z0-9_.-]{1,128}$`; set `mcp.strict_tool_names: false` on that endpoint only when a server intentionally uses names outside the MCP-recommended pattern. Wildcard `tool` matchers require `mcp.strict_tool_names` to remain enabled. - -The current registry declares these profile facts for later runtime enforcement: - -- `2025-03-26` permits nonempty same-side top-level JSON-RPC batches; OpenShell's planned enforcement caps them at 64 members. -- `2025-06-18` prohibits top-level JSON-RPC arrays. -- `2025-11-25` prohibits top-level JSON-RPC arrays. - -OpenShell uses the per-request header only to enforce the allowlist; the current parser does not yet apply these version-specific batch rules. OpenShell does not treat the client's `initialize.params.protocolVersion` as the selected revision, inspect the server's initialization response, or bind `MCP-Session-Id`. The version check is stateless and applies independently to each later request, including GET and DELETE. - -MCP endpoints must declare a concrete destination with `host` and `port` or `ports`. A policy entry that only sets `protocol: mcp` is invalid and is not treated as a wildcard MCP authorization. Use `path: /mcp` when the server's MCP endpoint is path-scoped; omitting `path` matches every HTTP path on that host and port. - -Existing versionless inspected MCP policies remain valid and normalize to the exact `2025-11-25` default; they do not need a migration edit. This is a pinned default, not a moving latest or all-known selection. For an unsupported revision, omit `protocol` and the `mcp` stanza to authorize deliberate, uninspected TCP passthrough only when that weaker L4 boundary is acceptable; OpenShell does not silently downgrade to an older inspected profile. - -MCP policy enforcement is directional. It applies to HTTP request bodies sent by the sandboxed process to the configured endpoint. JSON-RPC responses and server-to-client MCP messages carried on response bodies or SSE streams are relayed but are not currently parsed for policy enforcement. - -MCP and JSON-RPC endpoint policies currently require full policy YAML applied with `openshell policy set`; the incremental `openshell policy update --add-endpoint` parser does not accept `mcp` or `json-rpc` as protocols. - -An MCP client first sends `initialize`. After the server returns a successful response, the client sends `notifications/initialized`. After initialization completes and the server advertises the `tools` capability, the client can call an advertised tool. The response does not need an allow rule because these rules inspect messages sent from the client to the server. This example adds both client initialization messages to the existing tool rules. It omits `tools/list` because it assumes the client already knows the tool names; add that method when the client performs discovery. - -```yaml showLineNumbers={false} - mcp_server: - name: mcp_server - endpoints: - - host: mcp.example.com - port: 443 - path: /mcp - protocol: mcp - enforcement: enforce - rules: - - allow: - method: initialize - - allow: - method: notifications/initialized - - allow: - method: tools/call - tool: read_status - - allow: - method: tools/call - tool: - any: [submit_report, list_reports] - deny_rules: - - method: tools/call - tool: delete_resource - binaries: - - { path: /usr/bin/python3 } -``` - -The example omits the entire `mcp` stanza, so OpenShell uses the exact `2025-11-25` revision and the other MCP defaults. Canonical serialization still shows the materialized revision list. To allow an older server intentionally, add an explicit compatibility or downgrade allowlist: - -```yaml showLineNumbers={false} - mcp: - versions: ["2025-03-26", "2025-11-25"] -``` - -OpenShell canonicalizes this list in semantic order. Later runtime negotiation selects one allowed revision and applies only that profile; it does not combine the two profiles. `mcp.max_body_bytes` controls how many MCP-over-HTTP request body bytes OpenShell buffers for inspection and defaults to `65536`. `mcp.strict_tool_names` defaults to `true` for each MCP endpoint. `mcp.allow_all_known_mcp_methods` defaults to `false`; when it is unset or `false`, the endpoint must define explicit MCP method rules. If an MCP endpoint sets `mcp.allow_all_known_mcp_methods: true` and omits `rules`, OpenShell allows all MCP-family methods and all tools, then applies any `deny_rules`. A broad allow or deny rule whose method matcher includes `tools/call` cannot be combined with tool-specific allow rules because it would bypass or erase the tool filter; add `tool` or `params.name` to scope `tools/call`, or remove the tool-specific rules. - -Use `protocol: json-rpc` and `method` when you need generic JSON-RPC 2.0 matching for a non-MCP server. Generic JSON-RPC method rules accept exact method names, or `method: "*"` as the all-method sentinel; other wildcard or glob patterns are rejected. `json_rpc.max_body_bytes` controls the generic JSON-RPC inspection buffer. - -Generic JSON-RPC policy `params` matchers are not supported. Generic JSON-RPC policy rules match only the JSON-RPC method. For batch requests, OpenShell evaluates each JSON-RPC call independently and denies the whole batch if any call is denied. - -For MCP, `tool` accepts a string glob or `{ any: [...] }` matcher for `tools/call` `params.name`. Rules that use `tool` or lower-level `params.name` must set `method: tools/call` unless `mcp.allow_all_known_mcp_methods: true` enables the endpoint method profile. MCP method globs are accepted only for the `tools/` method family, such as `tools/*`; omit `method` instead of writing `method: "*"` only when the endpoint method profile should allow all MCP methods. Omit `tool` to allow all tools for a `tools/call` method rule. OpenShell does not support MCP tool argument matching yet; allowed tools accept all argument payloads by default. Other MCP `params` keys are rejected. For batch requests, OpenShell evaluates each JSON-RPC call independently and denies the whole batch if any call is denied. - -### GraphQL matching - -GraphQL endpoints use `protocol: graphql`. The proxy parses GraphQL-over-HTTP `GET` and `POST` requests, classifies each operation, and evaluates rules against the operation type, optional operation name, and selected root fields. - -GraphQL endpoint policies currently require full policy YAML applied with `openshell policy set`; the incremental `openshell policy update --add-endpoint` parser does not accept `graphql` as a protocol. - -```yaml showLineNumbers={false} - github_graphql: - name: github_graphql - endpoints: - - host: api.github.com - port: 443 - path: "/graphql" - protocol: graphql - enforcement: enforce - rules: - - allow: - operation_type: query - fields: [viewer, repository] - - allow: - operation_type: mutation - operation_name: Issue* - fields: [createIssue] - deny_rules: - - operation_type: mutation - fields: [deleteRepository] - binaries: - - { path: /usr/bin/gh } -``` - -For allow rules, every selected root field in an operation must match one of the configured `fields` globs. For deny rules, one matching root field blocks the request. Batched GraphQL requests are fail-closed: if any operation is malformed, denied, or unregistered, the whole HTTP request is denied. - -Hash-only persisted queries cannot be classified from the request alone. OpenShell denies them unless the endpoint uses `persisted_queries: allow_registered` and provides a trusted `graphql_persisted_queries` entry keyed by hash or saved-query ID. - -### GraphQL-over-WebSocket matching - -Some APIs carry GraphQL operations over RFC 6455 WebSockets, commonly for subscriptions and realtime updates. Configure these as `protocol: websocket`, allow the upgrade with a normal `GET` rule, then add GraphQL operation rules for client operation messages. OpenShell recognizes modern `graphql-transport-ws` `subscribe` messages and legacy `graphql-ws` `start` messages. - -```yaml showLineNumbers={false} - realtime_graphql: - name: realtime_graphql - endpoints: - - host: realtime.example.com - port: 443 - path: "/graphql" - protocol: websocket - enforcement: enforce - rules: - - allow: - method: GET - path: "/graphql" - - allow: - operation_type: subscription - fields: [messageAdded] - - allow: - operation_type: query - fields: [viewer] - websocket_credential_rewrite: true - binaries: - - { path: /usr/bin/node } -``` - -When a WebSocket endpoint has GraphQL operation policy, client operation messages are fail-closed on malformed JSON, unsupported message types, parse errors, unregistered hash-only persisted queries, or unallowed operations. Use GraphQL operation rules for client messages rather than a raw `WEBSOCKET_TEXT` allow rule. Protocol lifecycle messages such as `connection_init`, `ping`, `pong`, and `complete` are allowed without payload logging; if `websocket_credential_rewrite: true` is set, placeholders inside those text messages are resolved before forwarding. - -### GraphQL service policy shapes - -GraphQL field names are application-specific, so treat these as starting shapes to review against the actual app schema: - -| Service | Endpoint shape | Starting policy | -|---|---|---| -| Railway | `backboard.railway.app/graphql/v2` | Allow `query`; allow only reviewed deployment mutations; deny `volumeDelete`, `projectDelete`, `*Delete`, `*Destroy`. | -| GitHub | `api.github.com/graphql` | Allow `query`; optionally allow low-risk mutations such as reactions; deny broad destructive/admin roots like `deleteRef`, `deleteRepository`, `updateBranchProtectionRule`, and `delete*`. | -| GitLab | `/api/graphql` | Prefer read-only token scopes where possible; allow `query`; deny mutations by default or allow only reviewed workflow roots. | -| Shopify Admin | `*.myshopify.com/admin/api/**/graphql.json` | Allow `query`; allow app-specific mutations only; deny `*Delete`, `bulkOperationRunMutation`, and high-impact inventory/order/customer roots unless approved. | -| monday.com | `api.monday.com/v2` | Allow board/item reads; allow tightly scoped create/update mutations only where needed; deny delete/archive roots. | -| Salesforce GraphQL | Salesforce GraphQL endpoint | Allow `query`; deny record create/update/delete mutations unless the sandbox is intended to modify CRM data. | -| Hygraph | Project content API endpoint | Allow content reads; deny generated destructive content roots such as `delete*`, `deleteMany*`, `unpublish*`, and batch mutations unless a publishing workflow requires them. | -| Atlassian GraphQL Gateway | `api.atlassian.com/graphql` | Allow reads by default; require explicit mutation allowlists because the gateway spans Jira, Confluence, Bitbucket, and admin surfaces. | +A sandbox policy is a YAML configuration that controls what a sandbox can +access. It defines which files processes in the sandbox can read and write, +which user they run as, which network destinations each binary can reach, and +which requests it can send. OpenShell denies anything the policy does not allow. + +This page explains how policies work. To try one in a running sandbox, follow +[Write Your First Sandbox Network Policy](/tutorials/first-network-policy). To +create and change policies, refer to [Manage Sandbox +Policies](/how-it-works/policies/manage-policies). + +## What a Policy Controls + +A policy file sets `version: 1` and has up to five other top-level sections. +Each section controls a different part of the sandbox and takes effect at a +specific time: + +| Section | Controls | Enforced by | Takes effect | +|---|---|---|---| +| `filesystem_policy` | Paths that sandbox processes can read, or read and write. | Landlock LSM in the kernel. | At sandbox startup. | +| `landlock` | Whether the sandbox still starts, without your filesystem rules, if OpenShell cannot apply them. | Sandbox runtime. | At sandbox startup. | +| `process` | User and group that sandbox processes run as. | Docker and Podman, when they create the sandbox. | At sandbox creation. | +| `network_policies` | Destinations each binary can reach, and the requests it can send. | Sandbox network proxy. | While the sandbox runs. | +| `network_middlewares` | Additional inspection, transformation, or blocking of traffic that network rules allow. | Sandbox network proxy. | While the sandbox runs. | + +Filesystem, Landlock, and process settings are fixed once the sandbox starts. +Network rules and middleware can change while it runs, as described in [How +Changes Take +Effect](/how-it-works/policies/manage-policies#how-changes-take-effect). + +Network rules make up most of a typical policy. OpenShell denies every outbound +connection from a sandbox unless a rule in `network_policies` allows it. Each +rule lists the destinations it allows and the binaries that can reach them, and +can also restrict the requests those binaries send, for example to allow reading +from an API but not writing to it. [Network +Rules](/how-it-works/policies/network-rules) explains how OpenShell evaluates +rules and gives examples you can adapt. + +The [Policy Schema Reference](/how-it-works/policies/schema) describes every +field in each section, and [Supervisor +Middleware](/extensibility/supervisor-middleware) explains how middleware +processes traffic. + +## Where the Active Policy Comes From + +Every sandbox runs under a policy, even when you do not supply one. When more +than one policy is available, OpenShell uses the first applicable source in this +order: + +1. A global policy, which a gateway administrator applies to every sandbox. +2. The sandbox's saved policy. At creation, `--policy` takes precedence over + `OPENSHELL_SANDBOX_POLICY`. Later policy changes update the saved policy. +3. A policy included in the sandbox image. +4. OpenShell's restrictive [default + policy](/how-it-works/policies/default-policy). + +An invalid image policy keeps the workload from starting until you repair it. +OpenShell does not skip it and use the default. + +### Base and Effective Policies + +The selected sandbox policy is the base policy. Attached providers can +contribute additional network rules, for example so that a GitHub provider can +reach the GitHub API. The effective policy combines the base policy with those +provider rules, and it is the policy the sandbox enforces. + +Start edits from the base policy so you do not copy provider-owned rules into +your own configuration. Inspect the effective policy when you need to know what +the sandbox can reach. [Inspect the Current +Policy](/how-it-works/policies/manage-policies#inspect-the-current-policy) shows +both views. + +### Global Policy + +A gateway administrator can apply one policy to every sandbox on the gateway. +The global policy replaces each sandbox's own policy rather than limiting it. +While it is active, OpenShell blocks sandbox policy changes and proposal +approvals, and suppresses provider-contributed network rules. Deleting the +global policy restores normal policy selection and provider rules. Refer to +[Apply a Global +Policy](/how-it-works/policies/manage-policies#apply-a-global-policy) for the +commands. ## Next Steps -Explore related topics: - -- To learn about the built-in sandbox policy, refer to [Default Policy](/how-it-works/policies/default-policy). -- To view the full field-by-field YAML definition, refer to the [Policy Schema Reference](/how-it-works/policies/schema). -- To review the default policy breakdown, refer to [Default Policy](/how-it-works/policies/default-policy). +- Use [Network Rules](/how-it-works/policies/network-rules) to learn how + OpenShell evaluates network rules and to adapt examples for common services. +- Use [Manage Sandbox Policies](/how-it-works/policies/manage-policies) to + create, update, verify, and roll back policies. +- Use the [Policy Prover](/how-it-works/policies/prover) to check that a policy + grants no more access than a boundary you define. +- Use the [Policy Advisor](/how-it-works/policies/advisor) to let an agent + propose narrow network rules for your review. +- Use the [Policy Schema Reference](/how-it-works/policies/schema) for every + field, default, and validation rule. diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index 85c00d8d77..95b48dcf15 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -1,43 +1,63 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Standalone Policy Prover" -sidebar-title: "Prover" -description: "Install and use the standalone OpenShell policy prover to check a local candidate policy against a managed boundary." -keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, Containment, CI" +title: "Policy Prover" +sidebar-title: "Policy Prover" +description: "Understand how OpenShell uses the policy prover, and check that a policy grants no more access than a boundary policy you define." +keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, Subagent" position: 4 --- -`openshell-prover` checks whether the authority in a local candidate policy is -contained within a local boundary policy. The command reads files from the host -and does not connect to an OpenShell gateway. - -The candidate must be the fully composed effective policy after the proposed -change, including any provider-contributed authority. The boundary is an -operator-owned ceiling. It does not grant authority by itself. - -## Install the Prover - -The standard OpenShell installer includes `openshell-prover`: - -```shell -curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh -openshell-prover --version -``` - -The prover remains independent of the gateway at runtime. If you only need the -standalone binary, use the artifacts listed in the -[Support Matrix](/about/support-matrix#standalone-policy-prover). These -artifacts and `openshell-prover-checksums-sha256.txt` are attached to -[OpenShell releases](https://github.com/NVIDIA/OpenShell/releases). - -The release archive includes the solver linkage required by the executable. -It does not require the main `openshell` command, a gateway configuration, or -a separate Z3 installation. - -## Check a Policy Boundary - -Create `boundary.yaml` with this boundary policy: +The policy prover is OpenShell's policy verification engine. It uses an SMT +solver to check whether policies satisfy specified properties, such as staying +within an allowed access boundary. Its guarantees apply to the policy features +and behavior represented by its model. OpenShell is actively extending the model +to cover more policy features. + +OpenShell uses the prover for two types of checks: + +- A boundary check verifies that a policy allows no access beyond a boundary + policy, which defines the most access allowed in an environment. +- A proposal risk check verifies that a proposed network rule adds no risky + access compared with the sandbox's current policy, such as access to cloud + metadata addresses or new destinations for provider credentials. + +You run a boundary check with the `openshell-prover` CLI, which is primarily +intended for agents. For example, a parent agent can check that a policy it +writes for a subagent stays within the parent's own maximum allowed policy. A +proposal risk check runs automatically each time an agent proposes a network +rule through the [policy advisor](/how-it-works/policies/advisor). + +The two checks answer different questions, so passing one does not imply +passing the other. A proposed rule that passes the proposal risk check can still +exceed your boundary, and a policy that passes the boundary check can still +contain access that the proposal risk check would flag. + +This page covers the boundary check. To learn about the proposal risk check, +refer to [Policy Advisor](/how-it-works/policies/advisor). + +## Run a Boundary Check + +A boundary check compares the policy you are testing, called the candidate, with +a boundary policy that you write. The boundary is usually the most access that +an environment allows, such as an enterprise's maximum policy for its agents. If +the candidate allows something the boundary does not, the prover shows an +example. + +The check covers filesystem access, process identity, Landlock settings, network +connections, and REST requests. If a policy uses anything else, such as GraphQL +or MCP rules, the prover reports that it cannot check the policy instead of +ignoring those rules. [What the Boundary Check +Covers](#what-the-boundary-check-covers) describes each part and its limits. + +The Homebrew, Debian, and RPM packages install the `openshell-prover` CLI. The +snap package does not include it, so on a snap installation, download the +`openshell-prover` archive for your platform from the [OpenShell +releases](https://github.com/NVIDIA/OpenShell/releases). The CLI reads policy +files on your machine, does not need a gateway, and does not apply or approve +policies. + +Create `boundary.yaml`, a boundary that allows reading `/usr` and `/etc`: ```yaml version: 1 @@ -47,7 +67,7 @@ filesystem_policy: - /etc ``` -Create `candidate.yaml` with this contained candidate: +Create `candidate.yaml`, a candidate that allows reading only `/usr`: ```yaml version: 1 @@ -56,16 +76,22 @@ filesystem_policy: - /usr ``` -Run the check: +Check the candidate against the boundary: ```shell openshell-prover check candidate.yaml --boundary boundary.yaml ``` -A contained candidate prints `result: within_boundary` and exits with status `0`. -To see an exceeding result and its counterexample, replace `candidate.yaml` -with a policy that grants writes to `/tmp`. The boundary grants no write -access: +The candidate allows less than the boundary, so the check passes: + +```text +result: within_boundary +coverage: domains=filesystem,network_l4,network_rest,process,landlock +``` + +The `coverage` line lists the parts of a policy that the prover can check. Now +change the candidate so that it also allows writing to `/tmp`, which the +boundary does not allow: ```yaml version: 1 @@ -76,196 +102,130 @@ filesystem_policy: - /tmp ``` -```shell -openshell-prover check candidate.yaml --boundary boundary.yaml +Run the same command again. The check fails, and the `counterexample` line +shows access that the candidate allows but the boundary does not: + +```text +result: exceeds_boundary +coverage: domains=filesystem,network_l4,network_rest,process,landlock +counterexample: filesystem write /tmp ``` -To check the current effective policy of an existing sandbox, export it without -display metadata: +### Check a Sandbox's Policy + +To check the policy that a sandbox enforces, save its effective policy and use +it as the candidate: ```shell openshell sandbox get my-sandbox --policy-only > candidate.yaml openshell-prover check candidate.yaml --boundary boundary.yaml ``` -This export includes provider-contributed rules. For a proposed change that is -not active, supply the complete post-change effective policy. The standalone -prover does not compose a base policy with provider rules. - -Use JSON when a script consumes the result: +The effective policy includes rules from attached providers, so the check +includes network access that providers add. The prover does not add provider +rules itself. +To check a change before you apply it, give the prover the complete effective +policy as it would be after the change. -```shell -openshell-prover check candidate.yaml \ - --boundary boundary.yaml \ - --output json -``` +## Read the Result -The JSON object includes the result, exit code, input paths, schema version, -prover version, and modeled domains. `schema_version` versions the JSON contract, -`prover_version` identifies the implementation that produced the result, and -`coverage.domains` is the machine-readable declaration of modeled policy -domains. An exceeding result also includes a typed counterexample. Automation -should use `result` and `reason_code` instead of parsing the human-readable -explanation. - -| Field | When populated | -|---|---| -| `counterexample` | An object for `exceeds_boundary`; otherwise `null`. | -| `reason_code` | A stable identifier for an emitted `error`, `unsupported`, or `inconclusive` result; otherwise `null`. | -| `reason` | A human-readable explanation paired with `reason_code`; otherwise `null`. | - -The `counterexample.domain` field selects one of these objects: - -| Domain | Fields | -|---|---| -| `filesystem` | `access` (`read` or `write`) and `path`. | -| `process` | `field` (`run_as_user` or `run_as_group`), `boundary`, and `candidate`. | -| `landlock` | `boundary` and `candidate` compatibility modes. | -| `network` | `binary`, `ancestor_binary`, `binary_identity_required`, `host`, `destination_ip`, `trusted_gateway`, `port`, `protocol`, `method`, and `path`. | - -Network `binary` and `ancestor_binary` values are `null` when binary identity -enforcement is disabled. `method` and `path` are `null` for L4 witnesses. -`trusted_gateway: true` means the witness uses a recognized host-gateway alias -with a runtime-provided trusted gateway binding; `false` uses ordinary -destination validation. - -The stable reason codes are `invalid_input`, `unsupported_policy_shape`, -`unresolved_workdir`, `unresolved_binary_path`, -`unresolved_filesystem_path`, `solver_timeout`, `solver_unknown`, -`resource_limit`, `invalid_witness`, and `cancelled`. - -The solver has a finite 10-second default budget. Set a different positive -budget with an integer followed by `ms`, `s`, or `m`: +The prover reports one of these results: -```shell -openshell-prover check candidate.yaml \ - --boundary boundary.yaml \ - --timeout 30s \ - --output json +| Result | Exit code | Meaning | +|---|---|---| +| `within_boundary` | `0` | The candidate allows nothing beyond the boundary in the parts of the policy that the prover checks. | +| `exceeds_boundary` | `1` | The candidate allows access that the boundary does not. The counterexample shows one example. | +| `error` | `2` | The prover could not run the check, for example because a file is missing or a policy is invalid. | +| `unsupported` | `3` | A policy uses something the prover cannot check. The reason explains what. | +| `inconclusive` | `3` | The prover could not finish, for example because it ran out of time or the policies are too large. | + +Only `within_boundary` means that the check passed. Treat every other result as +a failure. For example, a candidate with a GraphQL rule returns: + +```text +result: unsupported +coverage: domains=filesystem,network_l4,network_rest,process,landlock +reason: candidate policy rule 'g' uses protocol 'graphql'; only L4 TCP and REST are modeled ``` -## Exit Codes - -| Exit | Result | Meaning | -|---|---|---| -| `0` | `within_boundary` | Containment was established for the reported modeled domains. | -| `1` | `exceeds_boundary` | The candidate exceeds the boundary; inspect the counterexample. | -| `2` | `error` | Arguments, input files, policy syntax, or command execution prevented a valid check. | -| `3` | `unsupported` or `inconclusive` | The model cannot soundly cover the policy shape, or the solver did not reach a determination. | -| `130` | `inconclusive` when graceful handling completes | The user interrupted the command with Ctrl-C on Unix; a second or very early interruption may prevent output. | - -Only exit `0` means the verification succeeded. Treat unsupported and -inconclusive results as failures in CI. - -## Interpretation and Limits - -The containment check covers filesystem paths, process identity settings, -Landlock compatibility requirements, L4 destination authority, and enforced -REST method and path authority, including explicit REST denies. The result -object reports the policy domains modeled by each check. Policies that use -recognized authority outside that coverage return `unsupported` rather than -silently ignoring it. - -Both inputs use the same bounded YAML/JSON parser and authored policy schema as -OpenShell. Unknown fields, duplicate keys, malformed field types, and unsupported -managed `metadata` or `review` annotations return `error` with -`reason_code: invalid_input` and exit `2`. Schema-valid controls outside the -containment model return `unsupported` and exit `3`. - -The prover applies aggregate limits across the candidate and boundary before -semantic shape validation: 1,024 network rules, 4,096 endpoints, 4,096 binary -selectors, 65,536 authored port entries, 4,096 `allowed_ips` entries, 16,384 -REST rules, 4 KiB per modeled pattern, and 1 MiB of modeled pattern text. -Exceeding any limit returns `inconclusive` with `reason_code: resource_limit`. -A cancellation already requested at preflight takes precedence over that -result; otherwise a resource limit takes precedence over unsupported -policy-shape diagnostics. This ordering keeps validation work bounded for -checked-in CI inputs. - -### Process and Landlock settings - -Matching supported `run_as_user` and `run_as_group` values do not expand the -configuration. Changing an explicit non-root identity to `root` or `0` returns -`exceeds_boundary` with the field and both values. Other identity changes return -`unsupported`; the command does not resolve accounts from a sandbox image. -These comparisons assume consistent identity resolution and execution settings. -They do not prove permission relationships between arbitrary Linux accounts. - -Landlock `hard_requirement` may not become `best_effort`. That change returns -`exceeds_boundary`. Keeping the same mode or strengthening it to -`hard_requirement` passes this part of the check. This compares the requested -requirement, not whether a target kernel successfully installed Landlock -restrictions. - -### Destination IP restrictions - -The network action includes an IPv4 or IPv6 destination address. CIDR entries -in `allowed_ips` are modeled together with host, port, executable, and REST -restrictions; network counterexamples include `destination_ip`. An empty IP -list follows the runtime's destination rules and is not a universal allowlist. -The check does not resolve DNS on the host running the CLI. - -Policies whose potentially overlapping endpoints select different destination -restrictions remain unsupported because the runtime uses the selected -endpoint's address filter. The prover conservatively treats wildcard host -selectors on a shared port as potentially overlapping, including when their -literal suffixes differ. CIDR unions within a supported endpoint are checked by -the solver. Unsupported IP-literal wildcard selectors and ranges rejected by -the runtime also cannot produce a successful proof. - -Consumers must inspect `coverage.domains` and require every domain relevant to -their authorization decision. A successful result applies only to those -reported domains and the documented assumptions. - -### Remaining limits - -Network binary selectors, endpoint host and path selectors, and REST allow and -deny method and path selectors must use ASCII literals in both the candidate and -boundary. A non-ASCII literal in one of these fields returns `unsupported` with -`reason_code: unsupported_policy_shape`, including when it appears only in a -deny. Embedded NUL bytes in these fields are also unsupported. This is a -prover-model limitation, not a general policy validation rule: filesystem paths -and unrelated policy text retain their existing Unicode behavior. ASCII -wildcard selectors still cover non-ASCII runtime values matched by the policy -engine. - -Containment means `Allowed(candidate)` is a subset of `Allowed(boundary)` under -the reported model. It does not establish least privilege, automatic approval -eligibility, semantic safety, or equivalence to a running sandbox's kernel -state. In particular: - -- The command does not fetch the current sandbox policy or compose provider - rules. The caller must supply the effective candidate. -- The command does not apply, approve, or persist a policy. -- Environment-dependent authority, such as an unresolved image workdir, - returns `unsupported` when the result depends on that missing context. -- Binary comparisons that can change when an image resolves an exact selector - through a symlink return `unsupported`. This covers exact candidate grants - under boundary globs and exact boundary denies replaced by candidate globs. - Use matching exact canonical paths when possible. -- Policies with overlapping L4 and enforced REST endpoints return - `unsupported` because inspection selection depends on the complete set of - matching runtime endpoint configurations. -- Network containment checks both supported runtime configurations: binary - identity enforcement enabled and disabled. When enabled, grants and denies - match the executable or an ancestor identity. Network counterexamples report - the configuration and identities that expose the additional authority. -- REST containment witnesses use canonical request methods and paths. -- If a solver string cannot be decoded and validated faithfully, the command - returns `inconclusive` with `reason_code: invalid_witness` instead of emitting - a counterexample. -- Both files are interpreted in the same sandbox filesystem namespace and - mount model, with stable path resolution when enforcement rules are created. - The CLI does not resolve sandbox paths against the host. -- Filesystem containment supports removing grants and reducing write grants to - read-only grants at matching paths. A boundary grant for `/` also covers other - paths for the same access. Comparisons between different paths otherwise - return `unsupported` with `reason_code: unresolved_filesystem_path`: a lexical - child can resolve outside its parent through a symlink, and unrelated paths - can resolve to the same object. This includes narrowing `/tmp` to `/tmp/cache`. - If the boundary grants no access of the requested kind, adding that access - returns `exceeds_boundary`. - -Use the [Policy Schema Reference](/how-it-works/policies/schema) for the full policy -language. A successful prover result covers only the policy domains reported in -its evidence. +For agents and scripts, add `--output json`. Read the `result` and +`reason_code` fields instead of parsing the text output. `reason_code` is a +stable identifier, such as `unsupported_policy_shape` or `solver_timeout`. Also +check that `coverage.domains` includes every part of the policy that you rely +on. Run `openshell-prover check --help` for all options, including `--timeout`, +which changes the default 10-second time limit. + +## What the Boundary Check Covers + +A passing result means that the candidate allows nothing beyond the boundary in +the parts of the policy that the prover checks. It does not mean that the policy +is as narrow as it could be, that it is safe for a particular task, or that a +running sandbox enforces it. + +The check currently covers five parts of a policy. Within each part, some +comparisons depend on information that the prover does not have, such as the +files in the sandbox image. For those comparisons, the prover returns +`unsupported` instead of guessing. Very large policies, for example with more +than 1,024 network rules or 4,096 endpoints across both files, return +`inconclusive` with the `reason_code` `resource_limit`. + +### Filesystem Access + +The prover compares read and write access path by path, so both policies must +list the same paths. The candidate can remove a path that the boundary lists or +change it from read-write to read-only, and a boundary entry for `/` covers +every path. If the boundary grants no access of a kind, such as no write access +at all, any candidate path with that access exceeds the boundary. + +Comparing different paths returns `unsupported`, even when one path is inside +the other. For example, a candidate that allows writing to `/tmp/cache` under a +boundary that allows writing to `/tmp` returns `unsupported`, because a symlink +in the sandbox image could make `/tmp/cache` point outside `/tmp`. A policy +whose result depends on the sandbox's working directory also returns +`unsupported`. + +### Process Identity + +The prover compares `run_as_user` and `run_as_group`. Matching values pass, and +a change from a non-root identity to root exceeds the boundary. Any other +change, such as from UID `1500` to `1600`, returns `unsupported`, because the +prover cannot look up accounts in the sandbox image. + +### Landlock + +The prover compares the `compatibility` setting. Keeping the setting or changing +it to `hard_requirement` passes, and changing `hard_requirement` to +`best_effort` exceeds the boundary. The prover compares the settings, not what +a kernel enforces. + +### Network Connections + +The prover compares which binaries can reach which hosts, ports, and destination +addresses, including `allowed_ips` ranges. It checks each policy both with and +without binary identity enforcement, because a sandbox runtime can turn +enforcement off. It does not resolve hostnames. + +The prover returns `unsupported` in these cases: + +- The candidate lists an exact binary path that the boundary covers only with a + glob, such as `/usr/bin/curl` under `/usr/bin/*`. A symlink in the sandbox + image could make the exact path refer to an executable outside the glob. Use + the same exact paths in both policies when you can. +- Endpoints on the same port set different `allowed_ips`, and their hosts are + the same or one of them is a wildcard. The prover treats a wildcard host as + overlapping every host on its port. +- An exact host and a wildcard host share a port and neither sets + `allowed_ips`, such as `github.com` and `*.githubusercontent.com` on port 443. +- An endpoint omits `host`, sets both `port` and `ports`, or uses an IPv6 + address as its host. +- A host, path, method, or binary contains non-ASCII characters. + +### REST Requests + +The prover compares method and path allow and deny rules on endpoints with +`protocol: rest`. These endpoints must use `enforcement: enforce`. An endpoint +in audit mode returns `unsupported`, because it does not block requests. A host +and port that has both a REST endpoint and an endpoint without request rules +also returns `unsupported`, as do REST rules that match query parameters or use +`?` or bracket expressions in paths. Other request protocols, such as WebSocket, +GraphQL, MCP, and JSON-RPC, return `unsupported`. diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 929afdb7e8..505a0a7838 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -2,17 +2,17 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Policy Schema Reference" -sidebar-title: "Schema" -description: "Complete field reference for the sandbox policy YAML including static and dynamic sections." +sidebar-title: "Schema Reference" +description: "Fields, defaults, and constraints for the sandbox policy schema." keywords: "Generative AI, Cybersecurity, Policy, Schema, YAML, Reference, Security" -position: 3 +position: 7 --- -Complete field reference for the sandbox policy YAML. Each field is documented with its type, whether it is required, and whether it is static (locked at sandbox creation) or dynamic (hot-reloadable on a running sandbox). +This reference defines the sandbox policy schema. A policy is a YAML file of at +most 4 MiB. OpenShell rejects a policy that contains unknown fields or duplicate +keys. -## Top-Level Structure - -A policy YAML file contains the following top-level fields: +## Top-Level Fields ```yaml showLineNumbers={false} version: 1 @@ -23,162 +23,75 @@ network_policies: { ... } network_middlewares: { ... } ``` -| Field | Type | Required | Category | Description | +| Field | Type | Required | Takes effect | Description | |---|---|---|---|---| -| `version` | integer | Yes | -- | Policy schema version. Must be `1`. | -| `filesystem_policy` | object | No | Static | Controls which directories the agent can read and write. | -| `landlock` | object | No | Static | Configures Landlock LSM enforcement behavior. | -| `process` | object | No | Static | Sets the user and group the agent process runs as. | -| `network_policies` | map | No | Dynamic | Declares which binaries can reach which network endpoints. | -| `network_middlewares` | map | No | Dynamic | Attaches ordered middleware by destination host; each implementation's manifest selects its supported HTTP and WebSocket operations. | - -The YAML root and each present top-level section listed as an object or map must be mappings. Each named network policy or middleware entry must also be a mapping. The OPA loader rejects the first malformed container before normalization or access preset expansion, with a structural error that excludes authored keys and values. - -Static fields are set at sandbox creation time. Changing them requires destroying and recreating the sandbox. Dynamic fields can be updated on a running sandbox with `openshell policy update` for incremental merges or `openshell policy set` for full replacement, and take effect without restarting. - -For query parameters and MCP tool names, the OPA loader normalizes nonempty string matchers to explicit `glob` objects after validation. For example, `query: { status: "active*" }` becomes `query: { status: { glob: "active*" } }` in runtime endpoint data. The same normalization applies to allow and deny rules and to MCP `tool` aliases. Matchers supported by both YAML and protobuf use the same runtime representation; `any` matchers retain their listed values. Raw OPA data also accepts empty scalar query matchers, which match empty query values and remain unchanged during loading. An empty protobuf glob cannot represent that OPA-only matcher. - -## Parsing and representation - -OpenShell uses one canonical authored-policy representation for YAML and JSON. -Runtime policy loading and the policy prover both decode through the same -bounded parser before projecting the document into their runtime-specific -models. The parser requires `version: 1`, rejects duplicate mapping keys and -YAML merge keys, and accepts one document of at most 4 MiB. It limits nesting -to 64 levels, total AST nodes to 100,000, parser events to 300,000, cumulative -scalar data to 4 MiB, alias expansions to 100 with a 5:1 alias-to-anchor ratio, -and each mapping or sequence to 10,000 entries. - -Unknown keys in closed schema objects are rejected with their field path before -the policy is converted or analyzed. The following maps are intentionally -open user namespaces, so their keys are preserved as data: middleware `config`, -query matcher names, GraphQL persisted-query names, and recursively nested MCP -`params` names. - -An absent `filesystem_policy` resolves to the runtime-effective -`include_workdir: true`. An explicitly present `filesystem_policy: {}` keeps -`include_workdir: false`. During protobuf conversion, `ports` takes precedence -over scalar `port`, one effective port serializes in compact scalar form, empty -rule names fall back to their map key, and runtime-only provenance fields are -omitted. Because proto3 scalar fields do not preserve presence, protobuf `version: 0` means omission. Canonical serialization materializes -`version: 1`; any other unsupported protobuf version is rejected. A protobuf port above -65535 is rejected rather than clamped. - -## Policy Load Diagnostics +| `version` | integer | Yes | -- | Schema version. Must be `1`. | +| `filesystem_policy` | object | No | Startup | Paths the workload can read and write. | +| `landlock` | object | No | Startup | Landlock enforcement behavior. | +| `process` | object | No | Startup | User and group that the workload runs as. | +| `network_policies` | map | No | Live | Which binaries can reach which network endpoints. | +| `network_middlewares` | map | No | Live | Middleware applied to allowed traffic. | -When the supervisor's OPA engine rejects a policy load or reload, its error message identifies the validation category without copying policy or middleware names, hostnames, paths, values, or source text. The message also omits the original error chain. This applies to the engine's file, string, and protobuf loaders. - -Each message contains at most eight error items and 512 UTF-8 bytes, including the heading, separators, and the `additional violations omitted` marker when more items cannot fit. Categories help you choose which part of the submitted policy to inspect: - -- Typed validation distinguishes process identity, filesystem paths and limits, Landlock compatibility, endpoint hosts and ports, credential signing and rewriting, MCP configuration, and middleware configuration. -- L7 validation reports `invalid L7 policy configuration` for semantic errors and `invalid L7 protocol configuration` for protocol configuration or alias errors. -- Endpoint conflicts report `ambiguous network endpoint selectors`. -- YAML errors report a fixed parser category, with numeric line and column when the parser provides them. -- File I/O, Rego loading, and internal policy-data failures return fixed messages. - -A candidate rejected during validation does not replace the active engine or advance its generation. The supervisor then applies the gateway's `policy_validation_failure_mode`; `fail_closed` can publish a quarantine generation that denies egress, while `retain_last_valid` keeps an existing valid generation active. See [Gateway Configuration](/how-it-works/gateways/configuration) for that setting and startup behavior. - -These bounds apply to returned OPA load errors. Warnings for accepted policies, runtime request diagnostics, and gateway-authored policy parser messages have separate reporting behavior. - -## Version - -The version field identifies which schema the policy uses: - -| Field | Type | Required | Description | -|---|---|---|---| -| `version` | integer | Yes | Schema version number. Currently must be `1`. | +Startup fields take effect when a sandbox starts. Live fields can change while +it runs. Refer to [How Changes Take +Effect](/how-it-works/policies/manage-policies#how-changes-take-effect). ## Filesystem Policy -**Category:** Static - -Controls filesystem access inside the sandbox. Paths not listed in either `read_only` or `read_write` are inaccessible. - -| Field | Type | Required | Description | +| Field | Type | Default | Description | |---|---|---|---| -| `include_workdir` | bool | No | When `true`, automatically adds the agent's working directory to `read_write`. | -| `read_only` | list of strings | No | Paths the agent can read but not modify. Typically system directories like `/usr`, `/lib`, `/etc`. | -| `read_write` | list of strings | No | Paths the agent can read and write. Typically `/tmp`; set `include_workdir: true` to add the driver-resolved working directory. | - -**Validation constraints:** - -- Every path must be absolute (start with `/`). -- Paths must not contain `..` traversal components. The server normalizes paths before storage, but rejects policies where traversal would escape the intended scope. -- Read-write paths must not be overly broad (for example, `/` alone is rejected). -- Each individual path must not exceed 4096 characters. -- The combined total of `read_only` and `read_write` paths must not exceed 256. +| `include_workdir` | bool | See below | Adds the sandbox's working directory to `read_write`. | +| `read_only` | list of strings | `[]` | Paths the workload can read. | +| `read_write` | list of strings | `[]` | Paths the workload can read and write. | -Policies that violate these constraints are rejected with `INVALID_ARGUMENT` at creation or update time. Disk-loaded YAML policies that fail validation fall back to a restrictive default. +When `filesystem_policy` is omitted, `include_workdir` is `true`. When +`filesystem_policy` is present, `include_workdir` defaults to `false`. Paths +that are not listed are inaccessible. When the effective policy has at least one +network rule, OpenShell also adds the baseline paths described in [Default +Policy](/how-it-works/policies/default-policy#baseline-filesystem-paths). -Example: +Each path must be absolute, must not contain `..`, and must not exceed 4096 +bytes. `read_write` cannot contain `/`. A policy can list at most 256 paths. ```yaml showLineNumbers={false} filesystem_policy: include_workdir: true - read_only: - - /usr - - /lib - - /proc - - /dev/urandom - - /etc - read_write: - - /tmp - - /dev/null + read_only: [/usr, /lib, /etc] + read_write: [/tmp] ``` ## Landlock -**Category:** Static - -Configures [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforcement at the kernel level. Landlock provides mandatory filesystem access control below what UNIX permissions allow. - -| Field | Type | Required | Values | Description | -|---|---|---|---|---| -| `compatibility` | string | No | `best_effort`, `hard_requirement` | How OpenShell handles Landlock failures. Refer to the behavior table below. | - -**Compatibility modes:** - -| Value | No paths configured | Kernel ABI unavailable | Individual path inaccessible | All paths inaccessible | -|---|---|---|---|---| -| `best_effort` | Landlock skipped (no-op). | Warns and continues without Landlock. | Skips the path, applies remaining rules. | Warns and continues without Landlock (refuses to apply an empty ruleset). | -| `hard_requirement` | Aborts sandbox startup. | Aborts sandbox startup. | Aborts sandbox startup, except in Kubernetes sidecar (current-user) mode where the inaccessible path is skipped. | Aborts sandbox startup. | - -`best_effort` (the default) is appropriate for most deployments. It handles missing paths gracefully. For example, `/app` might not exist in every container image but is included in the baseline path set for containers that do have it. Individual missing paths are skipped while the remaining filesystem rules are still enforced. - -`hard_requirement` is for environments where any gap in filesystem isolation is unacceptable. If a listed path cannot be opened for any reason (missing, permission denied, symlink loop), sandbox startup fails immediately rather than running with reduced protection. Configuring `hard_requirement` with no filesystem paths is also a startup error. - -In Kubernetes sidecar (current-user) mode the sandbox cannot distinguish an intentionally denied path from a misconfigured one, so an individual inaccessible path is skipped and the remaining rules are applied instead of aborting. The other `hard_requirement` failures (kernel ABI unavailable, no paths configured, all paths inaccessible) still abort startup. +| Field | Type | Default | Values | +|---|---|---|---| +| `compatibility` | string | `best_effort` | `best_effort` or `hard_requirement` | -When a path is skipped under `best_effort`, the sandbox logs a warning that includes the path, the specific error, and a human-readable reason (for example, "path does not exist" or "permission denied"). +Both values skip individual paths that are missing or that the workload cannot +open. They differ when none of the listed paths can be applied, or when Landlock +fails to enforce the policy's rules: -Example: +| Value | Behavior | +|---|---| +| `best_effort` | The sandbox runs without the filesystem rules and logs a high-severity finding. | +| `hard_requirement` | The sandbox fails to start. | -```yaml showLineNumbers={false} -landlock: - compatibility: best_effort -``` +In either mode, a sandbox requires a kernel with Landlock ABI v3 or later. The +sandbox log's `Landlock ruleset built` event reports how many paths were applied +and skipped. ## Process -**Category:** Static - -Sets the OS-level identity for the agent process inside the sandbox. - -| Field | Type | Required | Description | +| Field | Type | Default | Description | |---|---|---|---| -| `run_as_user` | string | No | Overrides the user name or UID selected by the compute driver. Docker and Podman fall back to the image's OCI `USER`. | -| `run_as_group` | string | No | Overrides the group name or GID selected by the compute driver. Docker and Podman fall back to the image's OCI `USER`. | - -**Validation constraint:** An explicit policy value must be `sandbox` or a -numeric UID/GID from `1` through `4294967294`. OpenShell rejects `0` as root -and `4294967295` as the invalid identity sentinel. Docker and Podman may select -other named identities through OCI `USER` fallback. - -Omission is preserved independently for each field. For example, setting only -`run_as_user` keeps that explicit user while allowing the active driver to -select the group. +| `run_as_user` | string | Driver default | `sandbox` or a numeric UID for the workload. | +| `run_as_group` | string | Driver default | `sandbox` or a numeric GID for the workload. | -Example: +A numeric ID must be from `1` through `4294967294`, so OpenShell rejects root. +Each field is independent, so you can set one and let the compute driver choose +the other. Only Docker and Podman apply these fields, and only from the policy +that you pass when you create the sandbox. Without them, Docker and Podman use +the image's `USER`. Kubernetes and VM sandboxes run as the identity configured +for their driver. ```yaml showLineNumbers={false} process: @@ -188,105 +101,80 @@ process: ## Network Policies -**Category:** Dynamic - -A map of named network policy entries. Each entry declares a set of endpoints and a set of binaries. Only the listed binaries are permitted to connect to the listed endpoints. The map key is a logical identifier. The `name` field inside the entry is the display name used in logs. - -### Network Policy Entry - -Each entry in the `network_policies` map has the following fields: +A map of named rules. The key is the rule's name. Each rule allows every listed +binary to reach every listed endpoint. For how OpenShell evaluates rules, and +for examples, refer to [Network Rules](/how-it-works/policies/network-rules). | Field | Type | Required | Description | |---|---|---|---| -| `name` | string | No | Display name for the policy entry. Used in log output. Defaults to the map key. | -| `endpoints` | list of endpoint objects | Yes | Hosts and ports this entry permits. | -| `binaries` | list of binary objects | Yes | Executables allowed to connect to these endpoints. | +| `name` | string | No | Display name in logs. Defaults to the key. | +| `endpoints` | list of endpoint objects | No | Destinations the rule allows. | +| `binaries` | list of binary objects | No | Executables the rule applies to. An empty list matches no binary. | + +Rule keys cannot start with `_provider_`, which OpenShell reserves for rules +that providers contribute. ### Endpoint Object -Each endpoint defines a reachable destination and optional inspection rules. +Endpoint fields fall into four groups. + +#### Destination Fields | Field | Type | Required | Description | |---|---|---|---| -| `host` | string | Conditional | Hostname or IP address. Required for `protocol: tcp`; transparent TCP requires a valid DNS hostname and rejects literal IPs. A non-TCP proxy endpoint may omit `host` only when `allowed_ips` supplies the destination constraint. Supports a `*` wildcard inside the first DNS label only: `*.example.com`, `**.example.com`, and intra-label patterns like `*-aiplatform.googleapis.com` are accepted; bare `*`/`**`, TLD wildcards (`*.com`), and wildcards outside the first label are rejected at load time. Prefer exact hosts for `protocol: tcp`: a wildcard authorizes DNS queries for all matching names and can provide a DNS-label exfiltration channel. | -| `port` | integer | Yes | TCP port number. | -| `path` | string | No | Optional HTTP path glob used to select between L7 endpoints that share the same host and port. Empty means all paths. Use this when REST and GraphQL live under the same host, such as `/repos/**` and `/graphql`. | -| `protocol` | string | No | Set to `tcp` with a valid DNS hostname to allow native TCP clients through policy DNS and transparent capture without payload inspection. Omit the field for L4 passthrough through an explicit proxy, including legacy hostless `allowed_ips` endpoints. Set to `rest` for HTTP method/path inspection, `websocket` for RFC 6455 upgrade and client text-message inspection, `graphql` for GraphQL-over-HTTP operation inspection, `mcp` for MCP Streamable HTTP request inspection, or `json-rpc` for generic JSON-RPC-over-HTTP method inspection. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket traffic. Provider-credentialed endpoints require an inspected protocol unless `allow_uninspected_credentials` is explicitly set. | -| `tls` | string | No | TLS handling mode. The proxy auto-detects TLS by peeking the first bytes of each connection and terminates it for inspected HTTPS traffic, so this field is optional in most cases. Set to `skip` to disable auto-detection for edge cases such as client-certificate mTLS or non-standard protocols. Provider-credentialed endpoints reject `tls: skip` unless `allow_uninspected_credentials` is explicitly set. `skip` is the only accepted non-empty value; every other value, including the removed `terminate` and `passthrough` spellings, is rejected before persistence or activation. | -| `enforcement` | string | No | `enforce` actively blocks disallowed requests. `audit` logs violations but allows traffic through. Other values are rejected before activation rather than interpreted as audit mode. | -| `access` | string | No | Access preset. One of `read-only`, `read-write`, or `full`; other values are rejected before activation. Mutually exclusive with `rules`. Not valid on `protocol: mcp` or `protocol: json-rpc`; MCP uses explicit rules unless `mcp.allow_all_known_mcp_methods: true` enables the endpoint method profile, and JSON-RPC always uses explicit rules. | -| `rules` | list of allow rule objects | No | Fine-grained protocol-specific allow rules. Mutually exclusive with `access`. | -| `deny_rules` | list of deny rule objects | No | L7 deny rules that block specific requests even when allowed by `access` or `rules`. Deny rules take precedence over allow rules. | -| `allowed_ips` | list of string | No | CIDR or IP allowlist for SSRF override. Exact user-declared hostname endpoints may resolve to RFC 1918 private addresses without this field, but wildcard, hostless, and policy-advisor-proposed endpoints still require `allowed_ips` for private resolved IPs. A hostless allowlist is valid only for the legacy proxy path and cannot be combined with `protocol: tcp`. Entries overlapping loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), or unspecified (`0.0.0.0`) are rejected at load time. | -| `allow_encoded_slash` | bool | No | When `true`, L7 request parsing preserves `%2F` inside path segments instead of rejecting it. Use this for registries and APIs such as npm scoped packages (`/@scope%2Fname`). Defaults to `false`. | -| `websocket_credential_rewrite` | bool | No | When `true` on a `protocol: rest` or `protocol: websocket` endpoint, OpenShell rewrites credential placeholders in client-to-server WebSocket text messages after an allowed HTTP `101` upgrade. On provider-credentialed endpoints without `allow_uninspected_credentials`, OpenShell uses the parsed relay and rejects binary frames; text frames containing placeholders fail closed when rewrite is disabled. Defaults to `false`. | -| `request_body_credential_rewrite` | bool | No | When `true` on a `protocol: rest` endpoint, OpenShell rewrites credential placeholders in UTF-8 `application/json`, `application/x-www-form-urlencoded`, and `text/*` request bodies before forwarding upstream. The proxy buffers at most 256 KiB and updates `Content-Length` after rewriting. For chunked requests, the limit counts framing, extensions, and trailers. When rewrite is disabled and the sandbox has provider credentials, bodies continue to stream. Authoritatively unknown placeholder keys and valid issued credentials pass unchanged, including credentials bound to the destination. Invalid or unavailable credentials, and unavailable classification metadata fail closed with `credential_placeholder_in_request_body` (HTTP 403). Candidates are limited to 4096 wire bytes. No secret is substituted. Defaults to `false`. Mutually exclusive with `credential_signing`. | -| `allow_uninspected_credentials` | bool | No | Explicit security-sensitive opt-in that permits a provider-credentialed endpoint to use traffic paths OpenShell cannot inspect or rewrite, including L4-only and `tls: skip` tunnels. Defaults to `false`. Policy proposals that set it require explicit security-flagged approval. | -| `credential_signing` | string | No | Proxy-side credential signing mode. When set, the proxy strips the sandbox client's `Authorization` header and re-signs with real provider credentials. Values: `sigv4` (auto-detect payload mode from client headers), `sigv4:body` (buffer and hash body, max 10 MiB), `sigv4:no_body` (unsigned payload, stream body). Mutually exclusive with `request_body_credential_rewrite`. See [AWS SigV4](/how-it-works/providers/aws). | -| `signing_service` | string | No | AWS service name for SigV4 signing (e.g. `bedrock`, `s3`, `sts`). Required when `credential_signing` is set. | -| `signing_region` | string | No | AWS region override for SigV4 signing (e.g. `us-east-1`). When omitted, the region is extracted from the endpoint hostname. Required for non-standard AWS endpoints where the region cannot be inferred. | -| `credential_binding` | object | No | Binds static credentials from an attached provider to this endpoint when that provider's profile defines no endpoints. This field is valid only in a sandbox-scoped policy. | -| `credential_binding.provider` | string | Yes with `credential_binding` | Exact name of the provider instance attached to the sandbox. The referenced provider must have a profile, and that profile must define no endpoints. | -| `persisted_queries` | string | No | GraphQL hash-only behavior for `protocol: graphql` and GraphQL-over-WebSocket operation policy. Default is `deny`; use `allow_registered` only with `graphql_persisted_queries`. | -| `graphql_persisted_queries` | map | No | Trusted GraphQL persisted-query registry keyed by hash or saved-query ID. Values contain `operation_type`, optional `operation_name`, and optional root `fields`. | -| `graphql_max_body_bytes` | integer | No | Maximum GraphQL-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. | -| `mcp` | object | No | MCP endpoint options for `protocol: mcp`. Omit this key to use all MCP endpoint defaults, including the exact `2025-11-25` revision; `mcp: null` is invalid. The object is rejected on other protocols. Every MCP endpoint must still set a concrete `host` and `port` or `ports`; an entry containing only `protocol: mcp` is invalid and is not treated as a wildcard endpoint. | -| `mcp.versions` | list of string | No | Nonempty allowlist of exact supported MCP core revisions: `2025-03-26`, `2025-06-18`, and `2025-11-25`. Omission resolves to the exact allowlist `["2025-11-25"]`; it never means latest or all known revisions. The key must be absent to use this default; `versions: null` and `versions: []` are invalid. Values must be unique, contain no extra whitespace, and name a revision in the closed supported set. For every request except a valid standalone `initialize`, OpenShell selects the revision from one `MCP-Protocol-Version` header or, when the header is absent, the MCP specification's `2025-03-26` compatibility fallback. The selected revision must appear in this allowlist. Duplicate, empty, or unsupported header values receive `400 Bad Request`; a supported revision outside the allowlist receives `403 Forbidden`. The sessionless `2026-07-28` revision is not yet supported. For an unsupported revision, omit `protocol` and `mcp` only when deliberate uninspected L4 passthrough is an acceptable weaker boundary. | -| `mcp.max_body_bytes` | integer | No | Maximum MCP JSON-RPC-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. | -| `mcp.strict_tool_names` | bool | No | Defaults to `true`. Requires `tools/call` `params.name` values to match `^[A-Za-z0-9_.-]{1,128}$` before policy evaluation. Set to `false` only for compatibility with MCP servers that intentionally use non-recommended tool names. Wildcard `tool` matchers require this to remain enabled. | -| `mcp.allow_all_known_mcp_methods` | bool | No | Defaults to `false`. When `true`, enables the endpoint MCP method profile: omitted `rules` allow all MCP-family methods and all tools before `deny_rules`, and omitted rule `method` uses that profile. When unset or `false`, explicit MCP method rules are required; rules with `tool` or `params.name` must set `method: tools/call`. | -| `json_rpc` | object | No | JSON-RPC endpoint options. For `protocol: json-rpc`, `json_rpc.max_body_bytes` sets the maximum JSON-RPC-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. | - -The YAML representation keeps the names shown above. Protobuf and generated SDK -clients use `NetworkTlsMode`, `NetworkEnforcementMode`, and -`NetworkAccessPreset` enums for these fields. This is a breaking source and wire -contract change for clients generated from the earlier string fields. -Regenerate bindings from the current protobuf definitions and replace string -assignments with the corresponding enum values. Unspecified TLS keeps automatic -handling, unspecified enforcement keeps the audit default, and unspecified -access selects no preset. Unknown enum numbers are rejected before activation. - -**Validation constraints:** - -- When present, `endpoints`, `binaries`, `rules`, and `deny_rules` must be lists of objects, even when they contain only one entry. `null` cannot replace a list or an object entry. -- `access` and `rules` are mutually exclusive; setting both is rejected. -- `protocol: tcp` requires a valid DNS hostname. Hostless `allowed_ips`, IP-literal hosts, trailing-dot names, and malformed DNS selectors are rejected with a policy-validation error. -- `protocol: tcp` requires at least one port and rejects L7-only fields, including `path`, `enforcement`, `access`, `rules`, `deny_rules`, request rewriting and credential signing fields, and GraphQL, JSON-RPC, or MCP options. -- A `protocol: tcp` hostname constrains connection routing, not application authority. OpenShell does not inspect TLS SNI, HTTP `Host`, or another protocol-level destination in the stream. Compatible shared infrastructure can therefore expose other tenants, virtual hosts, or services behind an allowed hostname. -- A sandbox runtime must support policy DNS and transparent TCP capture before it can activate a policy containing `protocol: tcp`. Docker and Podman provide this runtime support. -- Adding the first `protocol: tcp` endpoint to a running sandbox that started without one is rejected atomically because its DNS and capture substrate is startup infrastructure. Recreate the sandbox with a TCP endpoint. A sandbox that started with the substrate can remove and re-add TCP endpoints dynamically. -- Policy-advisor agent proposals cannot request `protocol: tcp` or `tls: skip`. Add native TCP or raw TLS access through an administrator-authored policy. Agent proposals may omit `protocol` to use the explicit proxy with its default TLS termination and HTTP authority checks. -- When `protocol` is set, at least one of `access` or `rules` is required for `rest`, `websocket`, `graphql`, and `sql`. -- `mcp` and `json-rpc` reject `access` presets; use explicit `rules`. -- `json-rpc` requires explicit `rules` with `allow.method`. -- `mcp` requires `rules` unless `mcp.allow_all_known_mcp_methods: true`. -- YAML defaulting requires the `mcp` or `mcp.versions` key to be absent. Explicit `mcp: null`, `versions: null`, and `versions: []` values are rejected. At protobuf ingress, an empty repeated `versions` field means omission and resolves to `["2025-11-25"]` because protobuf repeated fields do not preserve field presence. -- `deny_rules` require `protocol`. For non-MCP protocols, `deny_rules` also require `rules` or `access` to define the base allow set. MCP `deny_rules` may omit both when `mcp.allow_all_known_mcp_methods: true` supplies the base allow set. -- `rules: []` (empty list) is rejected; use `access: full` or remove `rules`. -- Non-empty `rules` must contain at least one effective allow clause; rules where every entry lacks an allow are rejected as deny-all. -- `deny_rules: []` (empty list) is rejected; remove it if no denials are needed. -- `credential_signing` requires a resolvable AWS credential source before a sandbox policy can activate. Use an attached endpoint-bearing profile that declares `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` and covers the signed endpoint, or bind an attached endpointless profile that declares those keys with `credential_binding.provider`. - -For proxied HTTP responses, OpenShell completes delivery and signals EOF when -the server sends `Connection: close` or an HTTP/1.0 response without keep-alive. -This also applies to HTTPS and responses processed by middleware. A -`Content-Length` or chunked body does not override the server's close decision. - -Credential rewrite recognizes the canonical `openshell:resolve:env:KEY` placeholder form and whole-token provider-shaped aliases such as `provider-OPENSHELL-RESOLVE-ENV-API_TOKEN` when the referenced environment key exists in the configured provider credentials. - -Static provider placeholders also require the request host, port, and path to -match their credential binding. Profile endpoints supply this boundary by -default. An endpointless profile can instead use a sandbox policy endpoint with -`credential_binding.provider` set to the exact attached provider name. OpenShell -rejects unattached providers, profileless providers, endpointful profiles, and -global policies that use this field. Network policy admission does not expand -the credential boundary unless the endpoint explicitly supplies this binding. -OpenShell rejects a request mismatch with HTTP 403 and -`credential_endpoint_mismatch`. Refer to [Static Credential Endpoint -Binding](/how-it-works/providers/profiles#understand-static-credential-endpoint-binding). +| `host` | string | Conditional | Hostname, IP address, or wildcard pattern, such as `*.example.com`. | +| `port` | integer | Conditional | TCP port. Set `port` or `ports`. | +| `ports` | list of integers | Conditional | TCP ports. Use instead of `port`, not with it. | +| `path` | string | No | Path glob that selects among inspected endpoints on the same host and port. The most specific match wins. | +| `allowed_ips` | list of strings | No | IP addresses or CIDR ranges that resolved addresses must fall within. | + +A wildcard host must have at least three DNS labels. `*` can appear within the +first label, such as `*-api.example.com`, or as a whole later label, such as +`api.*.example.com`. `**` is allowed only as the whole first label. + +An endpoint can omit `host` only when it sets `allowed_ips`. Exact hostnames can +reach the private addresses they resolve to, unless the endpoint comes from an +approved policy advisor proposal. Wildcard and hostless endpoints can reach +private addresses only through `allowed_ips`. + +Loopback, link-local, and unspecified addresses, including the cloud metadata +address `169.254.169.254`, are always blocked. `openshell policy update` rejects +an `allowed_ips` entry that overlaps them, and in a complete policy such an +entry blocks every connection to the endpoint. OpenShell also blocks the +Kubernetes and etcd control-plane ports 2379, 2380, 6443, 10250, and 10255 on +endpoints that use an exact hostname, an IP address, or `allowed_ips`. A rule +for `host.openshell.internal` can still reach services on the gateway host. + +#### Inspection Fields + +| Field | Type | Default | Description | +|---|---|---|---| +| `protocol` | string | None | `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for request inspection, or `tcp` for a native TCP connection. Refer to [Connection and Request Checks](/how-it-works/policies/network-rules#connection-and-request-checks). | +| `tls` | string | Automatic | `skip` relays traffic without terminating TLS, so OpenShell cannot inspect it. Do not use it with a request protocol. | +| `enforcement` | string | `audit` | `enforce` blocks requests that break the endpoint's rules. `audit` logs them and allows the request. | +| `access` | string | None | Access preset: `read-only`, `read-write`, or `full`. Refer to [Access Presets](#access-presets). | +| `rules` | list | None | Allow rules. | +| `deny_rules` | list | None | Deny rules, which take precedence over allow rules. | +| `allow_encoded_slash` | bool | `false` | Accepts `%2F` in request paths, as used by npm scoped packages. | + +#### Credential Fields -This example allows the sandbox to reach Google Cloud Storage and binds the -static credentials from the attached `work-gcp` provider to that endpoint: +| Field | Type | Default | Description | +|---|---|---|---| +| `credential_binding.provider` | string | None | Binds the static credentials of an attached provider whose profile defines no endpoints. Valid only in a sandbox policy. | +| `request_body_credential_rewrite` | bool | `false` | Replaces credential placeholders in REST request bodies. | +| `websocket_credential_rewrite` | bool | `false` | Replaces credential placeholders in client WebSocket text messages. | +| `allow_uninspected_credentials` | bool | `false` | Allows a provider-credentialed endpoint to omit request inspection or use `tls: skip`. | +| `credential_signing` | string | None | AWS request signing: `sigv4`, `sigv4:body`, or `sigv4:no_body`. | +| `signing_service` | string | None | AWS service name, such as `bedrock` or `s3`. Required with `credential_signing`. | +| `signing_region` | string | From hostname | AWS region override, such as `us-east-1`. | + +Credential placeholders use the form `openshell:resolve:env:KEY`. Body rewriting +applies to UTF-8 JSON, form, and text bodies of up to 256 KiB, and cannot be +combined with `credential_signing`. Signing requires an attached provider with +AWS credentials. Refer to [AWS SigV4](/how-it-works/providers/aws) and [Static +Credential Endpoint +Binding](/how-it-works/providers/profiles#understand-static-credential-endpoint-binding). ```yaml showLineNumbers={false} network_policies: @@ -298,320 +186,254 @@ network_policies: access: full credential_binding: provider: work-gcp + binaries: + - path: /usr/bin/curl ``` -#### Access Levels - -The `access` field accepts one of the following values on REST, WebSocket, and GraphQL endpoints. MCP and JSON-RPC endpoints reject `access` because HTTP method/path presets cannot authorize JSON-RPC safely. Use explicit MCP rules, set `mcp.allow_all_known_mcp_methods: true` for the MCP method profile, or use explicit JSON-RPC rules. +#### Protocol Options -| Value | REST expansion | WebSocket expansion | GraphQL expansion | MCP / JSON-RPC expansion | -|---|---|---|---|---| -| `full` | All methods and paths. | WebSocket upgrade and all inspected client text-message paths. | All operation types. | Rejected. | -| `read-only` | `GET`, `HEAD`, `OPTIONS`. | WebSocket upgrade handshake only. | `query` operations. | Rejected. | -| `read-write` | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH`. | WebSocket upgrade handshake and client text messages. | `query` and `mutation` operations. | Rejected. | +| Field | Type | Default | Description | +|---|---|---|---| +| `persisted_queries` | string | `deny` | GraphQL hash-only queries: `deny` or `allow_registered`. | +| `graphql_persisted_queries` | map | None | Trusted persisted-query registry, keyed by hash or saved-query ID. With `allow_registered`, OpenShell denies hash-only queries that are not in the registry. | +| `graphql_max_body_bytes` | integer | `65536` | Maximum GraphQL request body size for inspection. | +| `mcp.versions` | list of strings | `["2025-11-25"]` | Allowed MCP revisions. Refer to [MCP Version Selection](#mcp-version-selection). | +| `mcp.max_body_bytes` | integer | `65536` | Maximum MCP request body size for inspection. | +| `mcp.strict_tool_names` | bool | `true` | Requires tool names to match `^[A-Za-z0-9_.-]{1,128}$`. | +| `mcp.allow_all_known_mcp_methods` | bool | `false` | When `true`, the endpoint allows every MCP method except those that deny rules match. If rules name specific tools, `tools/call` is limited to those tools. Rules can omit `method`. Refer to [MCP Rules](#mcp-rules). | +| `json_rpc.max_body_bytes` | integer | `65536` | Maximum JSON-RPC request body size for inspection. | + +#### Endpoint Constraints + +OpenShell rejects an endpoint that breaks these rules: + +- `access` and `rules` cannot be combined. `rest`, `websocket`, and `graphql` + endpoints need one of them. `mcp` and `json-rpc` endpoints need `rules`, + unless an MCP endpoint sets `mcp.allow_all_known_mcp_methods: true`. +- `deny_rules` require `protocol`, and require `rules` or `access` on endpoints + other than MCP. +- `protocol: tcp` requires a hostname and a port. It accepts no request fields, + such as `path`, `enforcement`, `access`, `rules`, credential rewriting or + signing, or protocol options. +- Endpoints whose hosts can match the same name on the same port, including + through wildcards, must use the same `tls` and `allowed_ips` values. + Inspected endpoints that can match the same request must also agree on + `protocol`, `enforcement`, and credential settings. + +Without `protocol`, `access` and `rules` have no effect. + +### Access Presets + +REST, WebSocket, and GraphQL endpoints accept these presets. MCP and JSON-RPC +endpoints do not. + +| Value | REST | WebSocket | GraphQL | +|---|---|---|---| +| `full` | All methods and paths. | Upgrade and all client text messages. | All operations. | +| `read-only` | `GET`, `HEAD`, `OPTIONS`. | Upgrade only. | `query` operations. | +| `read-write` | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH`. | Upgrade and client text messages. | `query` and `mutation` operations. | -For MCP endpoints, configure explicit `rules` with `method`, optional `tool`, and supported `params`. For generic JSON-RPC endpoints, configure explicit `rules` with `method`; JSON-RPC policy `params` matchers are not presently supported. +### Allow and Deny Rules -#### Allow Rule Objects +Each entry in `rules` wraps its matcher fields in `allow`. Each entry in +`deny_rules` lists the matcher fields directly. With `enforcement: enforce`, a +request that matches any deny rule is blocked, regardless of the allow rules or +access preset. -Used when `access` is not set. Each entry in `rules` contains an `allow` object. The tables below list the fields inside that `allow` object. +```yaml showLineNumbers={false} +rules: + - allow: + method: GET + path: /repos/** +deny_rules: + - method: GET + path: /repos/private/** +``` -##### REST Allow Rule (`protocol: rest`) +The matcher fields depend on the endpoint's `protocol`. -REST allow rules match HTTP requests by method, path, and optional query parameters. +### REST Rules | Field | Type | Required | Description | |---|---|---|---| -| `method` | string | Yes | HTTP method to allow (for example, `GET`, `POST`). `*` matches any method. | -| `path` | string | Yes | URL path glob. `*` and `**` match zero or more characters and may cross `/`; `?` matches one character; bracket classes such as `[0-9]` and `[!0]` are supported. | -| `query` | map | No | Query parameter matchers keyed by decoded param name. Matcher value can be a glob string (`tag: "foo-*"`) or an object with `any` (`tag: { any: ["foo-*", "bar-*"] }`). | +| `method` | string | Yes | HTTP method, or `*` for any method. | +| `path` | string | Yes | Path glob. Refer to [Matcher Semantics](#matcher-semantics). | +| `query` | map | No | Query parameter matchers, keyed by parameter name. Each value is a glob or `{ any: [globs] }`. | -Example REST allow rules: +In an allow rule, every value of a repeated query parameter must match. In a +deny rule, every configured parameter must be present, and one matching value +per parameter is enough. ```yaml showLineNumbers={false} rules: - allow: method: GET - path: /**/info/refs* + path: /api/v1/download query: - service: "git-*" - - allow: - method: POST - path: /**/git-upload-pack - query: - tag: - any: ["v1.*", "v2.*"] + platform: + any: ["linux-*", "darwin-*"] +deny_rules: + - method: "*" + path: "/repos/*/*/rulesets" ``` -##### WebSocket Allow Rule (`protocol: websocket`) - -WebSocket allow rules match the RFC 6455 HTTP upgrade by path and match client-to-server text messages on the same upgraded connection with the synthetic `WEBSOCKET_TEXT` method. Binary frames are relayed but are not rewritten. +### WebSocket Rules | Field | Type | Required | Description | |---|---|---|---| -| `method` | string | Yes | `GET` allows the upgrade handshake, `WEBSOCKET_TEXT` allows client text messages after upgrade, and `*` matches both inspected actions. | -| `path` | string | Yes | URL path pattern from the original upgrade request. Supports `*` and `**` glob syntax. | -| `query` | map | No | Query parameter matchers from the original upgrade request. Matcher syntax is the same as REST allow rules. | +| `method` | string | Yes | `GET` for the upgrade request, `WEBSOCKET_TEXT` for client text messages, or `*` for both. | +| `path` | string | Yes | Path glob of the upgrade request, not message content. | +| `query` | map | No | Query parameter matchers of the upgrade request. | -Example WebSocket allow rules: +OpenShell does not inspect binary frames or messages from the server. ```yaml showLineNumbers={false} rules: - allow: method: GET - path: /v1/realtime/** + path: /v1/realtime - allow: method: WEBSOCKET_TEXT - path: /v1/realtime/** + path: /v1/realtime +deny_rules: + - method: WEBSOCKET_TEXT + path: /v1/admin/** ``` -##### GraphQL Allow Rule (`protocol: graphql` or GraphQL-over-WebSocket) - -GraphQL allow rules match parsed GraphQL operations by operation type, optional operation name, and optional root fields. On `protocol: graphql`, they apply to GraphQL-over-HTTP `GET` and `POST` requests. On `protocol: websocket`, include a separate `GET` allow rule for the RFC 6455 upgrade, then use GraphQL allow rules for client operation messages using the `graphql-transport-ws` `subscribe` message type or the legacy `graphql-ws` `start` message type. +### GraphQL Rules | Field | Type | Required | Description | |---|---|---|---| -| `operation_type` | string | Yes | GraphQL operation type: `query`, `mutation`, `subscription`, or `*`. | -| `operation_name` | string | No | GraphQL operation-name glob. Omit to match any operation name. | -| `fields` | list of string | No | GraphQL root-field globs. Every selected root field must match one configured glob. Omit to match all root fields. | +| `operation_type` | string | Yes | `query`, `mutation`, or `subscription`. To allow every operation type, use `access: full`. | +| `operation_name` | string | No | Operation name glob. | +| `fields` | list of strings | No | Top-level field globs. | -Example GraphQL allow rules: +In an allow rule, every top-level field must match. In a deny rule, one matching +field is enough, and omitting `fields` denies every matching operation. One +denied operation denies an entire batched request. ```yaml showLineNumbers={false} rules: - allow: operation_type: query - fields: [viewer, repository] - allow: operation_type: mutation - operation_name: Issue* fields: [createIssue] +deny_rules: + - operation_type: mutation + fields: [deleteRepository] ``` -Example GraphQL-over-WebSocket allow rules: +For GraphQL over WebSocket, use `protocol: websocket` with a `GET` rule for the +upgrade and GraphQL rules for operations. Do not mix WebSocket and GraphQL +matcher fields in one rule. + +### MCP Rules + +| Field | Type | Required | Description | +|---|---|---|---| +| `method` | string | Conditional | MCP method, such as `initialize` or `tools/call`. Globs are allowed only in the `tools/` family, and `*` is not allowed. Required unless `mcp.allow_all_known_mcp_methods` is `true`. | +| `tool` | string or `{ any: [globs] }` | No | Tool name matcher for `tools/call`. | +| `params.name` | string or `{ any: [globs] }` | No | Lower-level equivalent of `tool`. | + +- Rules with `tool` or `params.name` must set `method: tools/call`, unless + `mcp.allow_all_known_mcp_methods` is `true`. +- A rule that matches all of `tools/call` cannot be combined with tool-specific + allow rules. +- Wildcard `tool` matchers require `mcp.strict_tool_names: true`. +- Tool arguments are not matched, so an allowed tool accepts any arguments. +- One denied call denies an entire batched request. +- Server responses and server-to-client messages are not inspected. + +A client sends `initialize` and `notifications/initialized` before calling +tools, so allow both: ```yaml showLineNumbers={false} rules: - allow: - method: GET - path: /graphql + method: initialize - allow: - operation_type: subscription - fields: [messageAdded] + method: notifications/initialized - allow: - operation_type: query - fields: [viewer] + method: tools/call + tool: + any: [search_web, list_issues] +deny_rules: + - method: tools/call + tool: send_email ``` -Do not combine `method`, `path`, or `query` with `operation_type`, `operation_name`, or `fields` inside the same WebSocket rule. When a WebSocket endpoint has GraphQL operation policy, use GraphQL rules for client messages instead of a raw `WEBSOCKET_TEXT` allow rule. - -##### MCP Allow And Deny Rules (`protocol: mcp`) - -MCP rules match sandbox-to-server MCP Streamable HTTP request bodies by MCP method and optional tool selectors. OpenShell parses the underlying JSON-RPC 2.0 envelope, validates known MCP request and notification params, and preserves unknown extension methods as policy-addressable literal method strings. An endpoint that omits `mcp.versions`, including one that omits the entire `mcp` object, immediately resolves to the exact `2025-11-25` allowlist. Canonical serialization and stored policy data contain that explicit materialized list, so adding another supported revision cannot widen the normalized policy. For every request except a valid standalone `initialize`, OpenShell checks exactly one `MCP-Protocol-Version` value against the allowlist before policy evaluation and repeats the check after middleware changes the request. An absent header selects the `2025-03-26` compatibility fallback; it does not select the policy default. The check stores no connection or session state. The current parser remains version-independent and does not yet apply the batch rules recorded in each revision's wire profile. `mcp.allow_all_known_mcp_methods` defaults to `false`, so endpoints require explicit MCP method rules. Set it to `true` to enable the endpoint method profile; in that mode, rules can omit `method`, and tool selectors are normalized to `tools/call` internally. By default, `tools/call` `params.name` must match the MCP-recommended tool-name pattern `^[A-Za-z0-9_.-]{1,128}$`; configure `mcp.strict_tool_names: false` on the endpoint only to allow a server that intentionally uses names outside that pattern. Wildcard `tool` matchers require `mcp.strict_tool_names` to remain enabled. JSON-RPC responses and server-to-client MCP messages on response bodies or SSE streams are relayed but are not currently parsed for policy enforcement. +#### MCP Version Selection -Use `rules` for MCP allow rules and `deny_rules` for MCP deny rules. Deny rules take precedence over allow rules. If an MCP endpoint sets `mcp.allow_all_known_mcp_methods: true` and omits `rules`, OpenShell allows all MCP-family methods and all tools, then applies any `deny_rules`. Otherwise, the endpoint must define explicit rules. A broad allow or deny rule whose method matcher includes `tools/call` cannot be combined with tool-specific allow rules because it would bypass or erase the tool filter; add `tool` or `params.name` to scope `tools/call`, or remove the tool-specific rules. In a batch request, one denied call denies the full batch. - -| Field | Type | Required | Description | -|---|---|---|---| -| `method` | string | No | MCP method name, such as `initialize`, `tools/list`, `tools/call`, or an unknown extension method. Globs are accepted only for the `tools/` method family, such as `tools/*`. Required unless `mcp.allow_all_known_mcp_methods` is `true`; when that option is true, omitted method uses the endpoint method profile. Do not use `method: "*"` for MCP; omit `method` only when using the allow-all MCP method profile. | -| `tool` | string or matcher | No | Convenience matcher for `tools/call` `params.name`. Supports a glob string or `{ any: [...] }`. Requires `method: tools/call` unless `mcp.allow_all_known_mcp_methods` is `true`; validation fails otherwise. Omit to match every tool. | -| `params` | map | No | MCP currently accepts only `params.name` as a lower-level tool-name matcher. Omit this field when using only `tool`; an explicit `params: null` is invalid in allow and deny rules. Requires `method: tools/call` unless `mcp.allow_all_known_mcp_methods` is `true`; validation fails otherwise. Tool argument matching is not supported yet; allowed tools accept all argument payloads by default. | - -An MCP client first sends `initialize`. After the server returns a successful response, the client sends `notifications/initialized`. After initialization completes and the server advertises the `tools` capability, the client can call an advertised tool. The response does not need an allow rule because these rules inspect messages sent from the client to the server. This example adds both client initialization messages to the existing tool rules. It omits `tools/list` because it assumes the client already knows the tool names; add that method when the client performs discovery. - -```yaml showLineNumbers={false} -endpoints: - - host: mcp.example.com - port: 443 - path: /mcp - protocol: mcp - enforcement: enforce - mcp: - max_body_bytes: 131072 - rules: - - allow: - method: initialize - - allow: - method: notifications/initialized - - allow: - method: tools/call - tool: search_web - - allow: - method: tools/call - tool: - any: [create_issue, list_issues] - deny_rules: - - method: tools/call - tool: send_email - - method: tools/call - tool: execute_code -``` - -This example omits `mcp.versions`, so OpenShell materializes `["2025-11-25"]`. To authorize an intentional compatibility range for an older server, set an explicit nonempty allowlist: +`mcp.versions` lists the MCP revisions an endpoint accepts: `2025-03-26`, +`2025-06-18`, or `2025-11-25`. When omitted, only `2025-11-25` is allowed. ```yaml showLineNumbers={false} mcp: versions: ["2025-03-26", "2025-11-25"] ``` -OpenShell canonicalizes an explicit list in semantic order. Later runtime negotiation selects one allowed revision and applies only that profile; it does not combine the profiles. - -##### JSON-RPC Allow Rule (`protocol: json-rpc`) - -JSON-RPC allow rules match sandbox-to-server JSON-RPC-over-HTTP request objects by RPC method. They apply to single JSON-RPC requests and batch requests. For a batch, OpenShell evaluates each call independently. Client-to-server JSON-RPC response frames in POST bodies are denied. Server-to-client messages on HTTP response bodies or MCP SSE streams are relayed but are not currently parsed for policy enforcement. - -| Field | Type | Required | Description | -|---|---|---|---| -| `method` | string | Yes | Exact JSON-RPC method name such as `initialize` or `reports.search`. Use `*` only as the allow-all sentinel. Other wildcard or glob patterns are rejected for generic JSON-RPC endpoints. | - -Generic JSON-RPC policy `params` matchers are not supported. Allow rules match only the JSON-RPC method. - -Example JSON-RPC allow rules: - -```yaml showLineNumbers={false} -endpoints: - - host: jsonrpc.example.com - port: 443 - path: /rpc - protocol: json-rpc - enforcement: enforce - json_rpc: - max_body_bytes: 131072 - rules: - - allow: - method: initialize - - allow: - method: reports.list - - allow: - method: reports.search -``` - -#### Deny Rule Objects - -Blocks specific operations on endpoints that otherwise have broad access. Deny rules are evaluated after allow rules and take precedence: if a request matches any deny rule, it is blocked regardless of what the allow rules or access preset permit. - -##### REST Deny Rule (`protocol: rest`) - -REST deny rules use the same field names as REST allow rules, but they appear directly under each `deny_rules` entry instead of under an `allow` wrapper. - -| Field | Type | Required | Description | -|---|---|---|---| -| `method` | string | Yes | HTTP method to deny (for example, `POST`, `DELETE`). `*` matches any method. | -| `path` | string | Yes | URL path pattern. Same glob syntax as allow rules. Use `**` to match any path. | -| `query` | map | No | Query parameter matchers. Same syntax as allow rule `query`. | - -Example REST deny rules: - -```yaml showLineNumbers={false} -endpoints: - - host: api.github.com - port: 443 - protocol: rest - enforcement: enforce - access: read-write - deny_rules: - - method: POST - path: "/repos/*/pulls/*/reviews" - - method: PUT - path: "/repos/*/branches/*/protection" - - method: "*" - path: "/repos/*/rulesets" -``` - -##### WebSocket Deny Rule (`protocol: websocket`) - -WebSocket deny rules use the same field names as WebSocket allow rules, but they appear directly under each `deny_rules` entry instead of under an `allow` wrapper. - -| Field | Type | Required | Description | -|---|---|---|---| -| `method` | string | Yes | `GET` denies matching upgrade handshakes, `WEBSOCKET_TEXT` denies matching client text messages after upgrade, and `*` matches both inspected actions. | -| `path` | string | Yes | URL path pattern from the original upgrade request. Same glob syntax as allow rules. | -| `query` | map | No | Query parameter matchers from the original upgrade request. Same syntax as allow rule `query`. | - -Example WebSocket deny rules: - -```yaml showLineNumbers={false} -endpoints: - - host: realtime.example.com - port: 443 - protocol: websocket - enforcement: enforce - access: read-write - deny_rules: - - method: WEBSOCKET_TEXT - path: "/v1/admin/**" -``` - -##### GraphQL Deny Rule (`protocol: graphql` or GraphQL-over-WebSocket) +OpenShell does not check the revision of a single `initialize` request, because +the client negotiates the revision in that request. For other requests, +OpenShell reads the revision from the `MCP-Protocol-Version` header, or uses +`2025-03-26` when the header is absent. A duplicate, empty, or +unsupported header value returns `400`, and a supported revision that the +endpoint does not allow returns `403`. For a client that requires an unsupported +revision, omit `protocol` and `mcp` to allow its traffic without MCP inspection. -GraphQL deny rules use the same field names as GraphQL allow rules, but they appear directly under each `deny_rules` entry instead of under an `allow` wrapper. On WebSocket GraphQL endpoints, they apply only to classified GraphQL operation messages; protocol lifecycle messages such as `connection_init`, `ping`, `pong`, and `complete` are allowed as WebSocket control-plane messages and are not payload-logged. +### JSON-RPC Rules | Field | Type | Required | Description | |---|---|---|---| -| `operation_type` | string | Yes | GraphQL operation type to deny: `query`, `mutation`, `subscription`, or `*`. | -| `operation_name` | string | No | GraphQL operation-name glob. | -| `fields` | list of string | No | GraphQL root-field globs. Any matching root field blocks the request. Omit to deny every operation that matches `operation_type` and `operation_name`. | +| `method` | string | Yes | Exact method name, or `*` for all methods. Other globs are rejected. | -Example GraphQL deny rules: +Parameters are not matched. One denied call denies an entire batched request. ```yaml showLineNumbers={false} -endpoints: - - host: api.github.com - port: 443 - protocol: graphql - enforcement: enforce - access: read-write - deny_rules: - - operation_type: mutation - fields: [deleteRepository] - - operation_type: mutation - operation_name: Admin* +rules: + - allow: + method: reports.search +deny_rules: + - method: reports.delete ``` -##### JSON-RPC Deny Rule (`protocol: json-rpc`) - -JSON-RPC deny rules use the same field names as JSON-RPC allow rules, but they appear directly under each `deny_rules` entry instead of under an `allow` wrapper. Deny rules take precedence over allow rules. In a batch request, one denied call denies the full batch. +### Binary Object | Field | Type | Required | Description | |---|---|---|---| -| `method` | string | Yes | Exact JSON-RPC method name to deny, or `*` to deny all JSON-RPC methods. Other wildcard or glob patterns are rejected for generic JSON-RPC endpoints. | - -JSON-RPC deny rules do not support policy `params` matchers yet. Use MCP `tool` rules for MCP tool calls, or deny generic JSON-RPC methods by `method`. +| `path` | string | Yes | Executable path or glob, such as `/usr/bin/curl` or `/usr/lib/jvm/*/bin/java`. | -Example JSON-RPC deny rules: +A binary matches the executable that opens the connection or any of its parent +processes. Scripts run as their interpreter, so list the interpreter, such as +`/usr/bin/python3.12`, for a Python script. List the executable's real path, not +a symlink to it. OpenShell records a hash of each executable the first time it +takes part in a connection, and denies later connections if the file changes. +Refer to [Binary +Matching](/how-it-works/policies/network-rules#binary-matching). -```yaml showLineNumbers={false} -endpoints: - - host: jsonrpc.example.com - port: 443 - path: /rpc - protocol: json-rpc - enforcement: enforce - rules: - - allow: - method: "*" - deny_rules: - - method: reports.delete -``` - -### Binary Object +## Network Middleware -Identifies an executable that is permitted to use the associated endpoints. +A map of up to 10 middleware configurations. Middleware runs on traffic that +network rules allow, in ascending `order`. | Field | Type | Required | Description | |---|---|---|---| -| `path` | string | Yes | Filesystem path to the executable. Supports glob patterns with `*` and `**`. For example, `/sandbox/.vscode-server/**` matches any executable under that directory tree. | - -## Network Middleware - -**Category:** Dynamic - -A map of up to 10 middleware configs selected after network and L7 policy admit an HTTP request or WebSocket upgrade. Each map key is the stable policy-local config identity. Middleware selection is independent of the network policy entry that admitted the traffic. Every matching config runs once by ascending `order` before provider credential injection. WebSocket-capable bindings continue on client text messages after the upgrade. Order values must be unique across the policy, and runtime selection also enforces the 10-stage maximum. +| `middleware` | string | Yes | Built-in middleware, such as `openshell/regex`, or the name of a service registered with the gateway. | +| `endpoints.include` | list of strings | Yes | Host patterns the middleware applies to. | +| `endpoints.exclude` | list of strings | No | Host patterns to skip. Takes precedence over `include`. | +| `order` | integer | No | Run order. Lower values run first. Values must be unique. Defaults to `0`. | +| `config` | object | No | Configuration for the middleware. | +| `on_error` | string | No | `fail_closed` blocks traffic when the middleware fails. `fail_open` skips it. Defaults to `fail_closed`. | +| `name` | string | No | Display name. Defaults to the key. | + +Host patterns match the same way as endpoint hosts, up to 32 patterns per +configuration. A `fail_closed` configuration cannot apply to an endpoint with +`tls: skip`. Refer to [Supervisor +Middleware](/extensibility/supervisor-middleware). ```yaml showLineNumbers={false} network_middlewares: regex-redactor: - name: Redact API tokens middleware: openshell/regex order: 10 config: @@ -622,27 +444,49 @@ network_middlewares: exclude: ["trusted.example.com"] ``` -| Field | Type | Required | Description | -|---|---|---|---| -| `name` | string | No | Human-readable name for the middleware config. Defaults to the map key, which remains its stable identity. | -| `middleware` | string | Yes | Built-in middleware name or operator-owned registration name. `openshell/` is reserved for built-ins. | -| `order` | integer | No | Execution priority. Lower values run first, and values must be unique across the policy. Defaults to `0`; therefore, policies with multiple configs normally specify it explicitly. | -| `config` | object | No | Implementation-owned configuration validated by the selected middleware. | -| `on_error` | string | No | Applies only after an advertised operation binding is selected. `fail_closed` denies the HTTP request or closes the WebSocket when that stage fails; `fail_open` skips a failed HTTP stage or disables a broken WebSocket stage for the rest of that connection. Defaults to `fail_closed`. | -| `endpoints` | object | Yes | Host selector with required non-empty `include` and optional `exclude` lists, limited to 32 combined patterns. Exclusions take precedence. | - -Host selectors use the same case-insensitive exact and DNS glob semantics as network endpoints: `*` matches exactly one DNS label and `**` matches one or more labels, so `**.example.com` covers subdomains but not `example.com` itself. Brace alternates are rejected at validation. A matching attachment joins only the operation chains its implementation advertises. An HTTP-only attachment may inspect a WebSocket upgrade GET without joining the post-upgrade chain; OpenShell permits the messages and records `binding_not_selected` coverage regardless of `on_error`. WebSocket bindings inspect complete client text messages. Binary messages pass with `unsupported_message_type` coverage for active stages. A fail-closed selector that can cover a `tls: skip` endpoint is rejected because OpenShell cannot inspect that traffic through any operation. An all-`fail_open` match may cover the endpoint; the supervisor bypasses the middleware and emits a detection finding. - -See [Supervisor Middleware Configuration](/extensibility/supervisor-middleware/configure) for registration, failure behavior, and operational guidance. +## Matcher Semantics + +Glob patterns follow one set of rules. Each matcher splits values at a +separator: + +| Matcher | Separator | Case-sensitive | +|---|---|---| +| Endpoint `host` and middleware hosts | `.` | No | +| Binary `path` | `/` | Yes | +| REST and WebSocket rule `path` | `/` | Yes | +| Query values and MCP tool names | `.` | Yes | + +- `*` matches any characters except the separator. +- `**` matches across separators only when it is a whole segment, as in + `/repos/**`, `**.example.com`, or `github.**`. Next to other characters, as in + `**secret**`, it behaves like `*`. A whole-segment `**` needs at least one + segment, so `/repos/**` does not match `/repos`. +- `?` matches one character except the separator, and bracket classes such as + `[0-9]` match one character from a set. Endpoint hosts accept only `*` and + `**`, as described in [Destination Fields](#destination-fields). + +Because query values and MCP tool names use `.` as the separator, `*` does not +match a value that contains a dot. For example, `1.*` matches `1.2` but not +`1.2.3`, and `github.*` matches `github.search` but not `github.search.code`. +Use `**` to match any value. + +The endpoint `path` field, which selects among endpoints on the same host and +port, uses different rules. An empty path, `**`, or `/**` matches every path, +`/v1/**` matches `/v1` and every path under it, and in other patterns `*` also +matches `/`. ## Full Example -The following policy grants read-only GitHub API access and npm registry access: - ```yaml showLineNumbers={false} +version: 1 + +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /etc] + read_write: [/tmp] + network_policies: github_rest_api: - name: github-rest-api endpoints: - host: api.github.com port: 443 @@ -650,18 +494,15 @@ network_policies: enforcement: enforce access: read-only binaries: - - path: /usr/local/bin/claude - - path: /usr/bin/node - path: /usr/bin/gh npm_registry: - name: npm-registry endpoints: - host: registry.npmjs.org port: 443 protocol: rest + enforcement: enforce access: read-only allow_encoded_slash: true binaries: - - path: /usr/bin/npm - path: /usr/bin/node ``` diff --git a/docs/how-it-works/providers/profiles.mdx b/docs/how-it-works/providers/profiles.mdx index a5beaacad0..3cb9a6bfba 100644 --- a/docs/how-it-works/providers/profiles.mdx +++ b/docs/how-it-works/providers/profiles.mdx @@ -1082,5 +1082,5 @@ OpenShell rejects provider updates and refresh configuration when they would mak ## Next Steps - Use [Providers](/how-it-works/providers/overview) for the current provider command reference. -- Use [Customize Sandbox Policies](/how-it-works/policies/overview) to apply user-authored policy rules. +- Use [Manage Sandbox Policies](/how-it-works/policies/manage-policies) to apply user-authored policy rules. - Use [Policy Schema Reference](/how-it-works/policies/schema) for endpoint and L7 rule field details. diff --git a/docs/how-it-works/sandboxes/overview.mdx b/docs/how-it-works/sandboxes/overview.mdx index f1535dc487..4fd762ce91 100644 --- a/docs/how-it-works/sandboxes/overview.mdx +++ b/docs/how-it-works/sandboxes/overview.mdx @@ -653,7 +653,7 @@ OpenShell Terminal combines sandbox status and live logs in a single real-time d openshell term ``` -Use the terminal to spot blocked connections marked `action=deny` and provider-related proxy activity. If a connection is blocked unexpectedly, add the host to your network policy or update the attached provider profile. Refer to [Policies](/how-it-works/policies/overview) for the workflow. +Use the terminal to spot blocked connections marked `action=deny` and provider-related proxy activity. If a connection is blocked unexpectedly, add the host to your network policy or update the attached provider profile. Refer to [Manage Sandbox Policies](/how-it-works/policies/manage-policies) for the workflow. The dashboard has three panels stacked vertically: Gateways, Providers (or Global Settings), and Sandboxes. Navigate within a panel with `Up`/`Down` or `j`/`k`. At a list boundary the cursor overflows into the adjacent panel, skipping empty panels. Use `Tab`/`Shift+Tab` to cycle panels directly. Press `h`/`l` or `Left`/`Right` in the middle panel to switch between the Providers and Global Settings tabs. @@ -814,7 +814,7 @@ Every sandbox moves through a defined set of phases: Before workload activation, OpenShell validates the effective policy and matching provider configuration. A rejection keeps the workload unstarted and exposes a `ConfigurationInvalid` condition in `Provisioning`. Use `openshell sandbox get` -to inspect the diagnostic, then [repair the policy or provider configuration](/how-it-works/policies/overview#validation-failures). +to inspect the diagnostic, then [repair the policy or provider configuration](/how-it-works/policies/manage-policies#a-new-sandbox-stays-in-provisioning). Management operations remain available while startup is blocked. After repair, the supervisor completes startup without recreating the sandbox. Starting a stopped sandbox repeats configuration admission before launching its workload. diff --git a/docs/index.yml b/docs/index.yml index 6535fd9005..e4b3ff9245 100644 --- a/docs/index.yml +++ b/docs/index.yml @@ -60,14 +60,21 @@ navigation: contents: - page: "Overview" path: how-it-works/policies/overview.mdx - - page: "Advisor" - path: how-it-works/policies/advisor.mdx - - page: "Schema" - path: how-it-works/policies/schema.mdx - - page: "Prover" + - page: "Network Rules" + path: how-it-works/policies/network-rules.mdx + - page: "Manage Policies" + path: how-it-works/policies/manage-policies.mdx + - page: "Policy Prover" path: how-it-works/policies/prover.mdx + slug: prover + - page: "Policy Advisor" + path: how-it-works/policies/advisor.mdx + slug: advisor - page: "Default Policy" path: how-it-works/policies/default-policy.mdx + - page: "Schema Reference" + path: how-it-works/policies/schema.mdx + slug: schema - page: "Inference" path: how-it-works/inference.mdx - section: "Extensibility" diff --git a/docs/kubernetes/ingress.mdx b/docs/kubernetes/ingress.mdx index c9e081065b..6261ffb041 100644 --- a/docs/kubernetes/ingress.mdx +++ b/docs/kubernetes/ingress.mdx @@ -204,7 +204,7 @@ If you see the error `remote connection failure, transport failure reason: TLS e This situation can occur if you set `pkiInitJob.failOnTimeout=false` and cert-manager issued the certificate after the hook timed out. -For OpenShift 4.22+, see [OpenShift](/kubernetes/openshift#end-to-end-tls-openshift-422) for platform-specific instructions including Gateway and GatewayClass setup. +For OpenShift 4.22+, see [OpenShift](/kubernetes/openshift#end-to-end-tls-using-gateway-api-and-backendtlspolicy-openshift-422) for platform-specific instructions including Gateway and GatewayClass setup. ## SSH Relay diff --git a/docs/observability/logging.mdx b/docs/observability/logging.mdx index 61882c8b74..b41c40c885 100644 --- a/docs/observability/logging.mdx +++ b/docs/observability/logging.mdx @@ -230,7 +230,7 @@ An upstream that the proxy cannot reach returns `502 Bad Gateway`: The `error` field is a short machine-readable code (`policy_denied`, `middleware_denied`, `middleware_failed`, `ssrf_denied`, `upstream_unreachable`). The `detail` field is a human-readable explanation suitable for display in an agent transcript. The optional `reason` field, when present, provides the specific denial cause from the policy engine (for example, which binary was not allowed or which rule was missing). -For L7 REST policy denials, the body also includes structured policy fields such as `method`, `path`, `rule_missing`, and `next_steps`. When policy advisor is enabled, it also includes `agent_guidance`, a short plain-language instruction telling the agent to read `/etc/openshell/skills/policy_advisor.md`, propose the narrowest rule through `http://policy.local/v1/proposals`, wait for `policy_reloaded: true`, and retry. A middleware denial instead identifies the policy-local config in `middleware` and can include a validated `reason_code`. A fail-closed runtime failure uses `middleware_failed` with platform-owned text. Both middleware responses omit `rule_missing`, `next_steps`, and `agent_guidance` because no policy rule is missing. +For L7 REST policy denials, the body also includes structured policy fields such as `method`, `path`, `rule_missing`, and `next_steps`. When the policy advisor is enabled, the body also includes `agent_guidance`, a short plain-language instruction telling the agent to read `/etc/openshell/skills/policy_advisor.md`, propose the narrowest rule through `http://policy.local/v1/proposals`, wait for `policy_reloaded: true`, and retry. A middleware denial instead identifies the policy-local config in `middleware` and can include a validated `reason_code`. A fail-closed runtime failure uses `middleware_failed` with platform-owned text. Both middleware responses omit `rule_missing`, `next_steps`, and `agent_guidance` because no policy rule is missing. ## Filesystem Sandbox Logs diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index 20335c7c8b..a310900b98 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -97,10 +97,10 @@ The `protocol` field on an endpoint controls whether the proxy inspects individu | Aspect | Detail | |---|---| -| Default | Endpoints without a `protocol` field use L4-only enforcement: the proxy checks host, port, and binary, then relays the TCP stream without inspecting payloads. Provider-credentialed endpoints reject this mode unless an operator explicitly opts in. | +| Default | Endpoints without a `protocol` field apply no request rules. The proxy checks host, port, and binary, still terminates TLS and parses HTTP requests, and requires the request authority to match the destination, but it allows any HTTP method and path. Non-HTTP traffic is relayed without inspection. `protocol: tcp` endpoints also apply no request rules, and `tls: skip` endpoints relay traffic without terminating TLS. Provider-credentialed endpoints reject uninspected modes unless an operator explicitly opts in. | | What you can change | Add `protocol: rest` to enable per-request HTTP method/path inspection, `protocol: websocket` to inspect RFC 6455 upgrade handshakes and client text messages, or `protocol: graphql` to inspect GraphQL-over-HTTP operation type, operation name, and root fields. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket messages. Pair inspected protocols with `rules` or access presets (`full`, `read-only`, `read-write`). REST endpoints that need credential placeholders in supported text request bodies can set `request_body_credential_rewrite: true`. Set `allow_uninspected_credentials: true` only as an explicit exception for credentialed traffic that cannot use an inspected path. | -| Risk if relaxed | L4-only endpoints allow the agent to send any data through the tunnel after the initial connection is permitted. The proxy cannot see HTTP methods, paths, or GraphQL operations. Adding `access: full` with L7 inspection enables observability but permits all inspected actions. | -| Recommendation | Use `protocol: rest` with specific `rules` for APIs where intent is encoded in method and path. Add `request_body_credential_rewrite: true` only for REST APIs that require OpenShell-managed credentials in UTF-8 JSON, form, or text request bodies. Use `protocol: graphql` for GraphQL-over-HTTP APIs where destructive operations are body-encoded. Use `protocol: websocket` for RFC 6455 endpoints, with explicit `GET` and `WEBSOCKET_TEXT` rules for raw text protocols or explicit GraphQL operation rules for GraphQL-over-WebSocket. Prefer `access: read-only` or explicit allowlists, and deny hash-only persisted queries unless you maintain a trusted registry. Omit `protocol` for non-HTTP protocols. For WebSocket endpoints that must carry placeholder credentials in client text frames, add `websocket_credential_rewrite: true`. | +| Risk if relaxed | Endpoints without request rules allow the agent to send any request or data to the destination after the connection is permitted. The proxy does not restrict HTTP methods, paths, or GraphQL operations. Adding `access: full` with L7 inspection enables observability but permits all inspected actions. | +| Recommendation | Use `protocol: rest` with specific `rules` for APIs where intent is encoded in method and path. Add `request_body_credential_rewrite: true` only for REST APIs that require OpenShell-managed credentials in UTF-8 JSON, form, or text request bodies. Use `protocol: graphql` for GraphQL-over-HTTP APIs where destructive operations are body-encoded. Use `protocol: websocket` for RFC 6455 endpoints, with explicit `GET` and `WEBSOCKET_TEXT` rules for raw text protocols or explicit GraphQL operation rules for GraphQL-over-WebSocket. Prefer `access: read-only` or explicit allowlists, and deny hash-only persisted queries unless you maintain a trusted registry. Use `protocol: tcp` for native non-HTTP clients such as databases, and refer to [Allow Native TCP](/how-it-works/policies/network-rules#allow-native-tcp) for when to add `tls: skip`. For WebSocket endpoints that must carry placeholder credentials in client text frames, add `websocket_credential_rewrite: true`. | ### Enforcement Mode (`audit` vs `enforce`) @@ -132,7 +132,7 @@ After OPA policy allows a connection, the proxy resolves DNS and rejects undecla | Aspect | Detail | |---|---| -| Default | The proxy blocks private IPs for undeclared, wildcard, hostless, and policy-advisor-proposed endpoints. Exact hostnames declared in user policy may resolve to private RFC 1918 addresses. Loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), and unspecified (`0.0.0.0`) addresses are always blocked and cannot be overridden with `allowed_ips`. | +| Default | The proxy blocks private IPs for undeclared, wildcard, and hostless endpoints, and for endpoints added through the policy advisor. Exact hostnames declared in user policy may resolve to private RFC 1918 addresses. Loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), and unspecified (`0.0.0.0`) addresses are always blocked and cannot be overridden with `allowed_ips`. | | What you can change | Declare a known internal service as an exact `host` endpoint, or add `allowed_ips` (CIDR notation) to an endpoint to permit specific private IP ranges for wildcard or hostless policies. On Linux, the proxy consults the sandbox's `/etc/hosts` before DNS, so Kubernetes `hostAliases` can make LAN-only hostnames resolvable. Policies with `allowed_ips` entries that overlap loopback, link-local, or unspecified addresses fail to load with a clear validation error. | | Risk if relaxed | Without SSRF protection, a misconfigured policy could allow the agent to reach cloud metadata services (`169.254.169.254`), internal databases, or other infrastructure endpoints through DNS rebinding. | | Recommendation | Prefer exact hostname endpoints for stable internal services. Use `allowed_ips` when you need hostless or wildcard authorization, and scope the CIDR as narrowly as possible (for example, `10.0.5.20/32` for a single host). Loopback, link-local, and unspecified addresses are always blocked regardless of `allowed_ips`. `hostAliases` change resolution, not authorization: `host: searxng.local` with `/etc/hosts` mapping to `192.168.1.105` is trusted only when that exact hostname is declared by user policy. The policy advisor does not propose rules for always-blocked destinations and still requires a separate `allowed_ips` edit for private-IP endpoints. | @@ -181,7 +181,7 @@ The policy separates filesystem paths into read-only and read-write groups. | Aspect | Detail | |---|---| -| Default | System paths (`/usr`, `/lib`, `/etc`, `/var/log`) are read-only. The resolved working directory and `/tmp` are read-write. `/app` is conditionally included if it exists. | +| Default | System paths (`/usr`, `/lib`, `/etc`, `/var/log`) are read-only. The resolved working directory and `/tmp` are read-write. List any other path your workload needs, such as `/app`. | | What you can change | Add or remove paths in `filesystem_policy.read_only` and `filesystem_policy.read_write`. | | Risk if relaxed | Making system paths writable lets the agent replace binaries, modify TLS trust stores, or change DNS resolution. Validation rejects broad read-write paths (like `/`). | | Recommendation | Keep system paths read-only. If the agent needs additional writable space, add a specific subdirectory. | @@ -286,7 +286,7 @@ The following patterns weaken security without providing meaningful benefit. | Mistake | Why it matters | What to do instead | |---------|---------------|-------------------| -| Omitting an inspected protocol on REST or WebSocket API endpoints | Without `protocol: rest` or `protocol: websocket`, the proxy uses L4-only enforcement. It allows the TCP stream through after checking host, port, and binary, but cannot inspect individual HTTP requests or WebSocket text messages. | Add `protocol: rest` or `protocol: websocket` with specific `rules` to enable method and path control. | +| Omitting an inspected protocol on REST or WebSocket API endpoints | Without `protocol: rest` or `protocol: websocket`, the proxy applies no request rules. It allows any HTTP method and path after checking host, port, and binary, and does not inspect WebSocket text messages. | Add `protocol: rest` or `protocol: websocket` with specific `rules` to enable method and path control. | | Using `access: full` when finer rules would suffice | `access: full` with `protocol: rest` or `protocol: websocket` enables inspection but allows all methods and paths for that protocol. | Use `access: read-only` or explicit `rules` to restrict what the agent can do at the L7 level. | | Adding endpoints permanently when operator approval would suffice | Adding endpoints to the policy YAML makes them permanently reachable across all instances. | Use operator approval. Approved endpoints persist within the sandbox instance but reset on re-creation. | | Using broad binary globs | A glob like `/**` allows any binary to reach the endpoint, defeating binary-scoped enforcement. | Scope globs to specific directories (for example, `/sandbox/.vscode-server/**`). | @@ -296,7 +296,8 @@ The following patterns weaken security without providing meaningful benefit. ## Related Topics -- [Policies](/how-it-works/policies/overview) for applying and iterating on sandbox policies. +- [Sandbox Policies](/how-it-works/policies/overview) for how policies are evaluated and applied. +- [Manage Sandbox Policies](/how-it-works/policies/manage-policies) for applying and iterating on sandbox policies. - [Policy Schema](/how-it-works/policies/schema) for the full field-by-field YAML reference. - [Default Policy](/how-it-works/policies/default-policy) for the built-in default policy breakdown. - [Gateway Auth](/how-it-works/gateways/authentication) for gateway authentication details. diff --git a/docs/tutorials/first-network-policy.mdx b/docs/tutorials/first-network-policy.mdx index bed3335666..bf966a458d 100644 --- a/docs/tutorials/first-network-policy.mdx +++ b/docs/tutorials/first-network-policy.mdx @@ -80,34 +80,36 @@ Keep the interactive sandbox shell open for the rest of the tutorial. Exiting it Every denied connection produces a structured log entry. In your second (host) terminal, query the sandbox logs to confirm the denial and inspect the reason. ```shell -openshell logs demo --since 5m +openshell logs demo --since 5m --source sandbox ``` You see a line like: ```text -action=deny dst_host=api.github.com dst_port=443 binary=/usr/bin/curl deny_reason="no matching network policy" +[1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> api.github.com:443 [policy:- engine:opa] [reason:network connections not allowed by policy] ``` Every denied connection is logged with the destination, the binary that attempted it, and the reason. Nothing gets out silently. ## Apply a Read-Only GitHub API Policy -To allow the sandbox to reach the GitHub API, define a network policy that grants read-only access. The policy specifies which host, port, binary, and HTTP methods are permitted. Create a file called `github_readonly.yaml` with the following content: +To allow the sandbox to reach the GitHub API, add a network rule that grants `curl` read-only access. Run this command from your host terminal: -```yaml -version: 1 +```shell +openshell policy update demo \ + --rule-name github_api \ + --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:enforce \ + --wait +``` -filesystem_policy: - include_workdir: true - read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log] - read_write: [/tmp, /dev/null] -landlock: - compatibility: best_effort +The endpoint specification lists the host, port, access preset, protocol, and enforcement mode. `rest` tells the proxy to terminate TLS and inspect each HTTP request, `read-only` permits `GET`, `HEAD`, and `OPTIONS`, and `enforce` blocks every other request. `policy update` changes only the network rules and keeps the rest of the sandbox's policy. +The command adds a rule equivalent to this YAML in the policy's `network_policies` section: + +```yaml network_policies: github_api: - name: github-api-readonly endpoints: - host: api.github.com port: 443 @@ -115,25 +117,19 @@ network_policies: enforcement: enforce access: read-only binaries: - - { path: /usr/bin/curl } + - path: /usr/bin/curl ``` -The `filesystem_policy` and `landlock` sections preserve the default sandbox settings, while process identity is omitted so the active compute driver can select it. These sections are required because `policy set` replaces the entire policy. The `network_policies` section is the key part: `curl` can make GET, HEAD, and OPTIONS requests to `api.github.com` over HTTPS. Everything else is denied. The proxy auto-detects TLS on HTTPS endpoints and terminates it to inspect each HTTP request and enforce the `read-only` access preset at the method level. +To see the complete policy, run `openshell policy get demo --base`. -Apply it: - -```shell -openshell policy set demo --policy github_readonly.yaml --wait -``` - -`--wait` blocks until the sandbox confirms the new policy is loaded. No restart required. Policies are hot-reloaded. +`--wait` waits until the sandbox reports a result for the new policy revision. No restart is required, because network rules reload while the sandbox runs. This tutorial uses `curl` and `read-only` access to keep things simple. When building policies for real workloads: -- To scope the policy to an agent, replace the `binaries` section with your agent's binary, such as `/usr/local/bin/claude`, instead of `curl`. -- To grant write access, change `access: read-only` to `read-write` or add explicit `rules` for specific paths. Refer to the [Policy Schema](/how-it-works/policies/schema). -- To allow additional endpoints, stack multiple policies in the same file for PyPI, npm, or your internal APIs. Refer to [Policies](/how-it-works/policies/overview) for examples. +- To scope the rule to an agent, use your agent's binary, such as `/usr/local/bin/claude`, instead of `curl`. +- To grant write access, use the `read-write` preset or add explicit rules for specific paths. Refer to the [Policy Schema](/how-it-works/policies/schema). +- To allow other services, such as PyPI, npm, or your internal APIs, adapt the examples in [Network Rules](/how-it-works/policies/network-rules). @@ -161,25 +157,27 @@ curl -s -X POST https://api.github.com/repos/octocat/hello-world/issues \ -d '{"title":"oops"}' ``` -```json -{"error":"policy_denied","policy":"github-api-readonly","detail":"POST /repos/octocat/hello-world/issues not permitted by policy"} +The proxy returns a `403` response with a JSON body that includes fields like these, along with details about the denied request: + +```text +{...,"error":"policy_denied",...,"policy":"github_api",...,"rule":"POST /repos/octocat/hello-world/issues",...} ``` -The CONNECT request succeeded because `api.github.com` is allowed, but the L7 proxy inspected the HTTP method and returned `403`. `POST` is not in the `read-only` preset. An agent with this policy can read code from GitHub but cannot create issues, push commits, or modify anything. +The connection succeeded because `api.github.com` is allowed, but the proxy inspected the HTTP method and returned `403`. `POST` is not in the `read-only` preset. An agent with this policy can read code from GitHub but cannot create issues, push commits, or modify anything. ## Check the L7 Deny Log -L7 denials are logged separately from connection-level denials. The log entry includes the exact HTTP method and path that the proxy rejected. Run this from your second (host) terminal, leaving the sandbox shell open: +Request-level (L7) denials are logged separately from connection-level denials. The log entry includes the exact HTTP method and path that the proxy rejected. Run this from your second (host) terminal, leaving the sandbox shell open: ```shell -openshell logs demo --level warn --since 5m +openshell logs demo --since 5m --source sandbox ``` ```text -l7_decision=deny dst_host=api.github.com l7_action=POST l7_target=/repos/octocat/hello-world/issues l7_deny_reason="POST /repos/octocat/hello-world/issues not permitted by policy" +[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://api.github.com:443/repos/octocat/hello-world/issues [policy:github_api engine:l7] [reason:L7_REQUEST deny POST api.github.com:443/repos/octocat/hello-world/issues reason=POST /repos/octocat/hello-world/issues not permitted by policy] ``` -The log captures the exact HTTP method, path, and deny reason. In production, pipe these logs to your SIEM for a complete audit trail of every request your agent makes. +Policy events are INFO-level log records regardless of their severity, so do not filter them out with `--level warn`. In production, export these events to your SIEM for a complete audit trail of every request your agent makes. Refer to [Logging](/observability/logging) for the event format. To log violations without blocking requests, set `enforcement: audit` instead of `enforcement: enforce` in the policy. This is useful for building a policy iteratively: deploy in audit mode, review the logs, and switch to enforce when the rules are correct. @@ -207,4 +205,5 @@ bash examples/sandbox-policy-quickstart/demo.sh ## Next Steps +- To understand how OpenShell evaluates network rules, refer to [Sandbox Policies](/how-it-works/policies/overview). - To walk through a full policy iteration with Claude Code, including diagnosing denials and applying fixes from outside the sandbox, refer to [GitHub Sandbox](/tutorials/github-push-access). diff --git a/docs/tutorials/github-sandbox.mdx b/docs/tutorials/github-sandbox.mdx index afacbfb3de..53e32dc59d 100644 --- a/docs/tutorials/github-sandbox.mdx +++ b/docs/tutorials/github-sandbox.mdx @@ -108,34 +108,41 @@ not grant the missing network authority. In terminal 2, inspect recent sandbox logs: ```shell -openshell logs github-demo --level warn --since 5m +openshell logs github-demo --since 5m --source sandbox ``` You should see a denial for a request resembling this one: ```text -action=deny dst_host=github.com dst_port=443 binary=/usr/bin/git l7_action=POST l7_target=//.git/git-receive-pack +[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://github.com:443//.git/git-receive-pack [policy:_provider_my_github engine:l7] [reason:L7_REQUEST deny POST github.com:443//.git/git-receive-pack reason=POST //.git/git-receive-pack not permitted by policy] ``` +`_provider_my_github` is the rule that OpenShell composes from the attached +`my-github` provider. Policy events are INFO-level log records, so do not filter +them out with `--level warn`. + You can also run `openshell term` to inspect policy decisions in the terminal dashboard. ## Create a Repository-Scoped Policy -Create `github-push.yaml`. Replace `` and ``, and adjust `binaries` -to match your image: +`policy set` replaces the complete base policy, so start from the sandbox's +current one. In terminal 2, print it: -```yaml -version: 1 +```shell +openshell policy get github-demo --base +``` -filesystem_policy: - include_workdir: true - read_only: [/usr, /lib, /proc, /dev/urandom, /etc, /var/log] - read_write: [/tmp, /dev/null] +The command prints revision details followed by the policy. Save only the policy +YAML as `github-push.yaml`. Keep its filesystem, Landlock, and process settings +unchanged, because OpenShell rejects removed filesystem paths and changed +Landlock or process settings on a running sandbox. -landlock: - compatibility: best_effort +Add these entries under `network_policies`, creating the section if it is +missing. Replace `` and ``, and adjust `binaries` to match your +image: +```yaml network_policies: github_repository_push: name: github-repository-push @@ -179,10 +186,6 @@ repository. The second lets `gh` use REST operations scoped to the same repository. The attached GitHub provider continues to supply credential placement and its broader read-only rules. -The filesystem and Landlock sections preserve the fallback policy's static -settings because `policy set` replaces the complete user-authored base policy. -Process identity remains omitted so the compute driver can select it. - ## Apply the Policy Apply the policy and wait for the new revision to load: @@ -220,5 +223,5 @@ openshell sandbox delete github-demo ## Next Steps - Review [Profiles](/how-it-works/providers/profiles) for endpoint-scoped credential placement. -- Review [Policies](/how-it-works/policies/overview) for incremental policy updates and policy history. +- Review [Manage Sandbox Policies](/how-it-works/policies/manage-policies) for incremental policy updates and policy history. - Review the [Policy Schema](/how-it-works/policies/schema) for REST, GraphQL, and other protocol rules.