From cc9ee499a0c8c98f4cbc04ebef52c36e076bbff0 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 16:00:50 +0000 Subject: [PATCH 01/40] docs(policy): correct schema and default policy guidance Signed-off-by: Johnny Greco --- docs/how-it-works/policies/default-policy.mdx | 81 ++++++++-- docs/how-it-works/policies/schema.mdx | 143 ++++++++++++++---- 2 files changed, 180 insertions(+), 44 deletions(-) diff --git a/docs/how-it-works/policies/default-policy.mdx b/docs/how-it-works/policies/default-policy.mdx index d670c80e2e..7cfd84b23a 100644 --- a/docs/how-it-works/policies/default-policy.mdx +++ b/docs/how-it-works/policies/default-policy.mdx @@ -3,32 +3,81 @@ # SPDX-License-Identifier: Apache-2.0 title: "Default Policy Reference" sidebar-title: "Default Policy" -description: "Breakdown of the built-in default policy applied when you create an OpenShell sandbox without a custom policy." +description: "Policy source selection and the restrictive fallback used when a sandbox has no explicit or embedded policy." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Policy" position: 2 --- -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. +OpenShell uses the restrictive fallback only when a sandbox has no stored policy +and image policy discovery finds no policy. Creating a sandbox without +`--policy` does not by itself prove that the fallback is active. -## Filesystem Access +## Policy Selection -The fallback includes the sandbox working directory and grants read-only access to standard runtime paths: +At creation, an explicit `--policy` file takes precedence over the +`OPENSHELL_SANDBOX_POLICY` environment variable. Either source supplies the +sandbox-authored policy stored by the gateway. -- `/usr` -- `/lib` -- `/proc` -- `/dev/urandom` -- `/etc` -- `/var/log` +When the gateway has no stored sandbox policy, the supervisor checks the image's +current embedded path, `/etc/openshell/policy.yaml`, and then the legacy path, +`/etc/navigator/policy.yaml`. A valid embedded policy initializes the missing +stored policy. A missing embedded policy selects the restrictive fallback. An +invalid embedded policy keeps the first workload unstarted for configuration +repair; it does not silently select the fallback. -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. +An active gateway-global policy replaces sandbox policy selection and suppresses +provider-added network grants. Otherwise, attached providers can add their +network rules to the selected sandbox policy. Refer to +[How OpenShell Selects a Policy](/sandboxes/policies#how-openshell-selects-a-policy) for +the selection order. -## Network Access +## Fallback Filesystem Access -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. +The restrictive fallback includes the sandbox working directory and grants +read-only access to these paths: -## Process Identity +- `/bin`. +- `/usr`. +- `/lib`. +- `/proc`. +- `/dev/urandom`. +- `/etc`. +- `/var/log`. -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. +It grants read-write access to `/tmp` and `/dev/null`. Landlock user-policy +compatibility defaults to `best_effort`. -Use `openshell policy get --full` to inspect the effective policy. Refer to [Customize Sandbox Policies](/how-it-works/policies/overview) to replace the fallback. +The runtime can enrich filesystem paths required for operation before applying +the user ruleset. On current Linux isolation paths, a separate mandatory +Landlock baseline protects the private `/.openshell` hierarchy and requires the +supported baseline ABI. The user-policy `best_effort` setting does not disable +that mandatory protection or promise support on a kernel that cannot install it. + +## Fallback Network Access + +The fallback defines no network policies or middleware. Outbound network access +is denied until sandbox-authored or provider-composed rules permit a destination +and executable. + +Provider additions are runtime composition, not fields in the fallback policy. +Inspect the base and effective views separately when you need to identify a +provider-derived grant. + +## Fallback Process Identity + +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. Other drivers apply their configured non-root identity. + +## Inspect the Selected Policy + +Inspect the sandbox-authored base and the gateway's effective representation: + +```shell +openshell policy get --base +openshell policy get --full +``` + +`--full` shows gateway composition, not an attestation of the restrictions +already installed in a running kernel process. Use sandbox readiness, revision +status, request checks, and runtime logs to verify activation. diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 929afdb7e8..14757ec2f9 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -8,7 +8,9 @@ keywords: "Generative AI, Cybersecurity, Policy, Schema, YAML, Reference, Securi position: 3 --- -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 canonical authored YAML and JSON policy fields. It +distinguishes startup-time controls from dynamically activated controls and +documents defaults, matcher behavior, and validation constraints. ## Top-Level Structure @@ -32,13 +34,26 @@ network_middlewares: { ... } | `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. +The YAML root and each present top-level section listed as an object or map must +be a mapping. Each named network policy or middleware entry must also be a +mapping. -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. +Startup-time fields are installed before the workload process starts. After +activation, OpenShell rejects removals from the filesystem baseline and changes +to workdir inclusion, Landlock mode, and process identity. Additive filesystem +paths may be accepted into stored configuration but do not change the current +child. Recreate the sandbox for an assured new startup configuration. A sandbox +that has never activated can repair startup fields while configuration admission +is pending or rejected. -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. +Dynamic fields can activate on a running sandbox after the complete effective +candidate validates. `policy update` edits only `network_policies`; use an +editable base with `policy set` for middleware or complete replacement. +Middleware policy changes can select built-ins or external services already +registered with the gateway. Adding or changing an external service registration +requires a gateway restart. -## Parsing and representation +## 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 @@ -108,7 +123,10 @@ Controls filesystem access inside the sandbox. Paths not listed in either `read_ - Each individual path must not exceed 4096 characters. - The combined total of `read_only` and `read_write` paths must not exceed 256. -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. +Policies that violate these constraints are rejected with `INVALID_ARGUMENT` at +creation or update time. An invalid embedded image policy blocks initial +workload activation for configuration repair; only a missing image policy +selects the restrictive fallback. Example: @@ -136,18 +154,25 @@ Configures [Landlock LSM](https://docs.kernel.org/security/landlock.html) enforc |---|---|---|---|---| | `compatibility` | string | No | `best_effort`, `hard_requirement` | How OpenShell handles Landlock failures. Refer to the behavior table below. | -**Compatibility modes:** +These compatibility modes describe the optional user filesystem ruleset: | 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` | User ruleset skipped. | User ruleset warns and continues. | Skips the path and applies remaining user rules. | Warns and skips the empty user ruleset. | +| `hard_requirement` | Aborts sandbox startup. | Aborts sandbox startup. | Aborts when prepared with privileged path access; current-user preparation skips individually inaccessible paths and applies the remainder. | 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. +`best_effort` is the default for the user ruleset and handles missing paths +without discarding paths that can be enforced. On current Linux isolation paths, +OpenShell also installs a mandatory capability-free Landlock baseline that +protects `/.openshell` and requires ABI v3. The user `best_effort` value does not +disable that baseline or make an unsupported kernel compatible. `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. +When the runtime prepares user rules as the current workload identity, it cannot +distinguish an intentionally inaccessible path from a misconfigured one. It +skips individual inaccessible paths and applies the remainder. Other +`hard_requirement` failures still abort startup. 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"). @@ -199,8 +224,8 @@ Each entry in the `network_policies` map has the following fields: | 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. | +| `endpoints` | list of endpoint objects | No | Hosts and ports this entry permits. An omitted or empty list grants no destination. | +| `binaries` | list of binary objects | No | Executable selectors associated with the endpoints. Prefer explicit paths. Empty-scope runtime behavior depends on the trusted process-identity mode, so do not use an empty list as a portable any-process grant. | ### Endpoint Object @@ -209,11 +234,12 @@ Each endpoint defines a reachable destination and optional inspection rules. | 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. | +| `port` | integer | Conditional | One TCP port. Use either `port` or `ports` for clarity. When both are authored and `ports` is nonempty, `ports` takes precedence. | +| `ports` | list of integers | Conditional | One or more TCP ports. Takes precedence over scalar `port` when nonempty. At least one effective port is required for every protocol endpoint. | | `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. | +| `protocol` | string | No | Set to `tcp` with a valid DNS hostname for native TCP through policy DNS and transparent capture. Omit the field to define no protocol-specific request rules on the explicit proxy; default TLS detection and HTTP destination checks still apply. Use `tls: skip` for a deliberately raw proxy stream. Set to `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for the corresponding request 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. Remove those values and omit `tls` to use automatic handling. | +| `enforcement` | string | No | Defaults to `audit`. `enforce` actively blocks disallowed application requests. `audit` logs applicable request-rule violations but forwards the request; it does not bypass other parsing, destination, credential, or middleware checks. Other values are rejected before activation. Not valid with `protocol: tcp`. | | `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. | @@ -221,8 +247,8 @@ Each endpoint defines a reachable destination and optional inspection rules. | `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). | +| `allow_uninspected_credentials` | bool | No | Explicit security-sensitive opt-in that permits a provider-credentialed endpoint to omit a protocol-specific request policy or use a `tls: skip` tunnel. 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](/providers/aws-sigv4). | | `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. | @@ -231,12 +257,36 @@ Each endpoint defines a reachable destination and optional inspection rules. | `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.versions` | list of string | No | Nonempty allowlist of supported MCP core revisions. Omission allows only `2025-11-25`. See [MCP Version Selection](#mcp-version-selection). | | `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`. | +### MCP Version Selection + +`mcp.versions` accepts exact revisions from the closed supported set: +`2025-03-26`, `2025-06-18`, and `2025-11-25`. Omit the key to allow only +`2025-11-25`; omission never means the latest revision or all known revisions. +`versions: null`, `versions: []`, duplicate values, values with extra +whitespace, and revisions outside the supported set are invalid. + +Except for a valid standalone `initialize` request, OpenShell selects the +revision from one `MCP-Protocol-Version` header. When the header is absent, it +uses the MCP compatibility fallback `2025-03-26`. A duplicate, empty, or +unsupported header value returns `400 Bad Request`. A supported revision that +is not in the endpoint allowlist returns `403 Forbidden`. This is a stateless +per-request header check; OpenShell does not negotiate a revision or bind +revision-specific session state. + +The sessionless `2026-07-28` revision is not yet supported. If a client requires +an unsupported revision, omit both `protocol` and `mcp` to forgo MCP-specific +request enforcement while retaining the explicit proxy's default TLS handling +and HTTP destination checks. Use `protocol: tcp` or `tls: skip` only when the +client actually requires a raw stream and the weaker boundary is acceptable. + +### Endpoint Serialization and Validation + 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 @@ -253,8 +303,7 @@ access selects no preset. Unknown enum numbers are rejected before activation. - `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. +- 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. The standard runtime initializes the substrate unconditionally, so a running sandbox can add its first TCP endpoint through a live update. - 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`. @@ -272,6 +321,22 @@ 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. +### Matcher Semantics + +Different policy fields use different wildcard boundaries. + +| Matcher | Comparison | Important behavior | +|---|---|---| +| Endpoint `host` | Case-insensitive DNS name or IP comparison. | DNS `*` matches one label and `**` matches one or more labels. Host wildcard placement is restricted by validation. | +| Binary `path` | Canonical executable or trusted ancestor path. | Symlinks resolve to canonical identity. `*` matches within a path segment and `**` crosses directories. Identity enforcement depends on trusted runtime configuration. | +| REST or WebSocket request `path` | Case-sensitive URL path glob. | Both `*` and `**` can cross `/`; this differs from common shell glob intuition. | +| Query value | Case-sensitive decoded value glob. | For allow rules, every duplicate value for a configured key must match. | + +For a deny rule with query conditions, every configured key must be present and +each key must have at least one matching value. A duplicate nonmatching value +does not cancel another value that matches the deny. Deny rules still take +precedence over allows. + 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 @@ -324,7 +389,7 @@ REST allow rules match HTTP requests by method, path, and optional query paramet |---|---|---|---| | `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-*"] }`). | +| `query` | map | No | Query parameter matchers keyed by decoded, case-sensitive name. A matcher can be a glob string (`tag: "foo-*"`) or an object with `any` (`tag: { any: ["foo-*", "bar-*"] }`). Every duplicate value for a configured key must match an allow-rule matcher. | Example REST allow rules: @@ -454,7 +519,8 @@ 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. +OpenShell canonicalizes an explicit list in semantic order. See [MCP Version +Selection](#mcp-version-selection) for the stateless request-header behavior. ##### JSON-RPC Allow Rule (`protocol: json-rpc`) @@ -498,7 +564,7 @@ REST deny rules use the same field names as REST allow rules, but they appear di |---|---|---|---| | `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`. | +| `query` | map | No | Query parameter matchers. Every configured key must be present with at least one matching value. Additional nonmatching duplicate values do not cancel a match. | Example REST deny rules: @@ -597,16 +663,33 @@ endpoints: ### Binary Object Identifies an executable that is permitted to use the associated endpoints. +OpenShell resolves the executable's canonical identity and can match a trusted +ancestor process. Symlink spelling therefore does not bypass canonical path +checks. Trusted runtime configuration can disable binary identity enforcement, +so inspect the active runtime mode when diagnosing an unexpected result. + +With identity enforcement enabled, OpenShell pins each executable or ancestor +path used for authorization to its first observed SHA256 digest. A changed +digest, missing evidence, or conflicting evidence denies access. Command-line +paths do not authorize access. Restart the sandbox runtime after intentionally +replacing an executable at an authorized path so it can establish a new pin. | 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. | +| `path` | string | Yes | Canonical filesystem path selector. `*` matches within one path segment and `**` crosses directory boundaries. For example, `/sandbox/.vscode-server/**` matches executables below 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. +A map of up to 10 middleware configs selected after network and L7 policy admit +an HTTP request or WebSocket upgrade. Request middleware runs before provider +credential injection. Response middleware that advertises +`HTTP_RESPONSE/PRE_RETURN` runs on the final upstream response before return. +WebSocket-capable bindings continue on client text messages after the upgrade. +Selection is independent of the admitting network rule, and host matching alone +does not select an operation the implementation did not advertise. Each matching +config runs once by ascending `order`; order values must be unique. ```yaml showLineNumbers={false} network_middlewares: @@ -637,9 +720,12 @@ See [Supervisor Middleware Configuration](/extensibility/supervisor-middleware/c ## Full Example -The following policy grants read-only GitHub API access and npm registry access: +The following complete policy file grants enforced read-only GitHub API and npm +registry access: ```yaml showLineNumbers={false} +version: 1 + network_policies: github_rest_api: name: github-rest-api @@ -659,6 +745,7 @@ network_policies: - host: registry.npmjs.org port: 443 protocol: rest + enforcement: enforce access: read-only allow_encoded_slash: true binaries: From a1b38b79eef785426dcc3edbe3327b3ce0f45cb1 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 16:01:28 +0000 Subject: [PATCH 02/40] docs(policy): add network recipes and update command reference Signed-off-by: Johnny Greco --- docs/reference/policy-updates.mdx | 319 ++++++++++++++ docs/sandboxes/network-policy-recipes.mdx | 503 ++++++++++++++++++++++ 2 files changed, 822 insertions(+) create mode 100644 docs/reference/policy-updates.mdx create mode 100644 docs/sandboxes/network-policy-recipes.mdx diff --git a/docs/reference/policy-updates.mdx b/docs/reference/policy-updates.mdx new file mode 100644 index 0000000000..77e7e48a5e --- /dev/null +++ b/docs/reference/policy-updates.mdx @@ -0,0 +1,319 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +title: "Incremental Policy Update Reference" +sidebar-title: "Policy Updates" +description: "Command grammar, scope requirements, merge behavior, previews, and revision status for incremental sandbox policy updates." +keywords: "Generative AI, Cybersecurity, Policy, CLI, Incremental Update, Hot Reload" +position: 4 +--- + +`openshell policy update` merges explicit operations into a sandbox's current +`network_policies` map. It does not edit filesystem, Landlock, process, or +middleware sections. Use `policy set` with an exported base policy for those +broader changes. + +This reference describes the command's flags, input formats, and update +behavior. For the policy lifecycle and editing workflow, see +[Configure Sandbox Policies](/sandboxes/policies). Examples use `my-sandbox` +for an existing sandbox; adapt rule names, executable paths, and destinations +to its current base policy. + +## Command Flags + +One command can contain several compatible operations. The gateway applies the +batch atomically and persists at most one new revision. + +| Flag | Purpose | +|---|---| +| `--add-endpoint ` | Add or merge one endpoint and its declared binary scope. Repeat for multiple endpoints. | +| `--remove-endpoint ` | Remove the host and port match from every authored network rule. A multi-port endpoint keeps its other ports. | +| `--remove-rule ` | Remove a complete named `network_policies` entry. | +| `--add-allow ` | Append a REST or WebSocket method and path allow rule. | +| `--add-deny ` | Append a REST or WebSocket method and path deny rule. | +| `--binary ` | Add binaries with endpoints, or declare the complete stored binary scope for an L7 append. Repeat as needed. | +| `--rule-name ` | Name one new endpoint rule or select the existing rule for an L7 append. Required for `--add-allow` and `--add-deny`. | +| `--any-binary` | Declare that the existing L7 target stores an empty binary scope. Cannot be combined with `--binary`. | +| `--endpoint-path ` | Select one existing endpoint by its exact stored path. Pass `''` to select no path. | +| `--dry-run` | Fetch the current policy and preview the local merge without saving a revision. | +| `--wait` | Poll for the submitted revision's result. Cannot be combined with `--dry-run`. | +| `--timeout ` | Set the `--wait` timeout. Defaults to 60 seconds. | + +`--any-binary` describes the target rule's stored merge scope. It is not an +independent claim that every runtime identity mode admits every process. Prefer +explicit binary paths in authored rules and introductory workflows. + +## Endpoint Specification + +`--add-endpoint` uses this grammar: + +```text +host:port[:access[:protocol[:enforcement[:options]]]] +``` + +| Segment | Accepted values and behavior | +|---|---| +| `host` | Required destination hostname. | +| `port` | Required integer from 1 through 65535. | +| `access` | `read-only`, `read-write`, or `full` for inspected endpoints. | +| `protocol` | `tcp`, `rest`, `websocket`, or `sql`. Use full policy YAML for GraphQL, MCP, and JSON-RPC. | +| `enforcement` | `enforce` or `audit`. Omission selects audit for inspected requests. | +| `options` | Comma-separated options listed below. | + +Endpoint options are: + +| Option | Effect | +|---|---| +| `allowed-ip=` | Add a destination IP allowance. Repeat the option in the comma-separated list for multiple values. | +| `request-body-credential-rewrite` | Rewrite supported credential placeholders in inspected REST text bodies. | +| `websocket-credential-rewrite` | Rewrite supported placeholders in client WebSocket text messages on REST compatibility or WebSocket endpoints. | +| `allow-uninspected-credentials` | Accept the security-sensitive exposure of provider credentials on an uninspected traffic path. | + +Read-only HTTP access for curl: + +```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 +``` + +Native PostgreSQL access for psql: + +```shell +openshell policy update my-sandbox \ + --rule-name postgres \ + --binary /usr/bin/psql \ + --add-endpoint db.internal.example:5432::tcp \ + --wait +``` + +Read-only access to an internal HTTP API with an explicit destination IP allowance: + +```shell +openshell policy update my-sandbox \ + --rule-name private_api \ + --binary /usr/bin/curl \ + --add-endpoint 'api.internal.example:443:read-only:rest:enforce:allowed-ip=10.20.0.0/16' \ + --wait +``` + +The empty access segment before `tcp` is required by the positional grammar. +`protocol: tcp` rejects access, enforcement, and application-inspection options. +The standard runtime initializes policy DNS and transparent capture before the +workload starts, so a live update can add the first TCP endpoint. An alternate +backend must advertise the required capability and a ready substrate. + +An inspected REST or WebSocket endpoint needs an allow shape. For incremental +creation, supply an access preset. Create the endpoint in one command, then add +explicit allow or deny rules in a separate command. + +## L7 Rule Specification + +`--add-allow` and `--add-deny` use this grammar: + +```text +host:port[,port...]:METHOD:path_glob +``` + +The host, complete port set, rule name, complete binary set, and optional +endpoint path identify one existing REST or WebSocket endpoint. These flags do +not create an endpoint or change its scope. + +Quote specifications that contain `*`, `**`, `?`, or bracket classes. Request +path globs are not shell path globs. Both `*` and `**` can cross `/` boundaries, +`?` matches one character, and bracket classes are supported. + +The following examples target an existing `github_api` rule whose only binary +is `/usr/bin/gh` and whose REST endpoint is `api.github.com:443`. An allow append +permits POST requests to issue-creation paths: + +```shell +openshell policy update my-sandbox \ + --rule-name github_api \ + --binary /usr/bin/gh \ + --add-allow 'api.github.com:443:POST:/repos/*/issues' \ + --wait +``` + +A deny append blocks POST requests to administration paths on that endpoint: + +```shell +openshell policy update my-sandbox \ + --rule-name github_api \ + --binary /usr/bin/gh \ + --add-deny 'api.github.com:443:POST:/admin/**' \ + --wait +``` + +For an existing `realtime` rule with only `/usr/bin/node` and a WebSocket +endpoint at `realtime.example.com:443`, a text-message deny uses the +`WEBSOCKET_TEXT` method. The path matches the stored upgrade request path, not +message content: + +```shell +openshell policy update my-sandbox \ + --rule-name realtime \ + --binary /usr/bin/node \ + --add-deny 'realtime.example.com:443:WEBSOCKET_TEXT:/v1/admin/**' \ + --wait +``` + +Use `--endpoint-path` when a rule contains multiple endpoints with the same host +and ports. This selector identifies the endpoint. It is separate from the +request path appended by `--add-allow` or `--add-deny`. + +## Complete Scope Requirements + +A network rule grants each listed binary access to each listed endpoint and +port, subject to application rules and other checks. A rule with two binaries +and two endpoints therefore includes four connection pairs: + +| Binary | Endpoint | +|---|---| +| `/usr/bin/curl` | `api.example.com:443` | +| `/usr/bin/curl` | `uploads.example.com:443` | +| `/usr/bin/python3` | `api.example.com:443` | +| `/usr/bin/python3` | `uploads.example.com:443` | + +If Python should reach only `api.example.com`, put that binary and endpoint in a +separate rule. Adding Python to the original rule would also authorize the +uploads endpoint. + +The CLI requires complete affected scope for L7 appends and for endpoint merges +that could create new pairs. For example, an endpoint that stores ports 443 and +8443 must be targeted with `443,8443`, even if the new method was observed only +on 443. Likewise, repeat every stored binary path or use `--any-binary` only +when the stored rule actually has an empty binary list. + +Inspect the stored base before composing an append: + +```shell +openshell policy get my-sandbox --base +``` + +Copy the rule name, binary paths, endpoint path, and complete port set from that +view. Do not derive merge scope from `--full` when provider-owned rules are +present; those rules are not part of the sandbox base you can incrementally +edit. + +The gateway rejects incomplete or ambiguous declarations before revision +persistence. Read the reported expected scope and correct the command. Do not +fill scope mechanically without confirming that the broader effect matches your +intent. + +## Remove Permissions + +Preview endpoint removal before applying it because `--remove-endpoint` is not +scoped by `--rule-name`. It removes the matching host and port from every +authored network rule: + +```shell +openshell policy update my-sandbox \ + --remove-endpoint api.example.com:443 \ + --dry-run + +openshell policy update my-sandbox \ + --remove-endpoint api.example.com:443 \ + --wait +``` + +To remove one named rule instead: + +```shell +openshell policy update my-sandbox \ + --remove-rule github_readonly \ + --wait +``` + +Removing an endpoint from a multi-port endpoint removes only the named port. +When removal leaves an endpoint with no ports, OpenShell removes that endpoint. +When a rule loses its final endpoint, OpenShell removes the rule instead of +retaining an empty entry. Use `--remove-rule` when you intend to remove one +specific map entry, and inspect the resulting base policy after either command. + +## Preview a Merge + +`--dry-run` shows the proposed policy without saving it. For example, this +command previews a request-rule change to an existing `github_api` rule with +only `/usr/bin/gh` and a REST endpoint at `api.github.com:443`: + +```shell +openshell policy update my-sandbox \ + --rule-name github_api \ + --binary /usr/bin/gh \ + --add-allow 'api.github.com:443:GET:/repos/**' \ + --dry-run +``` + +The command validates argument shapes, connects to the gateway, fetches the +current sandbox configuration, and applies the merge locally. It creates no +revision. An unavailable gateway therefore causes a connection failure. The +preview does not establish that a later submission will pass every effective +policy, provider, credential, or runtime validation. + +## Merge and Concurrency Behavior + +All compatible flags in one command form one atomic batch. They succeed or fail +together and persist at most one revision. `--add-endpoint` cannot share a batch +with `--add-allow` or `--add-deny` because their scope flags have different +meanings. + +Concurrent writers use optimistic retry. The gateway reapplies the complete +operation batch to the latest revision and validates the result again. A no-op +merge reports an unchanged version and creates no revision. + +Rule names identify stored map entries for update and removal. The optional +human-readable `name` inside a YAML rule is not the `--rule-name` selector when +the two differ. Use the map key shown by `policy get --base`. + +Incremental updates are unavailable while a gateway-global policy is active. +Delete the global override through the operator workflow before changing a +sandbox policy. + +## Wait and Status Semantics + +Without `--wait`, success means the gateway accepted the submission. It does not +mean the supervisor activated it. + +With `--wait`, the CLI polls until it observes a terminal outcome or timeout. A +zero exit can represent a loaded revision, a no-op, or a revision superseded by +another update. Always inspect current state before relying on the change: + +```shell +openshell policy list my-sandbox +openshell policy get my-sandbox --full +``` + +A wait timeout means the CLI stopped polling. It does not establish whether the +revision later loaded or failed. Inspect revision status and sandbox readiness +before resubmitting, because an automatic retry can race with a late result. + +## Full Replacement + +`openshell policy set` handles middleware changes, advanced protocol shapes, and complete +replacement. Start from the current base and preserve sections you do not intend +to change. Follow [Replace the +Policy](/sandboxes/policies#replace-the-policy) to prepare an editable +YAML file using the CLI's readable output. + +For automation, the following alternative uses `jq` on the host to extract the +base policy without display metadata or provider-owned rules: + +```shell +set -o pipefail +openshell policy get my-sandbox --base --output json \ + | jq -e '.policy' > base-policy.json +``` + +Edit `base-policy.json`, then submit the complete policy: + +```shell +openshell policy set my-sandbox --policy base-policy.json --wait +``` + +Refer to [Inspect the Current Policy](/sandboxes/policies#inspect-the-current-policy) +for effective export, and use +[Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) when submission +and activation results differ. diff --git a/docs/sandboxes/network-policy-recipes.mdx b/docs/sandboxes/network-policy-recipes.mdx new file mode 100644 index 0000000000..122c73143e --- /dev/null +++ b/docs/sandboxes/network-policy-recipes.mdx @@ -0,0 +1,503 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +title: "Configure Network Policy Rules" +sidebar-title: "Network Policy Recipes" +description: "Choose and configure REST, native TCP, WebSocket, GraphQL, MCP, and JSON-RPC policy rules." +keywords: "Generative AI, Cybersecurity, Policy, Network, REST, TCP, WebSocket, GraphQL, MCP, JSON-RPC" +position: 7 +--- + +Network rules combine a destination and executable with optional +application-request checks. Each recipe on this page illustrates a permission +for a particular protocol and how to verify it. Choose the recipe that matches +your service and adapt it to your sandbox's policy. + +## Choose a Protocol + +OpenShell first checks whether a program can connect to a host and port +(Layer 4). For inspected protocols, it then checks the application request +(Layer 7). + +| Reader goal | `protocol` | Inspected boundary | +|---|---|---| +| Allow HTTP methods and paths | `rest` | HTTP requests. | +| Connect a database or other native client | `tcp` | Connection only, without payload inspection. | +| Control an RFC 6455 client | `websocket` | Upgrade request and supported client messages. | +| Control GraphQL operations | `graphql` | GraphQL-over-HTTP operations. | +| Control MCP methods and tools | `mcp` | MCP Streamable HTTP client requests. | +| Control generic JSON-RPC methods | `json-rpc` | JSON-RPC-over-HTTP client requests. | + +If you omit `protocol`, OpenShell does not apply protocol-specific request rules. +This does not disable the proxy's TLS handling or HTTP destination checks. Use +`protocol: tcp` for a native socket, or `tls: skip` when a client needs a raw +stream such as client-certificate mTLS. Provider-credentialed endpoints reject +uninspected traffic unless you explicitly set +`allow_uninspected_credentials: true`. + +## Understand Inspected HTTP Flow + +Inspected HTTP requests pass through several independent checks. + +```mermaid +flowchart TD + A["Sandbox tool"] --> B["Check destination
and process identity"] + B --> C["Check application request rules"] + C --> D["Run selected request middleware
Recheck transformed operations"] + D --> E["Resolve permitted credentials"] + E --> F["Upstream service"] + F --> G["Run selected response middleware"] + G --> A +``` + +Network rules check the destination and the calling program before allowing a +connection. Protocol rules then check the application request. Middleware can +filter or transform allowed traffic, and credential checks determine whether +OpenShell can supply credentials to the destination. See the recipes below for +native TCP and WebSocket boundaries that differ from ordinary inspected HTTP +requests. + +## Apply a Recipe + +Each recipe can be applied independently. Its YAML supplies an entry for the +`network_policies` section of a complete policy file. The commands use +`my-sandbox` as a placeholder for your running sandbox's name. Before applying +a recipe, confirm that the sandbox contains its client executable and that the +destination service is available. Replace example domains with services you +control. + +Start with the current base policy so you preserve startup settings and exclude +provider-owned rules: + +```shell +openshell policy get my-sandbox --base +``` + +Copy the YAML below the `---` separator into `policy.yaml`, leaving out the +status metadata above it. Add the recipe to the `network_policies` section, +creating that section if needed. Replace example hosts, paths, and binaries +with your own values. Each recipe supplies a network rule; keep the other +sections of your complete policy file. + +Keep unrelated rules, and replace an existing rule with the same key only when +that is your intent. Review the complete file, then apply it and confirm which +revision is active: + +```shell +openshell policy set my-sandbox --policy policy.yaml --wait +openshell policy list my-sandbox +openshell policy get my-sandbox --full +``` + +For changes supported by `openshell policy update`, [incremental +updates](/reference/policy-updates) preserve the other policy sections without +requiring a complete replacement file. A successful parse or submission +does not prove that the runtime activated the policy, so inspect revision status +before testing traffic. + +## Allow REST Reads + +Use `protocol: rest` to match HTTP methods, paths, and optional query values. +Set `enforcement: enforce` whenever the rule must block a request. + +Insert this entry under `network_policies`: + +```yaml +github_readonly: + name: github-readonly + endpoints: + - host: api.github.com + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: /usr/bin/curl +``` + +[Apply this rule](#apply-a-recipe), then verify an allowed GET and a denied +POST from a sandbox with `/usr/bin/curl` installed: + +```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 operation preset, not a guarantee that an upstream GET has no side effect. + +For explicit rules, `*` in a request path can cross `/` boundaries. Quote globs +in shell commands so the local shell does not expand them. + +### Observe a Rule Before Enforcing It + +An endpoint with `enforcement: audit` logs applicable request-rule violations +and forwards the requests. For a REST endpoint with `access: read-only`, a POST +therefore produces a violation event but still reaches the upstream service if +the other checks allow it. The resulting HTTP status comes from that service. +Choose audit mode when you intend to observe violations without blocking them. + +Inspect the policy event without a WARN filter: + +```shell +openshell logs my-sandbox --since 5m --source sandbox +``` + +Look for an event identifying the request-policy violation even though the +request was forwarded. OCSF policy events are INFO-level tracing records; their OCSF +severity does not change the CLI's tracing-level filter. Audit does not bypass +destination, executable, credential, parser, or middleware checks. An endpoint +must use `enforcement: enforce` for its request rules to block traffic. + +## Scope GitHub API Access to a Repository + +An agent using the GitHub CLI may need repository API operations in addition to +Git clone, fetch, or push. This rule permits `gh` to make REST requests under +one repository's API path. Replace `` and `` with its owner and name, +and keep only the executable paths used by your image: + +```yaml +github_repository_api: + name: github-repository-api + endpoints: + - host: api.github.com + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: + method: "*" + path: "/repos///**" + binaries: + - path: /usr/bin/gh + - path: /usr/local/bin/gh +``` + +The rule grants all HTTP methods within that path, including writes. Narrow the +methods and paths if the agent needs only specific operations. It does not +grant Git transport access or GraphQL mutations. + +[Apply the rule](#apply-a-recipe) with a GitHub provider attached. Its profile +must permit credential use at the destination, and the token must authorize the +repository operation. Provider-contributed read permissions remain available. +Verify a permitted operation on the selected repository and confirm that a +write to a disposable second repository receives an OpenShell denial. Use `gh` +for both checks so the requests use the executable selected by this rule. + +For a complete Git push example, see +[Grant GitHub Push Access to a Sandboxed Agent](/get-started/tutorials/github-sandbox). + +## Match Query Values + +REST query matchers run on decoded, case-sensitive values. This adaptable +template requires `/usr/bin/curl` and an HTTP service you control. Replace the +hostname, path, and accepted query values before applying it: + +```yaml +download_query: + name: download-query + 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 +``` + +For an allow rule, every value for a duplicate query key must match. For a deny +rule, every configured key must be present with at least one matching value; an +additional nonmatching duplicate does not cancel a matching value. Start with a +single query key when possible. [Apply the template](#apply-a-recipe), then test +the intended match and near misses against your service. + +## Allow Native TCP + +Use `protocol: tcp` when the application needs ordinary DNS resolution and a +native TCP socket. The rule checks the hostname, port, and executable but cannot +inspect the application payload. + +Insert this entry under `network_policies`: + +```yaml +postgres: + name: postgres + endpoints: + - host: db.internal.example + port: 5432 + protocol: tcp + binaries: + - path: /usr/bin/psql +``` + +Do not add `access`, `enforcement`, request rules, or credential-rewrite fields +to a TCP endpoint. This template requires `/usr/bin/psql` and a PostgreSQL +service reachable at the replacement hostname. [Apply the +template](#apply-a-recipe), verify that `psql` reaches the server, then verify +that another binary or port is denied at the connection boundary. + +Docker and Podman support native TCP enforcement. The standard runtime prepares +DNS and TCP connection handling before the workload starts, even when the +initial policy has no TCP endpoints. You can therefore add the first endpoint +without recreating the sandbox. Other runtimes must provide the same support +and have it ready before activating a TCP rule. + +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 does not inspect the payload's authority. + +For a connection-only check, use a client command that fails before application +authentication when the policy is absent, then succeeds far enough to reach the +server when the rule is present. A database authentication error can demonstrate +that the TCP connection reached the server, but it does not prove that database +credentials are correct. After removing the rule, confirm the client cannot +reach the server and inspect the sandbox log for the denied hostname and binary. + +Policy DNS returns a supervisor-owned synthetic address with a bounded TTL. +Clients must honor that TTL and resolve the hostname again before reconnecting; +a client that caches the synthetic address indefinitely can fail after its +mapping expires even while the endpoint remains allowed. Docker and Podman +currently advertise IPv4 egress for policy DNS, so AAAA queries return a +successful empty answer and dual-stack clients must continue with the A record. + +`allowed_ips` can constrain the addresses accepted for a hostname, but it does +not replace hostname authorization. Account for DNS rotation and service +failover before pinning addresses. Review both the hostname and resolved address +in diagnostics instead of broadening the wildcard when an address changes. + +## Allow WebSocket Messages + +Use `protocol: websocket` for an RFC 6455 upgrade and client-to-server message +policy. This adaptable 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: + name: 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 +``` + +[Apply the template](#apply-a-recipe), then 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 upstream-to-client messages. Provider-credentialed endpoints remain +on the parsed relay and reject binary frames unless +`allow_uninspected_credentials: true` explicitly accepts the weaker boundary. +Set `websocket_credential_rewrite: true` only when client text messages contain +OpenShell credential placeholders that must be resolved. + +## Allow GraphQL Operations + +Use `protocol: graphql` for GraphQL-over-HTTP. This template requires +`/usr/bin/gh`, a GitHub credential with the required permissions, and the GitHub +GraphQL schema. It allows selected queries and `createIssue`, while denying +`deleteRepository`: + +```yaml +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 + fields: [createIssue] + deny_rules: + - operation_type: mutation + fields: [deleteRepository] + binaries: + - path: /usr/bin/gh +``` + +[Apply the template](#apply-a-recipe), then test an allowed query and the denied +mutation with the configured client. For allow rules, every selected root field +must match. For deny rules, one matching root field blocks the request. A +malformed, denied, or unregistered operation denies an entire batched HTTP +request. + +GraphQL field names are application-specific. Do not treat a copied field list +as a verified safety boundary. Review and test it against the authoritative +schema for the deployed service version. Hash-only persisted queries require +`persisted_queries: allow_registered` and a trusted +`graphql_persisted_queries` registry. + +For GraphQL-over-WebSocket, use `protocol: websocket`, allow the upgrade with a +GET rule, and add GraphQL operation rules for client operation messages. Client +operation messages fail closed when malformed or disallowed. Lifecycle messages +such as `connection_init`, `ping`, `pong`, and `complete` are allowed without +payload logging. + +## Allow MCP Tools + +Use `protocol: mcp` for sandbox-to-server MCP Streamable HTTP requests. This +adaptable template requires `/usr/bin/python3` with an MCP client and a +Streamable HTTP server you control. It allows initialization, tool discovery, +and `read_status`, while denying `delete_resource`: + +```yaml +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/list + - allow: + method: tools/call + tool: read_status + deny_rules: + - method: tools/call + tool: delete_resource + binaries: + - path: /usr/bin/python3 +``` + +[Apply the template](#apply-a-recipe), then 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` selects the exact `2025-11-25` revision. Supported +explicit values are `2025-03-26`, `2025-06-18`, and `2025-11-25`. This is an +allowlist, not a date range or moving `latest`. OpenShell checks one +`MCP-Protocol-Version` header on each request except a valid standalone +`initialize`; it does not negotiate every revision's complete wire profile or +bind server session state. Server responses and SSE messages are relayed without +MCP policy parsing. + +## Allow JSON-RPC Methods + +Use `protocol: json-rpc` for non-MCP JSON-RPC-over-HTTP. This adaptable template +requires `/usr/bin/python3` with a JSON-RPC client and an HTTP service you +control. It allows `reports.list` and `reports.search`, while denying +`reports.delete`: + +```yaml +reports_rpc: + name: reports-rpc + endpoints: + - host: rpc.example.com + port: 443 + path: /rpc + protocol: json-rpc + enforcement: enforce + rules: + - allow: + method: reports.list + - allow: + method: reports.search + deny_rules: + - method: reports.delete + binaries: + - path: /usr/bin/python3 +``` + +[Apply the template](#apply-a-recipe), then verify `reports.list` succeeds and +`reports.delete` is denied. Method names are exact. Only `method: "*"` is +accepted as an all-method sentinel; other globs are rejected. Parameter matchers +are not supported. OpenShell evaluates every call in a batch and denies the full +batch if one call is denied. Server-to-client messages are not parsed for policy +enforcement. + +## Understand Overlapping Rules + +Network rules are not an ordered firewall list. More than one compatible rule +can match a connection or request, and matching allow rules can contribute +permission. An applicable request deny takes precedence over an allow. The +endpoint parser selected for a connection is a separate choice from whether the +parsed operation is allowed. + +For example, suppose one rule permits `GET +/repos/**` and another matching rule denies +`GET /repos/private/**` for the same host, port, and executable. The private +path is denied regardless of which rule appears first in YAML. Keep related +exceptions close enough to review together. + +For a sandbox configured with those two rules, the following requests test the +overlap. Replace `api.example.com` and the paths with routes on an HTTP service +you control: + +```shell +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/curl --silent --show-error --fail \ + https://api.example.com/repos/public/project +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/curl --silent --show-error \ + https://api.example.com/repos/private/project +``` + +For a service configured with those routes, the first request should reach the +service and the second should return an OpenShell policy denial. When unexpected access remains, +compare `policy get --base` with `policy get --full`: provider-contributed rules +appear only in the effective view, and a gateway-global policy replaces normal +sandbox/provider composition. + +## Keep Credentials Separate from Network Access + +A matching network rule permits traffic; it does not authorize every provider +credential at that destination. Credential bindings, request placeholders, and +the inspected transport determine whether OpenShell may inject a value. If a +connection succeeds but credential resolution fails, inspect the provider and +binding diagnostics instead of granting a broader endpoint or setting +`allow_uninspected_credentials`. + +Request-body and WebSocket credential rewriting apply only to their documented +text formats and size limits. Binary payloads and unsupported message +directions are not made safe by an allow rule. See [Provider +Profiles](/providers/profiles) for binding concepts and the [Policy Schema +Reference](/reference/policy-schema) for endpoint fields. + +## Next Steps + +- Use the [Policy Schema Reference](/reference/policy-schema) for protocol + defaults, field constraints, and matcher semantics. +- Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) to + separate connection, request, credential, middleware, and activation errors. From c7b2fb10b8b59d624f2ac80eb1b99f3b1b352b05 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 16:03:30 +0000 Subject: [PATCH 03/40] docs(policy): organize lifecycle guidance and troubleshooting Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 18 +- docs/how-it-works/policies/overview.mdx | 1135 +++++----------------- docs/sandboxes/troubleshoot-policies.mdx | 241 +++++ 3 files changed, 495 insertions(+), 899 deletions(-) create mode 100644 docs/sandboxes/troubleshoot-policies.mdx diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index b6f7acd0af..4c9895dd7a 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -5,7 +5,7 @@ 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." keywords: "Generative AI, Cybersecurity, Policy Advisor, Policy, Sandbox, policy.local, Agent Policy" -position: 7 +position: 9 --- 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. @@ -96,7 +96,7 @@ When policy advisor is enabled, the sandbox supervisor turns on three agent-faci - 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 loop has seven steps: +The loop has eight steps: 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`. @@ -191,7 +191,7 @@ 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. +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 [Configure Sandbox Policies](/sandboxes/policies) or [Policy Schema Reference](/reference/policy-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. @@ -272,7 +272,11 @@ If policy advisor is disabled, every route returns `404 feature_disabled`, the s ## What to Expect -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). +Approved network rules hot-reload without restarting the sandbox. Connections +attached to the previous policy generation close at the reload boundary, so a +retry opens against the current generation. Refer to +[Understand Reloads](/sandboxes/policies#understand-reloads) for HTTP, +WebSocket, tunnel, and long-lived stream behavior. Policy advisor emits audit events into the sandbox log. Use these lines to trace the full loop: @@ -284,6 +288,8 @@ Look for `HTTP:* DENIED`, `CONFIG:PROPOSED`, `CONFIG:APPROVED` or `CONFIG:REJECT ## 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 [Configure Sandbox Policies](/sandboxes/policies) for manual policy updates and L7 rule syntax. +- Use [Policy Schema Reference](/reference/policy-schema) for full YAML field details. +- Use the [Standalone Policy Prover](/reference/policy-prover) for optional + boundary checks and its explicit modeled-domain limits. - Use [Logging](/observability/logging) to interpret OCSF shorthand log entries. diff --git a/docs/how-it-works/policies/overview.mdx b/docs/how-it-works/policies/overview.mdx index 9b9d2a45e1..667a50b24e 100644 --- a/docs/how-it-works/policies/overview.mdx +++ b/docs/how-it-works/policies/overview.mdx @@ -1,974 +1,323 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Customize Sandbox Policies" +title: "Configure Sandbox Policies" sidebar-title: "Overview" -description: "Apply, iterate, and debug sandbox network policies with hot-reload on running OpenShell sandboxes." +description: "Allow specific sandbox actions, inspect effective permissions, and apply policy changes safely." keywords: "Generative AI, Cybersecurity, Policy, Network Policy, Sandbox, Security, Hot Reload" position: 6 --- -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). +Sandbox policies define what programs can access and which actions they can take +within an OpenShell sandbox. OpenShell uses declarative YAML configurations to +specify which files programs can access, which network destinations they can +reach, and which application requests they can make. -## Policy Structure +This guide explains how to inspect, change, and verify those permissions. -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. +## Policy Configuration -```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. +The policy configuration file groups related settings into sections, each +controlling a different aspect of how programs run and interact with their +environment. -## Supervisor Middleware +Filesystem settings define where programs can read and write, and process +settings select the user and group they run as. Network rules define which +services each program can reach and which requests it can send. Optional +middleware can inspect or transform allowed traffic, for example to redact +sensitive content from a request. -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`. +The configuration contains the following policy sections: ```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" +version: 1 +filesystem_policy: { ... } # Paths programs can read or write. +landlock: { ... } # Linux filesystem enforcement settings. +process: { ... } # User and group for sandbox processes. +network_policies: { ... } # Network destinations and permitted requests. +network_middlewares: { ... } # Additional traffic processing. ``` -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: +Refer to the [Policy Schema Reference](/reference/policy-schema) for every +accepted field. -- 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. +## Apply Policy Changes -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. +Every sandbox runs under a policy, but you do not have to supply a policy file +when creating one. OpenShell uses an existing policy or falls back to its +restrictive default. See [How OpenShell Selects a Policy](#how-openshell-selects-a-policy) +for the selection order. -### Update Commands +You can update network rules while a sandbox is running. Changes to filesystem +permissions and process identity require recreating the sandbox to take effect. +See [Understand Reloads](#understand-reloads) for the limits on each type of +change. -The incremental update surface is split into endpoint-level operations and method/path rule-level operations for REST and WebSocket endpoints. +The OpenShell CLI provides three ways to apply changes. Choose based on whether +you are creating a sandbox, changing network permissions, or replacing its +configuration: -| Flag | What it changes | Typical use | +| Command | Use it when | Important behavior | |---|---|---| -| `--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. +| `openshell sandbox create --policy` | You know the complete policy before creation. | The file supplies the sandbox's initial policy and can set startup-time controls. | +| `openshell policy update` | You need to add or remove network permissions. | It changes only `network_policies` and preserves every other section. | +| `openshell policy set` | You need to replace the complete policy, including middleware or startup settings. | Start from the current base so you preserve required startup settings and exclude provider-owned rules. | -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. +### Inspect the Current Policy -If you do not pass `--rule-name`, OpenShell generates one from the host and port, such as `allow_api_github_com_443`. +The base policy is the policy configured for the sandbox. Attached providers can +contribute additional network rules; the effective policy includes those rules +alongside the base policy. Start sandbox policy edits from the base view so you +do not copy provider-owned rules into your configuration. -### 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. +In the command examples, replace `my-sandbox` with the name of your sandbox: ```shell -openshell policy update demo \ - --add-endpoint api.github.com:443:read-only:rest:enforce \ - --binary /usr/bin/gh \ - --wait +openshell policy get my-sandbox --base +openshell policy get my-sandbox --full ``` -This creates a REST endpoint and sets its base allow behavior through the `read-only` access preset. +Check the effective view's `Source` field for a gateway-global override, which +blocks sandbox policy changes. Review all rules that could allow the operation +you intend to restrict. Matching rules can contribute permissions together, and +an applicable request deny takes precedence over an allow. See +[Understand Overlapping Rules](/sandboxes/network-policy-recipes#understand-overlapping-rules) +for the matching rules. -#### Add one more REST allow rule - -Use `--add-allow` after the REST endpoint already exists. +For review or a [policy containment check](/reference/policy-prover), export the +effective policy as YAML: ```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 +openshell sandbox get my-sandbox --policy-only > effective-policy.yaml ``` -This keeps the existing endpoint definition and appends one new allow rule. It does not add binaries or change the endpoint host and port. +The effective export can contain provider-owned rules. Prepare edits from the +base view, as described in [Replace the Policy](#replace-the-policy). -#### Add a REST deny rule under an allowed endpoint +### Update Network Rules -Use `--add-deny` when you want to carve out a blocked subtree under an existing REST endpoint. +To give a program access to a service, add an endpoint specifying its executable +path, destination, and permitted request types. For a sandbox containing +`/usr/bin/curl`, this command adds +read-only access to the GitHub API: ```shell -openshell policy update demo \ - --add-deny 'api.github.com:443:POST:/admin/**' \ - --rule-name allow_api_github_com_443 \ - --binary /usr/bin/gh \ +openshell policy update my-sandbox \ + --rule-name github_readonly \ + --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:enforce \ --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 +Here, `rest` enables HTTP request inspection, `read-only` permits GET, HEAD, and +OPTIONS, and `enforce` blocks requests outside that set. The default enforcement +mode is `audit`, which logs an applicable request-rule violation but forwards the +request. Other matching rules can also grant permissions, so inspect the +effective policy before relying on a restriction. -Use `--add-endpoint` with `protocol: websocket` when the destination is an RFC 6455 WebSocket API. +The binary path must match the executable inside the sandbox. For other +protocols and their YAML configuration, see +[Configure Network Policy Rules](/sandboxes/network-policy-recipes). -```shell -openshell policy update demo \ - --add-endpoint realtime.example.com:443:read-write:websocket:enforce:websocket-credential-rewrite \ - --binary /usr/bin/node \ - --wait -``` +For changes to an existing rule, take its name, binaries, and endpoint details +from the base policy. Request-rule edits require the complete affected scope so +the command identifies exactly which permissions will change. You can also +remove one named rule or remove a destination across rules. -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. +To preview an incremental operation, use `--dry-run` instead of `--wait`. The CLI +fetches the current policy and shows the proposed result without saving it. This +preview does not run every check needed to activate the change. The +[Incremental Policy Update Reference](/reference/policy-updates) covers command +syntax, targeting requirements, and removal behavior. -#### Add a WebSocket text-message deny rule +### Replace the Policy -Use `WEBSOCKET_TEXT` when you want to refine client-to-server text-frame policy without matching message payload content. +A full replacement supplies every section of the policy, including settings +you intend to keep. Begin with the current base: ```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 +openshell policy get my-sandbox --base ``` -This adds a deny rule to the existing WebSocket endpoint. The path glob matches the WebSocket upgrade path. +The base view prints status metadata followed by a `---` separator and the policy +in YAML. Copy only the YAML below the separator into `policy.yaml`. Keep the +complete policy, including its filesystem, Landlock, process, and unrelated +network settings, then edit the sections you intend to change. Do not redirect +the entire output into a policy file, because the metadata is not part of the +policy. -#### Remove one endpoint or rule - -Use `--remove-endpoint` to remove one host and port pair, or `--remove-rule` to delete the whole named rule. +Submit the edited file with `--wait` to wait for its revision result: ```shell -openshell policy update demo --remove-endpoint pypi.org:443 --wait -openshell policy update demo --remove-rule github_repos --wait +openshell policy set my-sandbox --policy policy.yaml --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. +The new policy must pass validation before becoming active. Changes to startup +settings remain subject to the limits in [Understand Reloads](#understand-reloads). -This means: +To restore an earlier compatible base revision, follow [Restore an Earlier Base +Revision](/sandboxes/troubleshoot-policies#restore-an-earlier-base-revision). +For scripted exports, the command reference includes an optional +[JSON workflow](/reference/policy-updates#full-replacement) using `jq`. -- 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. +### Verify the Change -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. +A successful submission means the gateway accepted the change. With `--wait`, +the CLI also waits for a revision result, but success can mean the revision +loaded, made no change, or was superseded by another update. Inspect policy +history and the effective configuration before relying on new permissions: ```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 +openshell policy list my-sandbox +openshell policy get my-sandbox --full ``` -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. +Then test an operation that should be allowed and one that should be blocked. +Confirm any denial comes from OpenShell rather than the destination service or +a missing client tool. The [first network policy +tutorial](/get-started/tutorials/first-network-policy) demonstrates these checks +in a prepared sandbox. -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. +## Understand Reloads -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. +OpenShell validates a proposed policy before activating it. Network rules and +the policy's middleware settings can change while the workload runs. Filesystem +and process settings take effect at startup. External middleware registration is +a separate gateway operation. -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 -``` +| Change | Running workload behavior | Required action | +|---|---|---| +| Network request rules | The running workload receives the new rules. Connections using the previous active configuration close. | Use `update` or `set`, inspect status, and retry. | +| Middleware selected or configured in `network_middlewares` | You can add, remove, or reconfigure built-in middleware and external services already registered with the gateway. | Preserve the base and use `set`; `update` does not edit middleware. | +| External middleware registration in gateway TOML | Policy updates cannot register a new service or change its gateway connection settings. | Update the registration and restart the gateway. | +| First native TCP endpoint | The standard runtime can add it while the workload runs because policy DNS and transparent capture are already available. | Use `update` or `set`, inspect status, and retry. | +| Added filesystem paths | The saved policy can change, but the running workload keeps its existing filesystem permissions. | Recreate the sandbox to apply the new startup configuration. | +| Removed baseline paths or changed workdir, Landlock, or process identity | OpenShell rejects the change after the workload starts. | Recreate the sandbox with the intended startup configuration. | +| Repair before first activation | If the workload has never started, you can replace rejected startup settings. | Repair them before provisioning times out. | + +OpenShell calls each version of the active configuration a generation. When a +new generation becomes active, OpenShell closes HTTP keep-alive connections, +tunnels, upgrades, and long-lived streams that still use the previous one. A +parsed WebSocket relay closes with code `1012`. The client must reconnect so its +next request uses the new configuration. -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. +```mermaid +flowchart TD + A["Review the proposed change"] --> B["Submit it to the gateway"] + B --> C{"Gateway accepts it?"} + C -->|No| D["Do not save the change
Keep the current configuration"] + C -->|Yes| E["Runtime checks the complete configuration"] + E --> F{"Can it become active?"} + F -->|Yes| G["Make it active
Close connections using the old version"] + F -->|No| H["Use the configured failure mode
or wait for startup repair"] + G --> I["Check status and retry the request"] + D --> A + H --> A +``` + +A no-op update may create no new revision. If validation fails, the effect on +existing access depends on the failure stage and the configured runtime failure +mode. [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) explains +how to identify that stage and repair the configuration. + +## How OpenShell Selects a Policy + +When more than one policy is available, OpenShell uses the first applicable +policy in this order: + +1. A gateway-global policy set by an administrator. +2. The sandbox's saved policy. At creation, `--policy` takes precedence over + `OPENSHELL_SANDBOX_POLICY`; subsequent policy changes update the saved policy. +3. A policy included in the sandbox image. +4. OpenShell's [restrictive default policy](/reference/default-policy). + +OpenShell then adds network rules from attached providers, unless a +gateway-global policy is active. A global policy replaces the sandbox's policy +and suppresses provider-added rules, as described below. + +An invalid image policy must be repaired before the workload can start; +OpenShell does not skip it and use the default. See +[Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) for repair guidance. ## 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. +A gateway administrator can apply one policy across the gateway's sandboxes. +This global override replaces each sandbox policy; it is not a ceiling +intersected with existing grants. While it is active, sandbox policy updates +are blocked and provider-added network grants +are suppressed. Credential authorization remains a separate check, so a network +grant alone does not make a provider credential usable at a destination. -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 -``` +Deleting the global override restores normal sandbox selection and provider +composition. The gateway rejects restoration when the resulting effective +configuration is invalid. Inspect effective policy before, during, and after an +operator changes the override. -## Debug Denied Requests +The OpenShell CLI provides separate operations for managing the override: -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 } -``` +| Operation | Command | +|---|---| +| Inspect the current override. | `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 override and restore normal policy selection. | `openshell policy delete --global` | -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 } -``` +The set and delete commands ask for confirmation unless you pass `--yes`. Keep +the prompt during interactive work because each operation changes the network +model for every sandbox on the gateway. -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. +## Allow Native TCP Connections -### GraphQL service policy shapes +Some applications, such as database clients, need a native connection rather +than an HTTP proxy. `protocol: tcp` provides ordinary DNS resolution and a native +TCP socket without application payload inspection. It supports Docker and +Podman. The standard runtime prepares DNS and TCP connection handling even when +the initial policy has no TCP endpoints, so you can add the first endpoint +without recreating the sandbox. Refer to +[Configure Network Policy Rules](/sandboxes/network-policy-recipes#allow-native-tcp) +for the recipe and shared-hosting limitations. -GraphQL field names are application-specific, so treat these as starting shapes to review against the actual app schema: +## Supervisor Middleware -| 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. | +For traffic that needs additional inspection or transformation, +`network_middlewares` selects middleware, the destination hosts it applies to, +and its configuration. Network rules must allow the traffic +before middleware can process it. + +Middleware can inspect HTTP requests before credential injection, HTTP responses +before they reach the sandbox, and complete client WebSocket text messages. Each +implementation declares which operations it supports; matching a destination +host does not enable unsupported operations. Binary and upstream-to-client +WebSocket messages are not inspected. + +Built-in middleware is available without registration. External middleware must +be registered in the gateway configuration before a policy can use it. Once it +is registered, a policy change can add it to or remove it from a running sandbox, +or change its policy settings. Adding or changing the gateway registration +requires restarting the gateway. Refer to +[Supervisor Middleware](/extensibility/supervisor-middleware) for registration, +processing order, supported transformations, and failure behavior. + +## Validation Failures + +A local parse error, gateway preflight rejection, initial configuration failure, +runtime rejection, wait timeout, credential denial, and middleware denial have +different effects. Do not widen endpoint access to repair a credential or +middleware problem. + +Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) to identify +the stage, whether the previous policy remains active, and the corrective action. +Gateway failure-mode settings are defined in +[Gateway Configuration](/reference/gateway-config). ## 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 [Configure Network Policy Rules](/sandboxes/network-policy-recipes) for + REST, native TCP, WebSocket, GraphQL, MCP, and JSON-RPC recipes. +- Use the [Policy Schema Reference](/reference/policy-schema) for fields, + defaults, matching rules, and constraints. +- Use [Policy Advisor](/sandboxes/policy-advisor) to let a sandbox propose a + narrow network change for review. +- Use the [Standalone Policy Prover](/reference/policy-prover) as an optional + containment check. Its result covers only the modeled domains and does not + apply policy or attest runtime state. diff --git a/docs/sandboxes/troubleshoot-policies.mdx b/docs/sandboxes/troubleshoot-policies.mdx new file mode 100644 index 0000000000..064a4817b0 --- /dev/null +++ b/docs/sandboxes/troubleshoot-policies.mdx @@ -0,0 +1,241 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +title: "Troubleshoot Sandbox Policies" +sidebar-title: "Troubleshoot Policies" +description: "Identify policy failure stages and repair connection, request, credential, middleware, and activation problems." +keywords: "Generative AI, Cybersecurity, Policy, Troubleshooting, Sandbox, Validation, Credentials, Middleware" +position: 8 +--- + +Policy failures can occur while submitting a change, activating it, or processing +traffic. The sandbox's state, policy history, and logs help identify which stage +failed and whether the previous policy is still active. Start with that evidence +before changing permissions. + +## Identify the Failure Stage + +The following OpenShell CLI commands show the information needed to locate a +failure. Replace `my-sandbox` with the affected sandbox's name, then use the table +to find the relevant diagnosis: + +```shell +openshell sandbox get my-sandbox +openshell policy list my-sandbox +openshell policy get my-sandbox --full +openshell logs my-sandbox --since 10m --source sandbox +``` + +Use what you observe and the latest revision status to locate the failure. + +| What you see | Failure stage | Existing access | What to do | +|---|---|---|---| +| The CLI reports an invalid file or argument before showing a revision. | Local command parsing | Unchanged. | Fix the file or arguments. | +| The CLI reports that the candidate was rejected and no revision was created. | Gateway validation | Unchanged. | Fix the reported schema, scope, provider, credential, or compatibility error. | +| The sandbox remains `Provisioning` with `ConfigurationInvalid`. | Initial runtime activation | No workload has activated. | Repair the effective policy or provider configuration during the repair window. | +| A revision fails and new egress stops. | Runtime activation with `fail_closed` | No. Existing connections close. | Submit a valid repair and verify recovery. | +| A revision fails but earlier network access still works. | Runtime activation with `retain_last_valid` | The last valid configuration remains active. | Repair the candidate; working access does not mean the change loaded. | +| `--wait` times out. | Status is not yet known. | Do not infer it from the timeout. | Inspect the revision and readiness before retrying. | +| The client receives `policy_denied`, a credential error, or a middleware error. | Request, credential, or middleware enforcement | The active network configuration remains. | Diagnose that control instead of widening the endpoint. | + +The gateway checks a proposed policy before saving it. The runtime checks the +resulting configuration again when the sandbox starts or reloads it, because +the runtime also sees concurrent changes and other policy sources. The gateway +setting that selects runtime rejection behavior is +`[openshell.gateway] policy_validation_failure_mode`; refer to +[Gateway Configuration](/reference/gateway-config) for its deployment contract. + +## Repair Initial Configuration + +Before first workload activation, an invalid effective policy or provider bundle +keeps the sandbox in `Provisioning` with a `ConfigurationInvalid` condition. The +gateway gives you a 300-second repair window. An effective policy, settings, +provider, profile, or attachment change resets the window from its stored change +time, and the first rejection for that change receives a full window. Repeated +failures do not extend it. + +Inspect the diagnostic: + +```shell +openshell sandbox get my-sandbox --output json +``` + +Repair the complete policy or the conflicting provider configuration. A +sandbox that has never started its workload can also replace startup settings +during the repair window. + +If the window expires, the sandbox enters `Error` with reason +`ProvisioningTimedOut`. The gateway reclaims workload and supervisor compute but +retains the sandbox record and diagnostic. Repair the configuration, wait for +cleanup, then start it explicitly: + +```shell +openshell sandbox get my-sandbox --output json +openshell sandbox start my-sandbox +``` + +A start retry receives a new 300-second window. Editing configuration after +timeout does not restart compute by itself. + +## Repair a Runtime Rejection + +The default runtime failure mode is `fail_closed`. An invalid candidate blocks +network access and closes connections that used the previously active +configuration. Submit a valid replacement, wait, and verify the active revision: + +```shell +openshell policy set my-sandbox --policy repaired-policy.yaml --wait +openshell policy list my-sandbox +``` + +With `retain_last_valid`, a previous valid configuration remains active. Do not +interpret working network access as adoption of the failed candidate. If no +previous valid configuration exists, the effective behavior is still +fail-closed. + +An OCSF configuration event reports the candidate, validation rationale, +configured and effective failure modes, the active configuration version, and +whether a previous policy remains active. + +## Diagnose Connection Denials + +A connection denial occurs before application-request inspection. Check these +dimensions in the effective policy: + +- Destination hostname and port. +- Calling executable or trusted ancestor. +- Destination IP and SSRF restrictions. +- Whether the selected runtime supports policy DNS and transparent TCP capture + when `protocol: tcp` is used. +- Whether a gateway-global policy replaced the sandbox policy. + +Use canonical executable paths. Resolve symlinks inside the sandbox when the +logged path differs from the policy: + +```shell +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + readlink -f /usr/bin/curl +``` + +Binary matching can also admit a trusted ancestor, and trusted runtime +configuration can disable identity enforcement. Inspect the logged process +identity and current runtime mode instead of assuming an empty binary list has +one universal meaning. Prefer explicit binary selectors. + +The standard runtime initializes policy DNS and transparent capture before the +workload starts, even when the initial policy has no TCP endpoints. You can add +the first TCP endpoint through a live update. If the error reports missing TCP +support or connection-handling setup, the selected runtime is not ready to +enforce native TCP rules. Changing the policy cannot supply that runtime support. + +## Diagnose Request Denials + +When the connection succeeds but OpenShell returns `policy_denied`, inspect the +protocol, enforcement mode, method or operation, request path, query values, and +endpoint path selector. + +For HTTP, the request authority must agree with the policy destination. A +non-default port in an absolute-form URL or `Host` authority must match the +transport destination. OpenShell rejects authority mismatches even when the +endpoint otherwise permits the method and path. + +Check that rules intended to block requests use `enforcement: enforce`. +Omitted enforcement is audit mode. Audit forwards an applicable request-rule violation after logging +it, but it does not bypass parsing, destination, credentials, middleware, or +other independent checks. + +Overlapping rules can contribute permissions. An applicable request deny wins +over an allow, while the most-specific compatible endpoint selects the parser +and request-processing metadata. Inspect all matching rules rather than only the +YAML block you most recently edited. + +## Diagnose Credential Denials + +Network permission and credential permission are separate. A provider +credential is usable only within its attached profile or explicit binding. + +For `credential_endpoint_mismatch`, inspect the provider profile and the request +host, port, and path: + +```shell +openshell profile export -o yaml +openshell provider get +``` + +Update a custom profile only when the destination is an intended credential +recipient. Do not widen sandbox policy to hide a binding mismatch. Refer to +[Static Credential Endpoint Binding](/providers/profiles#understand-static-credential-endpoint-binding). + +If a request body contains an invalid or revoked credential placeholder, +OpenShell can return `credential_placeholder_in_request_body`. Remove stale +placeholder text or restore the provider metadata. Do not enable body rewriting +or `allow_uninspected_credentials` merely to forward unrelated conversation +text. + +## Diagnose Middleware Denials + +Middleware runs after network rules allow the traffic. It must match the +destination host and support the operation being inspected. A deny decision or +a `fail_closed` stage failure blocks the request even when the endpoint uses +audit mode. + +If a policy names an unregistered external service, register it in the gateway +configuration and restart the gateway before using it in a policy. Built-ins and +already registered services can be added to a running sandbox through `policy set`. + +Inspect the policy-local middleware config, gateway registration, advertised +operation and phase, host selector, order, payload limits, and `on_error` value. +Request middleware runs before provider credential injection. Response +middleware runs on the final upstream response before return. WebSocket support +covers selected client text messages, not binary or upstream-to-client frames. + +Refer to [Supervisor Middleware](/extensibility/supervisor-middleware) for +registration, transport, mutation, limit, and observability details. + +## Check the Client Environment + +A missing executable or TLS trust store can cause a request to fail before +OpenShell evaluates the intended operation. Confirm the sandbox is running and +contains the required client. For a sandbox using curl with the standard Ubuntu +certificate bundle, these commands check the executable and trust store: + +```shell +openshell sandbox exec -n my-sandbox --no-login-shell -- test -x /usr/bin/curl +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + test -r /etc/ssl/certs/ca-certificates.crt +``` + +Compare the client error with a fresh event from +`openshell logs my-sandbox --since 5m --source sandbox`. A WARN +minimum level excludes OCSF entries because log filtering treats them as INFO, +regardless of their event severity. + +## Restore an Earlier Base Revision + +Choose a compatible revision from `openshell policy list my-sandbox`, then inspect its +base policy. For example, to retrieve revision 2: + +```shell +openshell policy get my-sandbox --rev 2 --base +``` + +Copy the YAML below the `---` separator into `previous-base.yaml`, excluding +the status metadata above it. Review the complete policy, then submit it as a +new revision: + +```shell +openshell policy set my-sandbox --policy previous-base.yaml --wait +openshell policy list my-sandbox +``` + +This does not restore provider profiles, attachments, credentials, or global +configuration. If a global override is active, sandbox updates remain blocked. +Deleting that override can itself be rejected when the restored sandbox and +provider composition is invalid. + +## Next Steps + +- Use [Configure Sandbox Policies](/sandboxes/policies) for the common update + and replacement workflow. +- Use the [Policy Schema Reference](/reference/policy-schema) for exact field + defaults and validation constraints. From 5259452442776b9b28d5036bf6413b72de98f15d Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 17:07:04 +0000 Subject: [PATCH 04/40] docs(policy): split policy overview into concepts and management tasks Signed-off-by: Johnny Greco --- docs/how-it-works/policies/overview.mdx | 466 +++++++++++------------- docs/sandboxes/manage-policies.mdx | 312 ++++++++++++++++ 2 files changed, 532 insertions(+), 246 deletions(-) create mode 100644 docs/sandboxes/manage-policies.mdx diff --git a/docs/how-it-works/policies/overview.mdx b/docs/how-it-works/policies/overview.mdx index 667a50b24e..1a8f2f31c8 100644 --- a/docs/how-it-works/policies/overview.mdx +++ b/docs/how-it-works/policies/overview.mdx @@ -1,199 +1,257 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Configure Sandbox Policies" +title: "Sandbox Policies" sidebar-title: "Overview" -description: "Allow specific sandbox actions, inspect effective permissions, and apply policy changes safely." +description: "Understand what sandbox policies control, how OpenShell evaluates network rules, where the active policy comes from, and how changes take effect." keywords: "Generative AI, Cybersecurity, Policy, Network Policy, Sandbox, Security, Hot Reload" -position: 6 +position: 1 --- -Sandbox policies define what programs can access and which actions they can take -within an OpenShell sandbox. OpenShell uses declarative YAML configurations to -specify which files programs can access, which network destinations they can -reach, and which application requests they can make. +A sandbox policy is a declarative YAML document that defines what programs +inside an OpenShell sandbox can do. It controls which files they can read and +write, which user they run as, which network destinations each program can +reach, and which application requests it can send. OpenShell denies anything the +policy does not allow. -This guide explains how to inspect, change, and verify those permissions. +This page explains how policies work. To try one in a running sandbox, follow +[Write Your First Sandbox Network Policy](/get-started/tutorials/first-network-policy). +To create, change, and verify policies, refer to +[Manage Sandbox Policies](/sandboxes/manage-policies). -## Policy Configuration +## What a Policy Controls -The policy configuration file groups related settings into sections, each -controlling a different aspect of how programs run and interact with their -environment. +A policy contains up to five sections. Different parts of the sandbox runtime +enforce each section, and each section takes effect at a specific time: -Filesystem settings define where programs can read and write, and process -settings select the user and group they run as. Network rules define which -services each program can reach and which requests it can send. Optional -middleware can inspect or transform allowed traffic, for example to redact -sensitive content from a request. +| Section | Controls | Enforced by | Takes effect | +|---|---|---|---| +| `filesystem_policy` | Paths programs can read, or read and write. | Landlock LSM in the kernel. | At sandbox startup. | +| `landlock` | Whether an unsupported kernel or inaccessible path aborts startup. | Sandbox startup checks. | At sandbox startup. | +| `process` | User and group for sandbox processes. | Identity change before the workload starts. | At sandbox startup. | +| `network_policies` | Destinations each program can reach and requests it can send. | Sandbox supervisor proxy. | While the sandbox runs. | +| `network_middlewares` | Additional inspection or transformation of allowed traffic. | Sandbox supervisor proxy. | While the sandbox runs. | -The configuration contains the following policy sections: +The following policy uses every section: -```yaml +```yaml showLineNumbers={false} version: 1 -filesystem_policy: { ... } # Paths programs can read or write. -landlock: { ... } # Linux filesystem enforcement settings. -process: { ... } # User and group for sandbox processes. -network_policies: { ... } # Network destinations and permitted requests. -network_middlewares: { ... } # Additional traffic processing. + +# Startup: paths the workload can read, or read and write. +filesystem_policy: + include_workdir: true + read_only: [/usr, /lib, /etc] + read_write: [/tmp] + +# Startup: behavior when the kernel or a listed path cannot support a rule. +landlock: + compatibility: best_effort + +# Startup, optional: override the identity selected by the compute driver. +# process: +# run_as_user: "1500" +# run_as_group: "1500" + +# Live: which programs can reach which destinations. +network_policies: + github_api: + name: github-api + endpoints: + - host: api.github.com + port: 443 + protocol: rest + enforcement: enforce + access: read-only + binaries: + - path: /usr/bin/curl + +# Live: middleware applied to allowed traffic, selected by destination host. +network_middlewares: + regex-redactor: + name: Redact API tokens + middleware: openshell/regex + order: 10 + config: + mode: redact + on_error: fail_closed + endpoints: + include: ["api.github.com"] ``` -Refer to the [Policy Schema Reference](/reference/policy-schema) for every -accepted field. +When a policy allows network access, OpenShell also adds baseline filesystem +paths that sandbox processes need, such as `/usr`, `/lib`, and `/tmp`, so a +policy can focus on the paths specific to its workload. The +[Default Policy](/reference/default-policy) page lists those paths. The +[Policy Schema Reference](/reference/policy-schema) defines every field. -## Apply Policy Changes +## How Network Rules Work -Every sandbox runs under a policy, but you do not have to supply a policy file -when creating one. OpenShell uses an existing policy or falls back to its -restrictive default. See [How OpenShell Selects a Policy](#how-openshell-selects-a-policy) -for the selection order. +Every outbound connection from a sandbox passes through the sandbox supervisor, +which checks it against `network_policies`. If no rule allows the connection, +the supervisor denies it. -You can update network rules while a sandbox is running. Changes to filesystem -permissions and process identity require recreating the sandbox to take effect. -See [Understand Reloads](#understand-reloads) for the limits on each type of -change. +### Rules Pair Programs with Destinations -The OpenShell CLI provides three ways to apply changes. Choose based on whether -you are creating a sandbox, changing network permissions, or replacing its -configuration: +Each entry in `network_policies` is a named rule with a list of `endpoints` and +a list of `binaries`. The rule allows each listed binary to reach each listed +endpoint. A rule with two binaries and two endpoints therefore grants four +connection pairs: -| Command | Use it when | Important behavior | -|---|---|---| -| `openshell sandbox create --policy` | You know the complete policy before creation. | The file supplies the sandbox's initial policy and can set startup-time controls. | -| `openshell policy update` | You need to add or remove network permissions. | It changes only `network_policies` and preserves every other section. | -| `openshell policy set` | You need to replace the complete policy, including middleware or startup settings. | Start from the current base so you preserve required startup settings and exclude provider-owned rules. | +| Binary | Endpoint | +|---|---| +| `/usr/bin/curl` | `api.example.com:443` | +| `/usr/bin/curl` | `uploads.example.com:443` | +| `/usr/bin/python3` | `api.example.com:443` | +| `/usr/bin/python3` | `uploads.example.com:443` | -### Inspect the Current Policy +If Python should reach only `api.example.com`, put that binary and endpoint in a +separate rule. -The base policy is the policy configured for the sandbox. Attached providers can -contribute additional network rules; the effective policy includes those rules -alongside the base policy. Start sandbox policy edits from the base view so you -do not copy provider-owned rules into your configuration. +OpenShell identifies the calling program by its canonical executable path, so a +symlink does not change which rule applies. A rule can also match a trusted +ancestor process, such as the agent that launched a tool. OpenShell pins each +authorized executable to the file digest it first observes, and denies access if +the file at that path later changes. -In the command examples, replace `my-sandbox` with the name of your sandbox: +List the executable that makes the connection, which is not always the command +you type. A script such as `pip` runs under its interpreter, so a rule for +`pip` must list the Python interpreter. Any program that a listed binary +launches can also use the rule. A rule with an empty `binaries` list matches no +program in a sandbox. -```shell -openshell policy get my-sandbox --base -openshell policy get my-sandbox --full -``` +### Connection and Request Checks -Check the effective view's `Source` field for a gateway-global override, which -blocks sandbox policy changes. Review all rules that could allow the operation -you intend to restrict. Matching rules can contribute permissions together, and -an applicable request deny takes precedence over an allow. See -[Understand Overlapping Rules](/sandboxes/network-policy-recipes#understand-overlapping-rules) -for the matching rules. +OpenShell checks network traffic in two layers. The connection check applies to +every connection, and requires the destination host and port and the calling +binary to match a rule. For an endpoint with an inspected `protocol`, OpenShell then +parses each application request and checks it against the endpoint's `access` +preset or `rules`. -For review or a [policy containment check](/reference/policy-prover), export the -effective policy as YAML: +The `protocol` field selects what OpenShell inspects after the connection check: -```shell -openshell sandbox get my-sandbox --policy-only > effective-policy.yaml -``` +| `protocol` | What OpenShell checks | +|---|---| +| `rest` | HTTP method, path, and query values. | +| `websocket` | The upgrade request and complete client text messages. | +| `graphql` | GraphQL operation type, operation name, and root fields. | +| `mcp` | MCP methods and tool names in client requests. | +| `json-rpc` | JSON-RPC method names in client requests. | +| `tcp` | Nothing beyond the connection. The program gets a native TCP socket and OpenShell does not inspect the payload. | +| Omitted | No request rules. OpenShell still terminates TLS, parses HTTP requests, and checks that each request's authority matches the destination. | -The effective export can contain provider-owned rules. Prepare edits from the -base view, as described in [Replace the Policy](#replace-the-policy). +Inspected protocols can allow reads while blocking writes on the same API, which +a connection-only rule cannot do. Use an inspected protocol when the request, +not only the destination, determines the risk. -### Update Network Rules +### Enforce and Audit -To give a program access to a service, add an endpoint specifying its executable -path, destination, and permitted request types. For a sandbox containing -`/usr/bin/curl`, this command adds -read-only access to the GitHub API: +The `enforcement` field on an inspected endpoint decides what happens when a +request violates the endpoint's rules: -```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 -``` +- `enforce` blocks the request and returns an OpenShell `policy_denied` response. +- `audit` logs the violation and forwards the request. Audit is the default. -Here, `rest` enables HTTP request inspection, `read-only` permits GET, HEAD, and -OPTIONS, and `enforce` blocks requests outside that set. The default enforcement -mode is `audit`, which logs an applicable request-rule violation but forwards the -request. Other matching rules can also grant permissions, so inspect the -effective policy before relying on a restriction. +Audit mode lets you observe traffic before you enforce a rule, but it blocks +nothing. Set `enforcement: enforce` on every endpoint whose request rules must +block traffic. Audit applies only to request rules. It does not bypass the +destination, binary, credential, or middleware checks. -The binary path must match the executable inside the sandbox. For other -protocols and their YAML configuration, see -[Configure Network Policy Rules](/sandboxes/network-policy-recipes). +### Overlapping Rules -For changes to an existing rule, take its name, binaries, and endpoint details -from the base policy. Request-rule edits require the complete affected scope so -the command identifies exactly which permissions will change. You can also -remove one named rule or remove a destination across rules. +Network rules are not an ordered firewall list. Several rules can match the same +connection or request, and every matching allow contributes permission. An +applicable deny takes precedence over any allow, regardless of where either +appears in the file. -To preview an incremental operation, use `--dry-run` instead of `--wait`. The CLI -fetches the current policy and shows the proposed result without saving it. This -preview does not run every check needed to activate the change. The -[Incremental Policy Update Reference](/reference/policy-updates) covers command -syntax, targeting requirements, and removal behavior. +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/**`. Requests +under `/repos/private/` are denied. When you restrict access, review every rule +that could allow the operation, including rules that providers contribute. -### Replace the Policy +### Request Flow -A full replacement supplies every section of the policy, including settings -you intend to keep. Begin with the current base: +For inspected HTTP traffic, OpenShell applies its checks in this order: -```shell -openshell policy get my-sandbox --base +```mermaid +flowchart TD + A["Sandbox tool"] --> B["Check destination
and process identity"] + B --> C["Check application request rules"] + C --> D["Run selected request middleware
Recheck transformed operations"] + D --> E["Resolve permitted credentials"] + E --> F["Upstream service"] + F --> G["Run selected response middleware"] + G --> A ``` -The base view prints status metadata followed by a `---` separator and the policy -in YAML. Copy only the YAML below the separator into `policy.yaml`. Keep the -complete policy, including its filesystem, Landlock, process, and unrelated -network settings, then edit the sections you intend to change. Do not redirect -the entire output into a policy file, because the metadata is not part of the -policy. +Middleware runs only on traffic that network rules already allow. It can inspect +or transform requests before OpenShell injects provider credentials, inspect +responses before they reach the sandbox, and inspect complete client WebSocket +text messages. Built-in middleware is available to every policy. External +middleware must be registered with the gateway first. Refer to +[Supervisor Middleware](/extensibility/supervisor-middleware). -Submit the edited file with `--wait` to wait for its revision result: +### Network Access and Credentials -```shell -openshell policy set my-sandbox --policy policy.yaml --wait -``` +A network rule that allows traffic does not authorize every provider credential +at that destination. OpenShell supplies a provider credential only to the +destinations that its provider profile or an explicit credential binding +allows. Provider-credentialed endpoints also require inspected traffic unless +the endpoint sets `allow_uninspected_credentials: true`. When a request fails a +credential check, correct the provider binding instead of widening the network +rule. Refer to [Provider Profiles](/providers/profiles). + +## 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 gateway-global policy set by an administrator. +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](/reference/default-policy). -The new policy must pass validation before becoming active. Changes to startup -settings remain subject to the limits in [Understand Reloads](#understand-reloads). +An invalid image policy keeps the workload from starting until you repair it. +OpenShell does not skip it and use the default. -To restore an earlier compatible base revision, follow [Restore an Earlier Base -Revision](/sandboxes/troubleshoot-policies#restore-an-earlier-base-revision). -For scripted exports, the command reference includes an optional -[JSON workflow](/reference/policy-updates#full-replacement) using `jq`. +### Base and Effective Policies -### Verify the Change +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. -A successful submission means the gateway accepted the change. With `--wait`, -the CLI also waits for a revision result, but success can mean the revision -loaded, made no change, or was superseded by another update. Inspect policy -history and the effective configuration before relying on new permissions: +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](/sandboxes/manage-policies#inspect-the-current-policy) shows both views. -```shell -openshell policy list my-sandbox -openshell policy get my-sandbox --full -``` +### Gateway-Global Policy -Then test an operation that should be allowed and one that should be blocked. -Confirm any denial comes from OpenShell rather than the destination service or -a missing client tool. The [first network policy -tutorial](/get-started/tutorials/first-network-policy) demonstrates these checks -in a prepared sandbox. +A gateway administrator can apply one policy to every sandbox on the gateway. +The global policy replaces each sandbox's policy. It is not a ceiling +intersected with existing grants. While it is active, sandbox policy changes are +blocked and provider-contributed network rules are suppressed. Credential +authorization remains a separate check, so a global network grant does not make +a provider credential usable at a destination. -## Understand Reloads +Deleting the global policy restores normal policy selection and provider rules. +Refer to [Apply a Gateway-Wide +Policy](/sandboxes/manage-policies#apply-a-gateway-wide-policy) for the +commands. -OpenShell validates a proposed policy before activating it. Network rules and -the policy's middleware settings can change while the workload runs. Filesystem -and process settings take effect at startup. External middleware registration is -a separate gateway operation. +## How Changes Take Effect -| Change | Running workload behavior | Required action | +Live sections and startup sections behave differently when a policy changes: + +| Change | Effect on a running sandbox | Required action | |---|---|---| -| Network request rules | The running workload receives the new rules. Connections using the previous active configuration close. | Use `update` or `set`, inspect status, and retry. | -| Middleware selected or configured in `network_middlewares` | You can add, remove, or reconfigure built-in middleware and external services already registered with the gateway. | Preserve the base and use `set`; `update` does not edit middleware. | -| External middleware registration in gateway TOML | Policy updates cannot register a new service or change its gateway connection settings. | Update the registration and restart the gateway. | -| First native TCP endpoint | The standard runtime can add it while the workload runs because policy DNS and transparent capture are already available. | Use `update` or `set`, inspect status, and retry. | -| Added filesystem paths | The saved policy can change, but the running workload keeps its existing filesystem permissions. | Recreate the sandbox to apply the new startup configuration. | -| Removed baseline paths or changed workdir, Landlock, or process identity | OpenShell rejects the change after the workload starts. | Recreate the sandbox with the intended startup configuration. | -| Repair before first activation | If the workload has never started, you can replace rejected startup settings. | Repair them before provisioning times out. | +| Network rules | The sandbox receives the new rules. Connections that use the previous configuration 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 workdir, Landlock, or process settings | OpenShell rejects the change after the workload starts. | Recreate the sandbox. | OpenShell calls each version of the active configuration a generation. When a new generation becomes active, OpenShell closes HTTP keep-alive connections, @@ -201,6 +259,8 @@ tunnels, upgrades, and long-lived streams that still use the previous one. A parsed WebSocket relay closes with code `1012`. The client must reconnect so its next request uses the new configuration. +Every change passes validation before it becomes active: + ```mermaid flowchart TD A["Review the proposed change"] --> B["Submit it to the gateway"] @@ -215,109 +275,23 @@ flowchart TD H --> A ``` -A no-op update may create no new revision. If validation fails, the effect on -existing access depends on the failure stage and the configured runtime failure -mode. [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) explains -how to identify that stage and repair the configuration. - -## How OpenShell Selects a Policy - -When more than one policy is available, OpenShell uses the first applicable -policy in this order: - -1. A gateway-global policy set by an administrator. -2. The sandbox's saved policy. At creation, `--policy` takes precedence over - `OPENSHELL_SANDBOX_POLICY`; subsequent policy changes update the saved policy. -3. A policy included in the sandbox image. -4. OpenShell's [restrictive default policy](/reference/default-policy). - -OpenShell then adds network rules from attached providers, unless a -gateway-global policy is active. A global policy replaces the sandbox's policy -and suppresses provider-added rules, as described below. - -An invalid image policy must be repaired before the workload can start; -OpenShell does not skip it and use the default. See -[Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) for repair guidance. - -## Global Policy Override - -A gateway administrator can apply one policy across the gateway's sandboxes. -This global override replaces each sandbox policy; it is not a ceiling -intersected with existing grants. While it is active, sandbox policy updates -are blocked and provider-added network grants -are suppressed. Credential authorization remains a separate check, so a network -grant alone does not make a provider credential usable at a destination. - -Deleting the global override restores normal sandbox selection and provider -composition. The gateway rejects restoration when the resulting effective -configuration is invalid. Inspect effective policy before, during, and after an -operator changes the override. - -The OpenShell CLI provides separate operations for managing the override: - -| Operation | Command | -|---|---| -| Inspect the current override. | `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 override and restore normal policy selection. | `openshell policy delete --global` | - -The set and delete commands ask for confirmation unless you pass `--yes`. Keep -the prompt during interactive work because each operation changes the network -model for every sandbox on the gateway. - -## Allow Native TCP Connections - -Some applications, such as database clients, need a native connection rather -than an HTTP proxy. `protocol: tcp` provides ordinary DNS resolution and a native -TCP socket without application payload inspection. It supports Docker and -Podman. The standard runtime prepares DNS and TCP connection handling even when -the initial policy has no TCP endpoints, so you can add the first endpoint -without recreating the sandbox. Refer to -[Configure Network Policy Rules](/sandboxes/network-policy-recipes#allow-native-tcp) -for the recipe and shared-hosting limitations. - -## Supervisor Middleware - -For traffic that needs additional inspection or transformation, -`network_middlewares` selects middleware, the destination hosts it applies to, -and its configuration. Network rules must allow the traffic -before middleware can process it. - -Middleware can inspect HTTP requests before credential injection, HTTP responses -before they reach the sandbox, and complete client WebSocket text messages. Each -implementation declares which operations it supports; matching a destination -host does not enable unsupported operations. Binary and upstream-to-client -WebSocket messages are not inspected. - -Built-in middleware is available without registration. External middleware must -be registered in the gateway configuration before a policy can use it. Once it -is registered, a policy change can add it to or remove it from a running sandbox, -or change its policy settings. Adding or changing the gateway registration -requires restarting the gateway. Refer to -[Supervisor Middleware](/extensibility/supervisor-middleware) for registration, -processing order, supported transformations, and failure behavior. - -## Validation Failures - -A local parse error, gateway preflight rejection, initial configuration failure, -runtime rejection, wait timeout, credential denial, and middleware denial have -different effects. Do not widen endpoint access to repair a credential or -middleware problem. - -Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) to identify -the stage, whether the previous policy remains active, and the corrective action. -Gateway failure-mode settings are defined in -[Gateway Configuration](/reference/gateway-config). +The gateway checks a proposed policy before saving it. The sandbox checks the +complete effective configuration again before activating it, because it also +sees provider changes and concurrent updates. If activation fails, the gateway's +`policy_validation_failure_mode` setting decides whether the sandbox blocks new +egress (`fail_closed`, the default) or keeps the last valid configuration +(`retain_last_valid`). [Troubleshoot Sandbox +Policies](/sandboxes/troubleshoot-policies) explains how to identify and repair +each kind of failure. ## Next Steps -- Use [Configure Network Policy Rules](/sandboxes/network-policy-recipes) for - REST, native TCP, WebSocket, GraphQL, MCP, and JSON-RPC recipes. -- Use the [Policy Schema Reference](/reference/policy-schema) for fields, - defaults, matching rules, and constraints. -- Use [Policy Advisor](/sandboxes/policy-advisor) to let a sandbox propose a - narrow network change for review. -- Use the [Standalone Policy Prover](/reference/policy-prover) as an optional - containment check. Its result covers only the modeled domains and does not - apply policy or attest runtime state. +- Use [Manage Sandbox Policies](/sandboxes/manage-policies) to create, update, + verify, and roll back policies. +- Use [Network Policy Recipes](/sandboxes/network-policy-recipes) for rules you + can adapt for REST APIs, package registries, WebSocket, GraphQL, MCP, + JSON-RPC, and native TCP. +- Use [Policy Advisor](/sandboxes/policy-advisor) to let an agent propose narrow + network changes for your review. +- Use the [Policy Schema Reference](/reference/policy-schema) for every field, + default, and validation rule. diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx new file mode 100644 index 0000000000..0a62d2f44f --- /dev/null +++ b/docs/sandboxes/manage-policies.mdx @@ -0,0 +1,312 @@ +--- +# 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: "Create sandboxes with a policy, inspect and change sandbox policies, verify changes, roll back, and apply a gateway-wide policy." +keywords: "Generative AI, Cybersecurity, Policy, Sandbox, CLI, Hot Reload, Revision" +position: 2 +--- + +This page covers the tasks for working with sandbox policies: setting a policy +at creation, inspecting it, changing it on a running sandbox, verifying the +result, and rolling back. For how policies work, refer to +[Sandbox Policies](/sandboxes/policies). + +The command examples use `my-sandbox` as the sandbox name. When you omit the +name, the CLI uses the last sandbox you used. + +## Create a Sandbox with a Policy + +Pass a policy file when you create a sandbox to set its initial policy, +including the filesystem, Landlock, and process settings that cannot change +after the workload starts: + +```shell +openshell sandbox create --name my-sandbox --policy ./policy.yaml +``` + +To use the same file for every sandbox you create, set +`OPENSHELL_SANDBOX_POLICY`. The CLI reads it whenever you omit `--policy`: + +```shell +export OPENSHELL_SANDBOX_POLICY=./policy.yaml +openshell sandbox create --name my-sandbox +``` + +Without either, the sandbox uses a policy from its image or the restrictive +[default policy](/reference/default-policy). Write the file from the +[Network Policy Recipes](/sandboxes/network-policy-recipes) and the +[Policy Schema Reference](/reference/policy-schema). + +## Ship a Policy in an Image + +A sandbox image can include its own policy at `/etc/openshell/policy.yaml`. +When the gateway has no saved policy for a sandbox, the supervisor reads that +file and saves it as the sandbox's policy. The supervisor also checks the legacy +path `/etc/navigator/policy.yaml`. For example, add the file in a Dockerfile: + +```dockerfile +COPY policy.yaml /etc/openshell/policy.yaml +``` + +A `--policy` file or `OPENSHELL_SANDBOX_POLICY` takes precedence over the image +policy. If the image policy is invalid, the workload does not start until you +repair the configuration. OpenShell does not fall back to the default policy. +Refer to [Repair Initial +Configuration](/sandboxes/troubleshoot-policies#repair-initial-configuration). + +## Inspect the Current Policy + +Print the sandbox's base policy, without provider-contributed rules, or its +effective policy, which is what the sandbox enforces: + +```shell +openshell policy get my-sandbox --base +openshell policy get my-sandbox --full +``` + +Each view prints status metadata, a `---` separator, and the policy YAML. +Without `--base` or `--full`, `policy get` prints only the metadata. A `Source` +value of `global` means a gateway-global policy is active, which blocks sandbox +policy changes. + +Start edits from the base view so you do not copy provider-owned rules into +your configuration. Use the effective view to review everything that could allow +an operation, because several rules can grant permission together. + +To export the effective policy as plain YAML, for example for review or a +[policy prover check](/reference/policy-prover): + +```shell +openshell sandbox get my-sandbox --policy-only > effective-policy.yaml +``` + +To list the policy revision history with each revision's load status: + +```shell +openshell policy list my-sandbox +``` + +| Status | Meaning | +|---|---| +| `Pending` | The gateway saved the revision, and the sandbox has not loaded it yet. | +| `Loaded` | The sandbox loaded the revision. | +| `Failed` | The sandbox rejected the revision. Refer to [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies). | +| `Superseded` | A newer revision was saved before this one loaded. | + +To view an earlier revision, add `--rev` with its version number, for example +`openshell policy get my-sandbox --rev 2 --base`. + +## Add or Remove Network Access + +`openshell policy update` changes network rules on a running sandbox without a +complete policy file. It edits only `network_policies` and preserves every +other section. + +To give a program access to a service, add an endpoint that names the +executable, the destination, and the permitted request types. For a sandbox +containing `/usr/bin/curl`, this command adds read-only access 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 +``` + +Here, `rest` enables HTTP request inspection, `read-only` permits `GET`, `HEAD`, +and `OPTIONS`, and `enforce` blocks other requests. The binary path must match +the executable inside the sandbox. + +To allow an additional request on an existing rule, name the rule and repeat its +complete binary list from the base policy: + +```shell +openshell policy update my-sandbox \ + --rule-name github_readonly \ + --binary /usr/bin/curl \ + --add-allow 'api.github.com:443:POST:/repos/*/issues' \ + --wait +``` + +The CLI requires the complete scope of the target rule, so the command +identifies exactly which permissions change. Use `--add-deny` with the same +syntax to block a request. + +To remove access, remove the named rule: + +```shell +openshell policy update my-sandbox --remove-rule github_readonly --wait +``` + +`--remove-endpoint host:port` removes a destination from every rule that lists +it, so preview it first. + +To preview any update without saving it, use `--dry-run` instead of `--wait`. +The preview fetches the current policy and shows the merged result, but does +not run every check needed to activate it. The [Policy Command +Reference](/reference/policy-updates) covers the complete update syntax, scope +rules, and removal behavior. + +## Replace the Complete Policy + +Use `openshell policy set` for changes that `policy update` cannot make, such as +middleware, GraphQL, MCP, or JSON-RPC rules, `tls: skip`, credential fields, or +reorganizing rules. A full replacement supplies every section of the policy, +including settings you intend to keep, so start from the current base policy. + + + +Print the base policy: + +```shell +openshell policy get my-sandbox --base +``` + +Copy only the YAML below the `---` separator into `policy.yaml`. Do not +redirect the entire output into the file, because the metadata above the +separator is not part of the policy. Keep the filesystem, Landlock, process, +and unrelated network settings, including baseline paths that OpenShell added at +startup, then edit the sections you intend to change. + +Submit the edited file and wait for its result: + +```shell +openshell policy set my-sandbox --policy policy.yaml --wait +``` + + + +Extract the base policy as JSON without display metadata or provider-owned +rules: + +```shell +set -o pipefail +openshell policy get my-sandbox --base --output json \ + | jq -e '.policy' > base-policy.json +``` + +Edit `base-policy.json`, then submit the complete policy: + +```shell +openshell policy set my-sandbox --policy base-policy.json --wait +``` + + + + +The new policy must pass validation before it becomes active. Changes to +startup settings are subject to the limits in [How Changes Take +Effect](/sandboxes/policies#how-changes-take-effect). + +## Change Filesystem and Process Settings + +Filesystem, Landlock, and process settings take effect only when the sandbox +starts. After the workload starts, OpenShell rejects removed filesystem paths +and changed workdir, Landlock, or process settings. It can save added +filesystem paths, but the running workload keeps its existing permissions. + +To change these settings, export the base policy as described in +[Replace the Complete Policy](#replace-the-complete-policy), edit it, and create +a new sandbox with the file: + +```shell +openshell sandbox delete my-sandbox +openshell sandbox create --name my-sandbox --policy ./policy.yaml +``` + +Deleting a sandbox stops its processes and removes its state, so copy out +anything you need first. When the policy allows network access, OpenShell adds +baseline system paths at startup, so list only the paths your workload +requires. The [Default Policy](/reference/default-policy) page lists the +baseline paths. + +## Verify a Change + +A successful submission without `--wait` means only that the gateway accepted +the change. With `--wait`, the CLI polls until the revision reaches a result: + +| Result | Exit code | +|---|---| +| The sandbox loaded the revision. | `0` | +| The policy was unchanged, so no revision was created. | `0` | +| A newer revision superseded this one before it loaded. | `0` | +| The sandbox rejected the revision. | `1` | +| The wait timed out. | `124` | + +Because a zero exit can mean an unchanged or superseded revision, inspect the +revision history and the effective policy before you rely on new permissions: + +```shell +openshell policy list my-sandbox +openshell policy get my-sandbox --full +``` + +A timeout means the CLI stopped polling. It does not tell you whether the +revision later loaded or failed, so check the revision status before you submit +again. + +Then test one operation 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. The [first network policy +tutorial](/get-started/tutorials/first-network-policy) demonstrates these +checks. + +## Roll Back to an Earlier Revision + +To restore an earlier policy, choose a compatible revision from +`openshell policy list my-sandbox` and print its base policy. For example, to +retrieve revision 2: + +```shell +openshell policy get my-sandbox --rev 2 --base +``` + +Copy the YAML below the `---` separator into `previous-base.yaml`, review the +complete policy, and submit it as a new revision: + +```shell +openshell policy set my-sandbox --policy previous-base.yaml --wait +openshell policy list my-sandbox +``` + +Rolling back a policy does not restore provider profiles, attachments, +credentials, or a gateway-global policy. Startup settings in the earlier +revision are still subject to the limits in [How Changes Take +Effect](/sandboxes/policies#how-changes-take-effect). + +## Apply a Gateway-Wide Policy + +A gateway administrator can apply one policy to every sandbox on the gateway. +The global policy replaces each sandbox's policy, blocks sandbox policy changes, +and suppresses provider-contributed network rules until you delete it. These +operations 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` | + +A global policy takes effect immediately, so `policy set --global` does not +accept `--wait`. The set and delete commands ask for confirmation unless you +pass `--yes`. Keep the prompt during interactive work, because each operation +changes the network access of every sandbox on the gateway. + +The gateway rejects deleting the global policy when the restored sandbox and +provider configuration would be invalid. Inspect the effective policy of +affected sandboxes before and after the change. + +## Next Steps + +- Use [Network Policy Recipes](/sandboxes/network-policy-recipes) for rules you + can adapt to common services and protocols. +- Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) when a + change fails to load or a request is denied unexpectedly. +- Use the [Policy Command Reference](/reference/policy-updates) for every + `openshell policy` flag and the complete update syntax. From 4b80141fc242e2f125ae20ea2fe7c2856c542aa9 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 17:07:41 +0000 Subject: [PATCH 05/40] docs(policy): reorganize network recipes as a cookbook Signed-off-by: Johnny Greco --- docs/sandboxes/network-policy-recipes.mdx | 749 ++++++++++++++-------- 1 file changed, 491 insertions(+), 258 deletions(-) diff --git a/docs/sandboxes/network-policy-recipes.mdx b/docs/sandboxes/network-policy-recipes.mdx index 122c73143e..a8b6a240eb 100644 --- a/docs/sandboxes/network-policy-recipes.mdx +++ b/docs/sandboxes/network-policy-recipes.mdx @@ -1,106 +1,55 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Configure Network Policy Rules" -sidebar-title: "Network Policy Recipes" -description: "Choose and configure REST, native TCP, WebSocket, GraphQL, MCP, and JSON-RPC policy rules." -keywords: "Generative AI, Cybersecurity, Policy, Network, REST, TCP, WebSocket, GraphQL, MCP, JSON-RPC" -position: 7 +title: "Network Policy Recipes" +sidebar-title: "Network Recipes" +description: "Adapt network rules for REST APIs, package registries, internal services, WebSocket, GraphQL, MCP, JSON-RPC, native TCP, raw TLS, and provider credentials." +keywords: "Generative AI, Cybersecurity, Policy, Network, REST, TCP, WebSocket, GraphQL, MCP, JSON-RPC, PyPI, npm" +position: 3 --- -Network rules combine a destination and executable with optional -application-request checks. Each recipe on this page illustrates a permission -for a particular protocol and how to verify it. Choose the recipe that matches -your service and adapt it to your sandbox's policy. - -## Choose a Protocol - -OpenShell first checks whether a program can connect to a host and port -(Layer 4). For inspected protocols, it then checks the application request -(Layer 7). - -| Reader goal | `protocol` | Inspected boundary | -|---|---|---| -| Allow HTTP methods and paths | `rest` | HTTP requests. | -| Connect a database or other native client | `tcp` | Connection only, without payload inspection. | -| Control an RFC 6455 client | `websocket` | Upgrade request and supported client messages. | -| Control GraphQL operations | `graphql` | GraphQL-over-HTTP operations. | -| Control MCP methods and tools | `mcp` | MCP Streamable HTTP client requests. | -| Control generic JSON-RPC methods | `json-rpc` | JSON-RPC-over-HTTP client requests. | - -If you omit `protocol`, OpenShell does not apply protocol-specific request rules. -This does not disable the proxy's TLS handling or HTTP destination checks. Use -`protocol: tcp` for a native socket, or `tls: skip` when a client needs a raw -stream such as client-certificate mTLS. Provider-credentialed endpoints reject -uninspected traffic unless you explicitly set -`allow_uninspected_credentials: true`. - -## Understand Inspected HTTP Flow - -Inspected HTTP requests pass through several independent checks. - -```mermaid -flowchart TD - A["Sandbox tool"] --> B["Check destination
and process identity"] - B --> C["Check application request rules"] - C --> D["Run selected request middleware
Recheck transformed operations"] - D --> E["Resolve permitted credentials"] - E --> F["Upstream service"] - F --> G["Run selected response middleware"] - G --> A -``` +Each recipe on this page is a network rule for a common service or protocol, +with commands to verify it. For how OpenShell evaluates these rules, refer to +[How Network Rules Work](/sandboxes/policies#how-network-rules-work). -Network rules check the destination and the calling program before allowing a -connection. Protocol rules then check the application request. Middleware can -filter or transform allowed traffic, and credential checks determine whether -OpenShell can supply credentials to the destination. See the recipes below for -native TCP and WebSocket boundaries that differ from ordinary inspected HTTP -requests. +## Use a Recipe -## Apply a Recipe +Each recipe states when to use it, gives the rule, shows how to verify it, and +lists its limits. Replace the example hosts, paths, and binaries with your own +values, and confirm that the sandbox contains the client executable. The +commands use `my-sandbox` as the sandbox name. -Each recipe can be applied independently. Its YAML supplies an entry for the -`network_policies` section of a complete policy file. The commands use -`my-sandbox` as a placeholder for your running sandbox's name. Before applying -a recipe, confirm that the sandbox contains its client executable and that the -destination service is available. Replace example domains with services you -control. +When `openshell policy update` can express a rule, the recipe shows the command. +Otherwise, add the YAML entry under the `network_policies` section of a complete +policy file and apply it with `openshell policy set`, as described in [Replace +the Complete Policy](/sandboxes/manage-policies#replace-the-complete-policy). +Keep unrelated rules, and replace an existing rule with the same key only when +that is your intent. After you apply a rule, [verify the +change](/sandboxes/manage-policies#verify-a-change) before testing traffic. -Start with the current base policy so you preserve startup settings and exclude -provider-owned rules: +## HTTP APIs -```shell -openshell policy get my-sandbox --base -``` +These recipes use `protocol: rest`, which lets OpenShell check each HTTP +request's method, path, and query values. -Copy the YAML below the `---` separator into `policy.yaml`, leaving out the -status metadata above it. Add the recipe to the `network_policies` section, -creating that section if needed. Replace example hosts, paths, and binaries -with your own values. Each recipe supplies a network rule; keep the other -sections of your complete policy file. +### Allow Read-Only Access -Keep unrelated rules, and replace an existing rule with the same key only when -that is your intent. Review the complete file, then apply it and confirm which -revision is active: +Use this rule when a program needs to read from an HTTP API but must not change +anything. The `read-only` preset permits `GET`, `HEAD`, and `OPTIONS`. + + + ```shell -openshell policy set my-sandbox --policy policy.yaml --wait -openshell policy list my-sandbox -openshell policy get my-sandbox --full +openshell policy update my-sandbox \ + --rule-name github_readonly \ + --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:enforce \ + --wait ``` -For changes supported by `openshell policy update`, [incremental -updates](/reference/policy-updates) preserve the other policy sections without -requiring a complete replacement file. A successful parse or submission -does not prove that the runtime activated the policy, so inspect revision status -before testing traffic. - -## Allow REST Reads - -Use `protocol: rest` to match HTTP methods, paths, and optional query values. -Set `enforcement: enforce` whenever the rule must block a request. - -Insert this entry under `network_policies`: + + ```yaml github_readonly: @@ -115,8 +64,10 @@ github_readonly: - path: /usr/bin/curl ``` -[Apply this rule](#apply-a-recipe), then verify an allowed GET and a denied -POST from a sandbox with `/usr/bin/curl` installed: + + + +Verify an allowed `GET` and a denied `POST`: ```shell openshell sandbox exec -n my-sandbox --no-login-shell -- \ @@ -127,38 +78,14 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ --request POST https://api.github.com/zen ``` -The POST must return an OpenShell `policy_denied` response. `read-only` is an -HTTP operation preset, not a guarantee that an upstream GET has no side effect. +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. -For explicit rules, `*` in a request path can cross `/` boundaries. Quote globs -in shell commands so the local shell does not expand them. - -### Observe a Rule Before Enforcing It +### Scope Access to a Repository -An endpoint with `enforcement: audit` logs applicable request-rule violations -and forwards the requests. For a REST endpoint with `access: read-only`, a POST -therefore produces a violation event but still reaches the upstream service if -the other checks allow it. The resulting HTTP status comes from that service. -Choose audit mode when you intend to observe violations without blocking them. - -Inspect the policy event without a WARN filter: - -```shell -openshell logs my-sandbox --since 5m --source sandbox -``` - -Look for an event identifying the request-policy violation even though the -request was forwarded. OCSF policy events are INFO-level tracing records; their OCSF -severity does not change the CLI's tracing-level filter. Audit does not bypass -destination, executable, credential, parser, or middleware checks. An endpoint -must use `enforcement: enforce` for its request rules to block traffic. - -## Scope GitHub API Access to a Repository - -An agent using the GitHub CLI may need repository API operations in addition to -Git clone, fetch, or push. This rule permits `gh` to make REST requests under -one repository's API path. Replace `` and `` with its owner and name, -and keep only the executable paths used by your image: +Use this rule when an agent using the GitHub CLI needs repository API +operations for one repository. Replace `` and `` with the owner and +name, and keep only the executable paths used by your image: ```yaml github_repository_api: @@ -181,21 +108,68 @@ The rule grants all HTTP methods within that path, including writes. Narrow the methods and paths if the agent needs only specific operations. It does not grant Git transport access or GraphQL mutations. -[Apply the rule](#apply-a-recipe) with a GitHub provider attached. Its profile -must permit credential use at the destination, and the token must authorize the -repository operation. Provider-contributed read permissions remain available. -Verify a permitted operation on the selected repository and confirm that a -write to a disposable second repository receives an OpenShell denial. Use `gh` -for both checks so the requests use the executable selected by this rule. +Apply the rule with a GitHub provider attached. Its profile must permit +credential use at the destination, and the token must authorize the repository +operation. Provider-contributed read permissions remain available. Verify a +permitted operation on the selected repository, and confirm that a write to a +disposable second repository receives an OpenShell denial. Use `gh` for both +checks so the requests use the executable selected by this rule. -For a complete Git push example, see +In request paths, `*` can cross `/` boundaries. Quote globs in shell commands so +the local shell does not expand them. For a complete Git push example, see [Grant GitHub Push Access to a Sandboxed Agent](/get-started/tutorials/github-sandbox). -## Match Query Values +### Block Specific Requests -REST query matchers run on decoded, case-sensitive values. This adaptable -template requires `/usr/bin/curl` and an HTTP service you control. Replace the -hostname, path, and accepted query values before applying it: +Use a deny rule to carve an exception out of broader access. A matching deny +takes precedence over any allow, including allows from other rules. This +template allows reads under `/repos/` but blocks the private subtree. Replace +`api.example.com` and the paths with routes on an HTTP service you control: + +```yaml +repos_except_private: + name: repos-except-private + endpoints: + - host: api.example.com + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: + method: GET + path: /repos/** + deny_rules: + - method: GET + path: /repos/private/** + binaries: + - path: /usr/bin/curl +``` + +To add a deny rule to an existing REST rule without a complete policy file, use +`openshell policy update` with `--add-deny`, as described in the [Policy Command +Reference](/reference/policy-updates#request-rule-specification). + +Verify that the first request reaches the service and the second returns an +OpenShell policy denial: + +```shell +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/curl --silent --show-error --fail \ + https://api.example.com/repos/public/project +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/curl --silent --show-error \ + https://api.example.com/repos/private/project +``` + +When unexpected access remains, compare `openshell policy get --base` with +`openshell policy get --full`. Provider-contributed rules appear only in the +effective view, and a gateway-global policy replaces sandbox and provider rules. + +### Match Query Values + +Use query matchers when the same path serves different resources selected by +query parameters. Matchers run on decoded, case-sensitive values. This template +requires `/usr/bin/curl` and an HTTP service you control: ```yaml download_query: @@ -218,72 +192,204 @@ download_query: ``` For an allow rule, every value for a duplicate query key must match. For a deny -rule, every configured key must be present with at least one matching value; an -additional nonmatching duplicate does not cancel a matching value. Start with a -single query key when possible. [Apply the template](#apply-a-recipe), then test -the intended match and near misses against your service. +rule, every configured key must be present with at least one matching value, +and an additional nonmatching duplicate does not cancel a matching value. Start +with a single query key when possible, then test the intended match and near +misses against your service. + +### Observe a Rule Before Enforcing It -## Allow Native TCP +Use `enforcement: audit` to see which requests a rule would block before you +enforce it. Audit mode logs applicable request-rule violations and forwards the +requests. For a REST endpoint with `access: read-only`, a `POST` produces a +violation event but still reaches the upstream service if the other checks allow +it, so the resulting HTTP status comes from that service. -Use `protocol: tcp` when the application needs ordinary DNS resolution and a -native TCP socket. The rule checks the hostname, port, and executable but cannot -inspect the application payload. + + -Insert this entry under `network_policies`: +```shell +openshell policy update my-sandbox \ + --rule-name github_audit \ + --binary /usr/bin/curl \ + --add-endpoint api.github.com:443:read-only:rest:audit \ + --wait +``` + + + ```yaml -postgres: - name: postgres +github_audit: + name: github-audit endpoints: - - host: db.internal.example - port: 5432 - protocol: tcp + - host: api.github.com + port: 443 + protocol: rest + enforcement: audit + access: read-only binaries: - - path: /usr/bin/psql + - path: /usr/bin/curl ``` -Do not add `access`, `enforcement`, request rules, or credential-rewrite fields -to a TCP endpoint. This template requires `/usr/bin/psql` and a PostgreSQL -service reachable at the replacement hostname. [Apply the -template](#apply-a-recipe), verify that `psql` reaches the server, then verify -that another binary or port is denied at the connection boundary. + + -Docker and Podman support native TCP enforcement. The standard runtime prepares -DNS and TCP connection handling before the workload starts, even when the -initial policy has no TCP endpoints. You can therefore add the first endpoint -without recreating the sandbox. Other runtimes must provide the same support -and have it ready before activating a TCP rule. +Inspect policy events without a `--level warn` filter, because OCSF policy +events are INFO-level log records regardless of their event severity: -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 does not inspect the payload's authority. +```shell +openshell logs my-sandbox --since 5m --source sandbox +``` -For a connection-only check, use a client command that fails before application -authentication when the policy is absent, then succeeds far enough to reach the -server when the rule is present. A database authentication error can demonstrate -that the TCP connection reached the server, but it does not prove that database -credentials are correct. After removing the rule, confirm the client cannot -reach the server and inspect the sandbox log for the denied hostname and binary. +Look for an event identifying the request-policy violation even though the +request was forwarded. Audit does not bypass destination, executable, +credential, parser, or middleware checks. When the logs show only the violations +you expect, change the endpoint to `enforcement: enforce`. -Policy DNS returns a supervisor-owned synthetic address with a bounded TTL. -Clients must honor that TTL and resolve the hostname again before reconnecting; -a client that caches the synthetic address indefinitely can fail after its -mapping expires even while the endpoint remains allowed. Docker and Podman -currently advertise IPv4 egress for policy DNS, so AAAA queries return a -successful empty answer and dual-stack clients must continue with the A record. +## Package Registries + +Package managers download packages with `GET` requests, so a read-only REST rule +allows installs while blocking uploads. List the executable that opens the +connection. `pip` and `npm` are scripts, so their rules list the Python or Node +interpreter. Because a rule also matches programs that a listed binary launches, +listing an interpreter lets any program running under it reach the registry. + +### Allow PyPI Downloads + +Use this rule to let `pip` or `uv` install packages from PyPI: + +```yaml +pypi: + name: 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 + - path: /usr/local/bin/uv +``` + +OpenShell resolves `/usr/bin/python3` to its versioned interpreter, such as +`/usr/bin/python3.12`. If your image uses another interpreter, such as a +uv-managed Python, list its canonical path or a glob that matches it, such as +`/sandbox/.uv/python/*/bin/python3*`. Globs are not symlink-resolved. 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 + +Use this rule to let `npm` install packages from the public npm registry. Node +images from the official Node.js project install `node` at +`/usr/local/bin/node`, so adjust the path for your image: + +```yaml +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/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 add +an allow rule for the audit request if you need it. + +Verify that a scoped package lookup succeeds: + +```shell +openshell sandbox exec -n my-sandbox --no-login-shell -- \ + /usr/bin/npm view @types/node version +``` + +## Internal Services + +OpenShell blocks connections to private network addresses by default to prevent +server-side request forgery (SSRF). Endpoints with an exact hostname are the +exception. They can reach the private addresses that their hostname resolves to. +Loopback, link-local, and cloud metadata addresses are always blocked. + +### Restrict Destination Addresses + +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: + name: 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. `allowed_ips` constrains the +addresses accepted for a hostname but does not replace hostname authorization. +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. -`allowed_ips` can constrain the addresses accepted for a hostname, but it does -not replace hostname authorization. Account for DNS rotation and service -failover before pinning addresses. Review both the hostname and resolved address -in diagnostics instead of broadening the wildcard when an address changes. +## Other Application Protocols -## Allow WebSocket Messages +These recipes inspect protocols carried over HTTP. Each one requires explicit +rules for the protocol's operations. + +### Allow WebSocket Messages Use `protocol: websocket` for an RFC 6455 upgrade and client-to-server message -policy. This adaptable 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/**`: +policy. 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: @@ -307,10 +413,9 @@ realtime: - path: /usr/bin/node ``` -[Apply the template](#apply-a-recipe), then 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. +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 upstream-to-client messages. Provider-credentialed endpoints remain @@ -319,7 +424,7 @@ on the parsed relay and reject binary frames unless Set `websocket_credential_rewrite: true` only when client text messages contain OpenShell credential placeholders that must be resolved. -## Allow GraphQL Operations +### Allow GraphQL Operations Use `protocol: graphql` for GraphQL-over-HTTP. This template requires `/usr/bin/gh`, a GitHub credential with the required permissions, and the GitHub @@ -349,30 +454,69 @@ github_graphql: - path: /usr/bin/gh ``` -[Apply the template](#apply-a-recipe), then test an allowed query and the denied -mutation with the configured client. For allow rules, every selected root field -must match. For deny rules, one matching root field blocks the request. A -malformed, denied, or unregistered operation denies an entire batched HTTP -request. +Test an allowed query and the denied mutation with the configured client. For +allow rules, every selected root field must match. For deny rules, one matching +root field blocks the request. A malformed, denied, or unregistered operation +denies an entire batched HTTP request. GraphQL field names are application-specific. Do not treat a copied field list as a verified safety boundary. Review and test it against the authoritative schema for the deployed service version. Hash-only persisted queries require -`persisted_queries: allow_registered` and a trusted -`graphql_persisted_queries` registry. +`persisted_queries: allow_registered` and a trusted `graphql_persisted_queries` +registry. For GraphQL-over-WebSocket, use `protocol: websocket`, allow the upgrade with a -GET rule, and add GraphQL operation rules for client operation messages. Client -operation messages fail closed when malformed or disallowed. Lifecycle messages -such as `connection_init`, `ping`, `pong`, and `complete` are allowed without -payload logging. +`GET` rule, and add GraphQL operation rules for client operation messages. +Client operation messages fail closed when malformed or disallowed. Lifecycle +messages such as `connection_init`, `ping`, `pong`, and `complete` are allowed +without payload logging. + +### Combine REST and GraphQL on One Host + +Many APIs serve REST and GraphQL on the same host. A REST rule sees every +GraphQL call as `POST /graphql` and cannot tell a query from a mutation. Use the +endpoint `path` field to give each API its own endpoint in one rule: + +```yaml +github_api: + name: 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 +``` -## Allow MCP Tools +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`, and an MCP endpoint +cannot share a host and port with an endpoint that uses a different protocol. + +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 -adaptable template requires `/usr/bin/python3` with an MCP client and a -Streamable HTTP server you control. It allows initialization, tool discovery, -and `read_status`, while denying `delete_resource`: +template requires `/usr/bin/python3` with an MCP client and a Streamable HTTP +server you control. It allows initialization, tool discovery, and +`read_status`, while denying `delete_resource`: ```yaml mcp_server: @@ -400,24 +544,20 @@ mcp_server: - path: /usr/bin/python3 ``` -[Apply the template](#apply-a-recipe), then 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. +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` selects the exact `2025-11-25` revision. Supported -explicit values are `2025-03-26`, `2025-06-18`, and `2025-11-25`. This is an -allowlist, not a date range or moving `latest`. OpenShell checks one -`MCP-Protocol-Version` header on each request except a valid standalone -`initialize`; it does not negotiate every revision's complete wire profile or -bind server session state. Server responses and SSE messages are relayed without -MCP policy parsing. +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](/reference/policy-schema#mcp-version-selection). Server responses and +SSE messages are relayed without MCP policy parsing. -## Allow JSON-RPC Methods +### Allow JSON-RPC Methods -Use `protocol: json-rpc` for non-MCP JSON-RPC-over-HTTP. This adaptable template -requires `/usr/bin/python3` with a JSON-RPC client and an HTTP service you -control. It allows `reports.list` and `reports.search`, while denying +Use `protocol: json-rpc` for JSON-RPC-over-HTTP services other than MCP. This +template requires `/usr/bin/python3` with a JSON-RPC client and an HTTP service +you control. It allows `reports.list` and `reports.search`, while denying `reports.delete`: ```yaml @@ -440,60 +580,153 @@ reports_rpc: - path: /usr/bin/python3 ``` -[Apply the template](#apply-a-recipe), then verify `reports.list` succeeds and -`reports.delete` is denied. Method names are exact. Only `method: "*"` is -accepted as an all-method sentinel; other globs are rejected. Parameter matchers -are not supported. OpenShell evaluates every call in a batch and denies the full -batch if one call is denied. Server-to-client messages are not parsed for policy -enforcement. +Verify that `reports.list` succeeds and `reports.delete` is denied. Method names +are exact. Only `method: "*"` is accepted as an all-method sentinel, and other +globs are rejected. Parameter matchers are not supported. OpenShell evaluates +every call in a batch and denies the full batch if one call is denied. +Server-to-client messages are not parsed for policy enforcement. -## Understand Overlapping Rules +## Uninspected Connections -Network rules are not an ordered firewall list. More than one compatible rule -can match a connection or request, and matching allow rules can contribute -permission. An applicable request deny takes precedence over an allow. The -endpoint parser selected for a connection is a separate choice from whether the -parsed operation is allowed. +These recipes allow a connection without inspecting its application payload. +OpenShell still checks the destination, port, and executable. Prefer an +inspected protocol whenever the client supports it. -For example, suppose one rule permits `GET -/repos/**` and another matching rule denies -`GET /repos/private/**` for the same host, port, and executable. The private -path is denied regardless of which rule appears first in YAML. Keep related -exceptions close enough to review together. +### Allow Native TCP -For a sandbox configured with those two rules, the following requests test the -overlap. Replace `api.example.com` and the paths with routes on an HTTP service -you control: +Use `protocol: tcp` when the application needs ordinary DNS resolution and a +native TCP socket, such as a database client. The rule checks the hostname, +port, and executable but cannot inspect the application payload. + + + + +```shell +openshell policy update my-sandbox \ + --rule-name postgres \ + --binary /usr/bin/psql \ + --add-endpoint db.internal.example:5432::tcp \ + --wait +``` + + + + +```yaml +postgres: + name: postgres + endpoints: + - host: db.internal.example + port: 5432 + protocol: tcp + binaries: + - path: /usr/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. + +Docker and Podman support native TCP enforcement. The standard runtime prepares +DNS and TCP connection handling before the workload starts, even when the +initial policy has no TCP endpoints, so you can add the first endpoint without +recreating the sandbox. Other runtimes must provide the same support before +they can activate a TCP rule. + +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 does not inspect the payload's +authority. + +Policy DNS returns a supervisor-owned synthetic address with a bounded TTL. +Clients must honor that TTL and resolve the hostname again before reconnecting. +A client that caches the synthetic address indefinitely can fail after its +mapping expires, even while the endpoint remains allowed. Docker and Podman +currently advertise IPv4 egress for policy DNS, so AAAA queries return a +successful empty answer and dual-stack clients must continue with the A record. + +### Allow a Raw TLS Stream + +Use `tls: skip` when a client that uses the sandbox proxy must complete TLS with +the upstream service itself, for example to present a client certificate for +mTLS. OpenShell checks the destination, port, and executable, then relays the +encrypted stream without terminating TLS, inspecting requests, or injecting +credentials: + +```yaml +mtls_service: + name: mtls-service + endpoints: + - host: secure.example.com + port: 443 + tls: skip + binaries: + - path: /usr/bin/curl +``` + +Omit `protocol` on a `tls: skip` endpoint, because OpenShell does not evaluate +request rules for the relayed stream. Every endpoint that shares the same host +and port must also use `tls: skip`. A provider-credentialed endpoint requires +`allow_uninspected_credentials: true`, and credential placeholders pass upstream +unchanged. Fail-closed middleware cannot select a skipped endpoint, and policy +advisor cannot propose one. Use `protocol: tcp` instead when the client opens +native connections without the sandbox proxy. + +Verify that the client completes the mTLS handshake. The client sees the +upstream service's own certificate rather than one issued by the sandbox CA: ```shell openshell sandbox exec -n my-sandbox --no-login-shell -- \ /usr/bin/curl --silent --show-error --fail \ - https://api.example.com/repos/public/project -openshell sandbox exec -n my-sandbox --no-login-shell -- \ - /usr/bin/curl --silent --show-error \ - https://api.example.com/repos/private/project + --cert /sandbox/client.pem --key /sandbox/client.key \ + https://secure.example.com/ +``` + +## Credentials on Endpoints + +A network rule that allows traffic does not authorize every provider credential +at that destination. These recipes control where OpenShell supplies provider +credentials. + +### Bind a Provider Credential to an Endpoint + +Use `credential_binding` when an attached provider's profile defines no +endpoints of its own, and you want its static credentials to be usable at a +destination that your sandbox policy allows. This example allows Google Cloud +Storage and binds the credentials from the attached `work-gcp` provider to that +endpoint: + +```yaml +gcp_storage: + name: gcp-storage + endpoints: + - host: storage.googleapis.com + port: 443 + protocol: rest + enforcement: enforce + access: full + credential_binding: + provider: work-gcp ``` -For a service configured with those routes, the first request should reach the -service and the second should return an OpenShell policy denial. When unexpected access remains, -compare `policy get --base` with `policy get --full`: provider-contributed rules -appear only in the effective view, and a gateway-global policy replaces normal -sandbox/provider composition. - -## Keep Credentials Separate from Network Access - -A matching network rule permits traffic; it does not authorize every provider -credential at that destination. Credential bindings, request placeholders, and -the inspected transport determine whether OpenShell may inject a value. If a -connection succeeds but credential resolution fails, inspect the provider and -binding diagnostics instead of granting a broader endpoint or setting -`allow_uninspected_credentials`. - -Request-body and WebSocket credential rewriting apply only to their documented -text formats and size limits. Binary payloads and unsupported message -directions are not made safe by an allow rule. See [Provider -Profiles](/providers/profiles) for binding concepts and the [Policy Schema -Reference](/reference/policy-schema) for endpoint fields. +The provider must be attached to the sandbox, and its profile must define no +endpoints. The binding is valid only in a sandbox policy, not in a +gateway-global policy. A request that uses the credential elsewhere is rejected +with `credential_endpoint_mismatch`. Refer to [Static Credential Endpoint +Binding](/providers/profiles#understand-static-credential-endpoint-binding). + +If a connection succeeds but credential resolution fails, inspect the provider +and binding diagnostics instead of granting a broader endpoint or setting +`allow_uninspected_credentials`. Request-body and WebSocket credential rewriting +apply only to their documented text formats and size limits. For AWS request +signing, refer to [AWS SigV4](/providers/aws-sigv4). ## Next Steps From eb7a27da386cb394f30f4e89c6c988d558987ea6 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 17:10:41 +0000 Subject: [PATCH 06/40] docs(policy): restructure schema reference by field group and protocol Signed-off-by: Johnny Greco --- docs/how-it-works/policies/schema.mdx | 974 +++++++++++++++----------- 1 file changed, 549 insertions(+), 425 deletions(-) diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 14757ec2f9..f033531127 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -3,18 +3,18 @@ # 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." +description: "Field reference for the sandbox policy YAML, including defaults, matcher behavior, and validation constraints." keywords: "Generative AI, Cybersecurity, Policy, Schema, YAML, Reference, Security" -position: 3 +position: 6 --- -This reference defines the canonical authored YAML and JSON policy fields. It -distinguishes startup-time controls from dynamically activated controls and -documents defaults, matcher behavior, and validation constraints. +This reference defines every field in the authored YAML and JSON policy format, +with defaults, matcher behavior, and validation constraints. For how policies +are evaluated and applied, refer to [Sandbox Policies](/sandboxes/policies). ## Top-Level Structure -A policy YAML file contains the following top-level fields: +A policy file contains the following top-level fields: ```yaml showLineNumbers={false} version: 1 @@ -25,107 +25,62 @@ 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. | +| `filesystem_policy` | object | No | Startup | Controls which paths the workload can read and write. | +| `landlock` | object | No | Startup | Configures Landlock LSM enforcement behavior. | +| `process` | object | No | Startup | Sets the user and group that the workload runs as. | +| `network_policies` | map | No | Live | Declares which binaries can reach which network endpoints. | +| `network_middlewares` | map | No | Live | Attaches ordered middleware to allowed traffic by destination host. | The YAML root and each present top-level section listed as an object or map must be a mapping. Each named network policy or middleware entry must also be a mapping. -Startup-time fields are installed before the workload process starts. After +Startup fields are installed before the workload process starts. After activation, OpenShell rejects removals from the filesystem baseline and changes to workdir inclusion, Landlock mode, and process identity. Additive filesystem paths may be accepted into stored configuration but do not change the current -child. Recreate the sandbox for an assured new startup configuration. A sandbox -that has never activated can repair startup fields while configuration admission -is pending or rejected. +workload. Recreate the sandbox for an assured new startup configuration. A +sandbox that has never activated can repair startup fields while configuration +admission is pending or rejected. -Dynamic fields can activate on a running sandbox after the complete effective -candidate validates. `policy update` edits only `network_policies`; use an -editable base with `policy set` for middleware or complete replacement. -Middleware policy changes can select built-ins or external services already -registered with the gateway. Adding or changing an external service registration -requires a gateway restart. - -## 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 - -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`. | +Live fields can activate on a running sandbox after the complete effective +candidate validates. `openshell policy update` edits only `network_policies`. +Use `openshell policy set` with an editable base for middleware changes or +complete replacement. Middleware changes can select built-ins or external +services already registered with the gateway. Adding or changing an external +service registration requires a gateway restart. ## Filesystem Policy -**Category:** Static - -Controls filesystem access inside the sandbox. Paths not listed in either `read_only` or `read_write` are inaccessible. +Controls filesystem access inside the sandbox. Paths not listed in either +`read_only` or `read_write` are inaccessible, apart from the baseline paths +described in [Default Policy](/reference/default-policy). | Field | Type | Required | 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. | +| `include_workdir` | bool | No | When `true`, adds the driver-resolved working directory to `read_write`. | +| `read_only` | list of strings | No | Paths the workload can read but not modify. Typically system directories such as `/usr`, `/lib`, and `/etc`. | +| `read_write` | list of strings | No | Paths the workload can read and write. Typically `/tmp`. | + +An absent `filesystem_policy` resolves to `include_workdir: true`. An explicitly +present `filesystem_policy: {}` keeps `include_workdir: false`. -**Validation constraints:** +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. +- 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 path must not exceed 4096 characters. - The combined total of `read_only` and `read_write` paths must not exceed 256. Policies that violate these constraints are rejected with `INVALID_ARGUMENT` at creation or update time. An invalid embedded image policy blocks initial -workload activation for configuration repair; only a missing image policy +workload activation for configuration repair. Only a missing image policy selects the restrictive fallback. Example: @@ -146,35 +101,36 @@ filesystem_policy: ## 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. +Configures [Landlock LSM](https://docs.kernel.org/security/landlock.html) +enforcement for the user filesystem ruleset. Landlock provides mandatory +filesystem access control in the kernel, 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` | string | No | `best_effort`, `hard_requirement` | How OpenShell handles Landlock failures. Defaults to `best_effort`. | -These compatibility modes describe the optional user filesystem ruleset: +The runtime prepares the user filesystem ruleset as the workload identity. +Because it cannot distinguish an intentionally inaccessible path from a +misconfigured one, both modes skip an individual path that is missing or that +the workload cannot open, and apply the remaining rules. The modes differ when +no rule can be applied: -| Value | No paths configured | Kernel ABI unavailable | Individual path inaccessible | All paths inaccessible | -|---|---|---|---|---| -| `best_effort` | User ruleset skipped. | User ruleset warns and continues. | Skips the path and applies remaining user rules. | Warns and skips the empty user ruleset. | -| `hard_requirement` | Aborts sandbox startup. | Aborts sandbox startup. | Aborts when prepared with privileged path access; current-user preparation skips individually inaccessible paths and applies the remainder. | Aborts sandbox startup. | - -`best_effort` is the default for the user ruleset and handles missing paths -without discarding paths that can be enforced. On current Linux isolation paths, -OpenShell also installs a mandatory capability-free Landlock baseline that -protects `/.openshell` and requires ABI v3. The user `best_effort` value does not -disable that baseline or make an unsupported kernel compatible. - -`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. +| Value | No paths configured | Individual path missing or inaccessible | All paths missing or inaccessible | +|---|---|---|---| +| `best_effort` | User ruleset skipped. | Skips the path and applies the remaining rules. | Emits a high-severity detection finding and runs without the user ruleset. | +| `hard_requirement` | User ruleset skipped. | Skips the path and applies the remaining rules. | Aborts sandbox startup. | -When the runtime prepares user rules as the current workload identity, it cannot -distinguish an intentionally inaccessible path from a misconfigured one. It -skips individual inaccessible paths and applies the remainder. Other -`hard_requirement` failures still abort startup. +A `hard_requirement` ruleset also aborts startup when the kernel cannot apply +it. Use `hard_requirement` when running without the user ruleset is +unacceptable. It does not guarantee that every listed path is enforced. The +`Landlock ruleset built` event in the sandbox log reports how many paths were +applied and skipped. -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"). +On current Linux isolation paths, OpenShell also installs a mandatory +capability-free Landlock baseline that protects `/.openshell` and requires ABI +v3. Sandbox startup fails on a kernel without ABI v3 in either mode. The user +`best_effort` value does not disable that baseline or make an unsupported kernel +compatible. Example: @@ -185,19 +141,17 @@ landlock: ## Process -**Category:** Static - -Sets the OS-level identity for the agent process inside the sandbox. +Sets the OS-level identity for the workload process inside the sandbox. | Field | Type | Required | 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. +An explicit policy value must be `sandbox` or a numeric UID or 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 @@ -213,9 +167,10 @@ 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. +A map of named network rules. Each entry declares a set of endpoints and a set +of binaries, and allows each listed binary to reach each listed endpoint. The +map key is the rule's stable identifier and the selector for +`openshell policy update --rule-name`. ### Network Policy Entry @@ -223,121 +178,110 @@ Each entry in the `network_policies` map has the following fields: | 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 | No | Hosts and ports this entry permits. An omitted or empty list grants no destination. | -| `binaries` | list of binary objects | No | Executable selectors associated with the endpoints. Prefer explicit paths. Empty-scope runtime behavior depends on the trusted process-identity mode, so do not use an empty list as a portable any-process grant. | +| `name` | string | No | Display name used in log output. Defaults to the map key. | +| `endpoints` | list of endpoint objects | No | Destinations this entry permits. An omitted or empty list grants no destination. | +| `binaries` | list of binary objects | No | Executable selectors associated with the endpoints. In a sandbox, an omitted or empty list matches no program, so the entry allows nothing. Binaries are ignored only when trusted runtime configuration disables binary identity enforcement. | + +When present, `endpoints` and `binaries` must be lists of objects, even when +they contain only one entry. `null` cannot replace a list or an object entry. ### Endpoint Object -Each endpoint defines a reachable destination and optional inspection rules. +Each endpoint defines a reachable destination, how OpenShell inspects traffic +to it, and how provider credentials apply. The 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 | Conditional | One TCP port. Use either `port` or `ports` for clarity. When both are authored and `ports` is nonempty, `ports` takes precedence. | -| `ports` | list of integers | Conditional | One or more TCP ports. Takes precedence over scalar `port` when nonempty. At least one effective port is required for every protocol endpoint. | -| `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 for native TCP through policy DNS and transparent capture. Omit the field to define no protocol-specific request rules on the explicit proxy; default TLS detection and HTTP destination checks still apply. Use `tls: skip` for a deliberately raw proxy stream. Set to `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for the corresponding request 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. Remove those values and omit `tls` to use automatic handling. | -| `enforcement` | string | No | Defaults to `audit`. `enforce` actively blocks disallowed application requests. `audit` logs applicable request-rule violations but forwards the request; it does not bypass other parsing, destination, credential, or middleware checks. Other values are rejected before activation. Not valid with `protocol: tcp`. | -| `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 omit a protocol-specific request policy or use a `tls: skip` tunnel. 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](/providers/aws-sigv4). | -| `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 supported MCP core revisions. Omission allows only `2025-11-25`. See [MCP Version Selection](#mcp-version-selection). | -| `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`. | - -### MCP Version Selection - -`mcp.versions` accepts exact revisions from the closed supported set: -`2025-03-26`, `2025-06-18`, and `2025-11-25`. Omit the key to allow only -`2025-11-25`; omission never means the latest revision or all known revisions. -`versions: null`, `versions: []`, duplicate values, values with extra -whitespace, and revisions outside the supported set are invalid. +| `host` | string | Conditional | Hostname or IP address. Required for `protocol: tcp`. | +| `port` | integer | Conditional | One TCP port. Use either `port` or `ports`. | +| `ports` | list of integers | Conditional | One or more TCP ports. Takes precedence over `port` when nonempty. Every endpoint needs at least one effective port. | +| `path` | string | No | HTTP path glob that selects between inspected endpoints sharing a host and port, such as `/repos/**` and `/graphql`. Empty means all paths. | +| `allowed_ips` | list of strings | No | CIDR or IP allowlist for resolved destination addresses. | + +Host wildcards are allowed only inside the first DNS label. `*.example.com`, +`**.example.com`, and intra-label patterns such as `*-aiplatform.googleapis.com` +are accepted. Bare `*` or `**`, top-level-domain wildcards such as `*.com`, and +wildcards outside the first label are rejected at load time. A non-TCP proxy +endpoint may omit `host` only when `allowed_ips` supplies the destination +constraint. + +When several inspected endpoints share a host and port, the endpoint whose +`path` most specifically matches the request selects the parser and request +rules. A request that matches no endpoint path is denied. `path` is not valid +with `protocol: tcp`. On an endpoint without `protocol`, it does not restrict +access and narrows only which requests receive provider credentials. + +`allowed_ips` controls server-side request forgery (SSRF) protection. Exact +user-declared hostname endpoints may resolve to RFC 1918 private addresses +without this field. Wildcard, hostless, and policy-advisor-proposed endpoints +still require `allowed_ips` for private resolved addresses. When an endpoint +sets `allowed_ips`, every resolved address must fall within the list, including +public addresses. A hostless allowlist is valid only on the explicit proxy path, +matches any hostname on the port, 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`) addresses are rejected at load time. Those destinations, +including cloud metadata addresses, are always blocked. + +#### Inspection Fields -Except for a valid standalone `initialize` request, OpenShell selects the -revision from one `MCP-Protocol-Version` header. When the header is absent, it -uses the MCP compatibility fallback `2025-03-26`. A duplicate, empty, or -unsupported header value returns `400 Bad Request`. A supported revision that -is not in the endpoint allowlist returns `403 Forbidden`. This is a stateless -per-request header check; OpenShell does not negotiate a revision or bind -revision-specific session state. - -The sessionless `2026-07-28` revision is not yet supported. If a client requires -an unsupported revision, omit both `protocol` and `mcp` to forgo MCP-specific -request enforcement while retaining the explicit proxy's default TLS handling -and HTTP destination checks. Use `protocol: tcp` or `tls: skip` only when the -client actually requires a raw stream and the weaker boundary is acceptable. - -### Endpoint Serialization and Validation - -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. The standard runtime initializes the substrate unconditionally, so a running sandbox can add its first TCP endpoint through a live update. -- 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. - -### Matcher Semantics - -Different policy fields use different wildcard boundaries. - -| Matcher | Comparison | Important behavior | -|---|---|---| -| Endpoint `host` | Case-insensitive DNS name or IP comparison. | DNS `*` matches one label and `**` matches one or more labels. Host wildcard placement is restricted by validation. | -| Binary `path` | Canonical executable or trusted ancestor path. | Symlinks resolve to canonical identity. `*` matches within a path segment and `**` crosses directories. Identity enforcement depends on trusted runtime configuration. | -| REST or WebSocket request `path` | Case-sensitive URL path glob. | Both `*` and `**` can cross `/`; this differs from common shell glob intuition. | -| Query value | Case-sensitive decoded value glob. | For allow rules, every duplicate value for a configured key must match. | - -For a deny rule with query conditions, every configured key must be present and -each key must have at least one matching value. A duplicate nonmatching value -does not cancel another value that matches the deny. Deny rules still take -precedence over allows. +| Field | Type | Required | Description | +|---|---|---|---| +| `protocol` | string | No | `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for request inspection. `tcp` for native TCP without inspection. Omit for no protocol-specific request rules. | +| `tls` | string | No | Omit for automatic TLS handling. `skip` disables TLS detection and termination for the endpoint. | +| `enforcement` | string | No | `enforce` blocks requests that violate the endpoint's rules. `audit` logs them and forwards the request. Defaults to `audit`. | +| `access` | string | No | Access preset: `read-only`, `read-write`, or `full`. Mutually exclusive with `rules`. Refer to [Access Presets](#access-presets). | +| `rules` | list of allow rule objects | No | Protocol-specific allow rules. Mutually exclusive with `access`. | +| `deny_rules` | list of deny rule objects | No | Protocol-specific deny rules. A matching deny takes precedence over any allow. | +| `allow_encoded_slash` | bool | No | When `true`, request parsing preserves `%2F` inside path segments instead of rejecting it. Use it for APIs such as npm scoped packages (`/@scope%2Fname`). Defaults to `false`. | + +`protocol: tcp` provides ordinary DNS resolution and a native TCP socket through +policy DNS and transparent capture. When `protocol` is omitted, OpenShell +defines no protocol-specific request rules, but the explicit proxy's default TLS +detection and HTTP destination checks still apply. A `websocket` endpoint can +also use GraphQL operation rules for GraphQL-over-WebSocket traffic. +Provider-credentialed endpoints require an inspected protocol unless +`allow_uninspected_credentials` is set. + +The proxy detects TLS by peeking at the first bytes of each connection and +terminates it for inspected HTTPS traffic, so most endpoints omit `tls`. Set +`tls: skip` for cases such as client-certificate mTLS or nonstandard protocols. +Provider-credentialed endpoints reject `tls: skip` unless +`allow_uninspected_credentials` is set, and OpenShell never injects credentials +into a skipped tunnel. Use `tls: skip` only on endpoints that omit `protocol`, +because OpenShell copies tunneled bytes without evaluating request rules. All +endpoints that overlap on the same host and port must agree on `tls` and on +`allowed_ips`. `skip` is the only accepted non-empty value. Every other value, +including the removed `terminate` and `passthrough` spellings, is rejected +before persistence or activation. + +Other `enforcement` values are rejected before activation. `audit` does not +bypass parsing, destination, credential, or middleware checks, and +`enforcement` is not valid with `protocol: tcp`. + +Other `access` values are rejected before activation. `access` is 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. + +#### Credential Fields -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. +| Field | Type | Required | Description | +|---|---|---|---| +| `credential_binding` | object | No | Binds static credentials from an attached provider whose profile defines no endpoints. Valid only in a sandbox-scoped policy. | +| `credential_binding.provider` | string | With `credential_binding` | Exact name of the provider instance attached to the sandbox. | +| `request_body_credential_rewrite` | bool | No | Rewrites credential placeholders in supported REST request bodies. Defaults to `false`. Mutually exclusive with `credential_signing`. | +| `websocket_credential_rewrite` | bool | No | Rewrites credential placeholders in client WebSocket text messages on `rest` or `websocket` endpoints. Defaults to `false`. | +| `allow_uninspected_credentials` | bool | No | Security-sensitive opt-in that lets a provider-credentialed endpoint omit request inspection or use `tls: skip`. Defaults to `false`. | +| `credential_signing` | string | No | Proxy-side request signing: `sigv4`, `sigv4:body`, or `sigv4:no_body`. Mutually exclusive with `request_body_credential_rewrite`. | +| `signing_service` | string | With `credential_signing` | AWS service name for SigV4 signing, such as `bedrock`, `s3`, or `sts`. | +| `signing_region` | string | No | AWS region override for SigV4 signing, such as `us-east-1`. | + +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 @@ -350,6 +294,39 @@ 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). +`request_body_credential_rewrite` applies to UTF-8 `application/json`, +`application/x-www-form-urlencoded`, and `text/*` request bodies on +`protocol: rest` endpoints. 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. + +`websocket_credential_rewrite` applies to 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. + +Policy proposals that set `allow_uninspected_credentials` require explicit +security-flagged approval. + +With `credential_signing`, the proxy strips the sandbox client's +`Authorization` header and re-signs the request with real provider credentials. +`sigv4` detects the payload mode from client headers, `sigv4:body` buffers and +hashes the body up to 10 MiB, and `sigv4:no_body` streams an unsigned payload. +When `signing_region` is omitted, OpenShell extracts the region from the +endpoint hostname. Set it for nonstandard AWS endpoints where the region cannot +be inferred. 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`. Refer to [AWS SigV4](/providers/aws-sigv4). + This example allows the sandbox to reach Google Cloud Storage and binds the static credentials from the attached `work-gcp` provider to that endpoint: @@ -365,95 +342,215 @@ network_policies: provider: work-gcp ``` -#### Access Levels +#### Protocol Options -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. +| Field | Type | Required | Description | +|---|---|---|---| +| `persisted_queries` | string | No | GraphQL hash-only query behavior for `protocol: graphql` and GraphQL-over-WebSocket. `deny` (default) or `allow_registered`. | +| `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 options. Valid only with `protocol: mcp`. Omit the key to use all defaults. `mcp: null` is invalid. | +| `mcp.versions` | list of strings | No | Nonempty allowlist of supported MCP revisions. Omission allows only `2025-11-25`. Refer to [MCP Version Selection](#mcp-version-selection). | +| `mcp.max_body_bytes` | integer | No | Maximum MCP request body bytes buffered for inspection. Defaults to `65536`. | +| `mcp.strict_tool_names` | bool | No | Requires `tools/call` `params.name` values to match `^[A-Za-z0-9_.-]{1,128}$`. Defaults to `true`. | +| `mcp.allow_all_known_mcp_methods` | bool | No | Enables the endpoint MCP method profile. Defaults to `false`. Refer to [MCP Rules](#mcp-rules). | +| `json_rpc` | object | No | JSON-RPC options for `protocol: json-rpc`. | +| `json_rpc.max_body_bytes` | integer | No | Maximum JSON-RPC-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. | + +Use `persisted_queries: allow_registered` only with a +`graphql_persisted_queries` registry. Set `mcp.strict_tool_names: false` only +for compatibility with MCP servers that intentionally use non-recommended tool +names. Wildcard `tool` matchers require it to remain enabled. + +#### Endpoint Validation + +OpenShell rejects an endpoint that breaks any of these rules: + +- `access` and `rules` are mutually exclusive. +- For `rest`, `websocket`, and `graphql`, at least one of `access` or `rules` is + required. On an endpoint without `protocol`, `access` and `rules` have no + effect. +- `mcp` and `json-rpc` reject `access` presets. `json-rpc` requires explicit + `rules` with `allow.method`. `mcp` requires `rules` unless + `mcp.allow_all_known_mcp_methods: true`. +- `deny_rules` require `protocol`. For protocols other than MCP, `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: []` 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: []` is rejected. Remove it if no denials are needed. +- When present, `rules` and `deny_rules` must be lists of objects. + +`protocol: tcp` has additional constraints: + +- It requires a valid DNS hostname. Hostless `allowed_ips`, IP-literal hosts, + trailing-dot names, and malformed DNS selectors are rejected. +- It requires at least one port and rejects request-level fields, including + `path`, `enforcement`, `access`, `rules`, `deny_rules`, request rewriting and + credential signing fields, and GraphQL, JSON-RPC, or MCP options. +- Its 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. +- Prefer exact hosts. A wildcard authorizes DNS queries for all matching names + and can provide a DNS-label exfiltration channel. +- The sandbox runtime must support policy DNS and transparent TCP capture before + it can activate a TCP endpoint. Docker and Podman provide this support. The + standard runtime initializes it unconditionally, so a running sandbox can add + its first TCP endpoint through a live update. + +### Access Presets + +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 and path presets cannot authorize JSON-RPC safely. + +| Value | REST expansion | WebSocket expansion | GraphQL expansion | +|---|---|---|---| +| `full` | All methods and paths. | WebSocket upgrade and all inspected client text-message paths. | All operation types. | +| `read-only` | `GET`, `HEAD`, `OPTIONS`. | WebSocket upgrade handshake only. | `query` operations. | +| `read-write` | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH`. | WebSocket upgrade handshake and client text messages. | `query` and `mutation` operations. | -| 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. | +### Allow and Deny Rules -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. +Each entry in `rules` wraps its matcher fields in an `allow` object. Each entry +in `deny_rules` contains the same matcher fields directly, without a wrapper. +Deny rules take precedence: a request that matches any deny rule is blocked +regardless of what the allow rules or access preset permit. -#### Allow Rule Objects +```yaml showLineNumbers={false} +rules: + - allow: + method: GET + path: /repos/** +deny_rules: + - method: GET + path: /repos/private/** +``` -Used when `access` is not set. Each entry in `rules` contains an `allow` object. The tables below list the fields inside that `allow` object. +The matcher fields depend on the endpoint's `protocol`, as described in the +following sections. -##### REST Allow Rule (`protocol: rest`) +### REST Rules -REST allow rules match HTTP requests by method, path, and optional query parameters. +REST rules match HTTP requests by method, path, and optional query parameters. | 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, case-sensitive name. A matcher can be a glob string (`tag: "foo-*"`) or an object with `any` (`tag: { any: ["foo-*", "bar-*"] }`). Every duplicate value for a configured key must match an allow-rule matcher. | +| `method` | string | Yes | HTTP method, such as `GET` or `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, case-sensitive name. A matcher is a glob string (`tag: "foo-*"`) or an object with `any` (`tag: { any: ["foo-*", "bar-*"] }`). | -Example REST allow rules: +In an allow rule, every duplicate value for a configured query key must match. +In a deny rule, every configured key must be present with at least one matching +value, and an additional nonmatching duplicate does not cancel the match. ```yaml showLineNumbers={false} -rules: - - allow: - method: GET - path: /**/info/refs* - query: - service: "git-*" - - allow: - method: POST - path: /**/git-upload-pack - query: - tag: - any: ["v1.*", "v2.*"] +endpoints: + - host: api.github.com + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: + method: GET + path: /**/info/refs* + query: + service: "git-*" + - allow: + method: POST + path: /**/git-upload-pack + query: + tag: + any: ["v1.*", "v2.*"] + deny_rules: + - method: POST + path: "/repos/*/pulls/*/reviews" + - method: "*" + path: "/repos/*/rulesets" ``` -##### WebSocket Allow Rule (`protocol: websocket`) +### WebSocket Rules -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 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 +inspected or rewritten. | 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. | - -Example WebSocket allow rules: +| `method` | string | Yes | `GET` matches the upgrade handshake, `WEBSOCKET_TEXT` matches client text messages after the upgrade, and `*` matches both. | +| `path` | string | Yes | URL path glob from the original upgrade request, not message content. Same syntax as REST rules. | +| `query` | map | No | Query parameter matchers from the original upgrade request. Same syntax as REST rules. | ```yaml showLineNumbers={false} -rules: - - allow: - method: GET - path: /v1/realtime/** - - allow: - method: WEBSOCKET_TEXT - path: /v1/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: WEBSOCKET_TEXT + path: "/v1/admin/**" ``` -##### GraphQL Allow Rule (`protocol: graphql` or GraphQL-over-WebSocket) +### GraphQL Rules -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 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. | 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`, `subscription`, or `*`. | +| `operation_name` | string | No | Operation-name glob. Omit to match any operation name. | +| `fields` | list of strings | No | Root-field globs. Omit to match all root fields. | -Example GraphQL allow rules: +In an allow rule, every selected root field must match one configured glob. In +a deny rule, any matching root field blocks the request, and omitting `fields` +denies every operation that matches `operation_type` and `operation_name`. A +malformed, denied, or unregistered operation denies an entire batched HTTP +request. Hash-only persisted queries are denied unless the endpoint sets +`persisted_queries: allow_registered` with a trusted registry. ```yaml showLineNumbers={false} -rules: - - allow: - operation_type: query - fields: [viewer, repository] - - allow: - operation_type: mutation - operation_name: Issue* - fields: [createIssue] +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] ``` -Example GraphQL-over-WebSocket allow rules: +For GraphQL-over-WebSocket, use `protocol: websocket`, add a separate `GET` +allow rule for the upgrade, and use GraphQL rules for client operation messages. +OpenShell classifies `graphql-transport-ws` `subscribe` messages and legacy +`graphql-ws` `start` messages as operations. Lifecycle messages such as +`connection_init`, `ping`, `pong`, and `complete` are allowed as control-plane +messages and are not payload-logged. Do not combine `method`, `path`, or `query` +with `operation_type`, `operation_name`, or `fields` in the same rule. When a +WebSocket endpoint has GraphQL operation policy, use GraphQL rules for client +messages instead of a raw `WEBSOCKET_TEXT` allow rule. ```yaml showLineNumbers={false} rules: @@ -468,21 +565,48 @@ rules: fields: [viewer] ``` -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 Rules -##### 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. - -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. +MCP rules match sandbox-to-server MCP Streamable HTTP request bodies by MCP +method and optional tool selectors. | 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. +| `method` | string | Conditional | MCP method name, such as `initialize`, `tools/list`, `tools/call`, or an unknown extension method. Globs are accepted only for the `tools/` family, such as `tools/*`. Required unless `mcp.allow_all_known_mcp_methods` is `true`. Do not use `method: "*"`. | +| `tool` | string or matcher | No | Matcher for `tools/call` `params.name`. A glob string or `{ any: [...] }`. Omit to match every tool. | +| `params` | map | No | Lower-level matcher. MCP currently accepts only `params.name`. An explicit `params: null` is invalid. | + +OpenShell applies MCP rules with this behavior: + +- It parses the underlying JSON-RPC 2.0 envelope, validates known MCP request and + notification params, and preserves unknown extension methods as literal method + strings that rules can match. +- By default, explicit method rules are required, and rules with `tool` or + `params.name` must set `method: tools/call`. +- With `mcp.allow_all_known_mcp_methods: true`, rules can omit `method`, and tool + selectors are normalized to `tools/call`. If the endpoint also 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. +- By default, `tools/call` `params.name` must match the MCP-recommended tool-name + pattern. Wildcard `tool` matchers require `mcp.strict_tool_names` to remain + enabled. +- Tool argument matching is not supported. An allowed tool accepts any argument + payload. +- In a batch request, one denied call denies the full batch. +- JSON-RPC responses and server-to-client MCP messages on response bodies or SSE + streams are relayed but are not parsed for policy enforcement. + +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. Responses need no rules because MCP rules inspect only +client-to-server messages. This example allows both initialization messages and +selected tools. 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: @@ -512,27 +636,62 @@ endpoints: 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: +Every MCP endpoint must 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 Version Selection + +`mcp.versions` accepts exact revisions from the closed supported set: +`2025-03-26`, `2025-06-18`, and `2025-11-25`. Omit the key to allow only +`2025-11-25`. Omission never means the latest revision or all known revisions. +`versions: null`, `versions: []`, duplicate values, values with extra +whitespace, and revisions outside the supported set are invalid. At protobuf +ingress, an empty repeated `versions` field means omission and resolves to +`["2025-11-25"]`, because protobuf repeated fields do not preserve presence. + +An endpoint that omits `mcp.versions`, including one that omits the entire `mcp` +object, resolves immediately to the exact `2025-11-25` allowlist. Canonical +serialization and stored policy data contain that explicit list, so support for +a later revision cannot widen a normalized policy. OpenShell canonicalizes an +explicit list in semantic order. To authorize an intentional compatibility range +for an older server, set an explicit nonempty allowlist: ```yaml showLineNumbers={false} mcp: versions: ["2025-03-26", "2025-11-25"] ``` -OpenShell canonicalizes an explicit list in semantic order. See [MCP Version -Selection](#mcp-version-selection) for the stateless request-header behavior. +Except for a valid standalone `initialize` request, OpenShell selects the +revision from one `MCP-Protocol-Version` header before policy evaluation, and +repeats the check after middleware changes the request. When the header is +absent, it uses the MCP compatibility fallback `2025-03-26`, not the policy +default. A duplicate, empty, or unsupported header value returns +`400 Bad Request`. A supported revision that is not in the endpoint allowlist +returns `403 Forbidden`. This is a stateless per-request header check. OpenShell +does not negotiate a revision, bind revision-specific session state, or apply +the batch rules recorded in each revision's wire profile. -##### JSON-RPC Allow Rule (`protocol: json-rpc`) +The sessionless `2026-07-28` revision is not yet supported. If a client requires +an unsupported revision, omit both `protocol` and `mcp` to forgo MCP-specific +request enforcement while retaining the explicit proxy's default TLS handling +and HTTP destination checks. Use `protocol: tcp` or `tls: skip` only when the +client requires a raw stream and the weaker boundary is acceptable. + +### JSON-RPC Rules -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. +JSON-RPC rules match sandbox-to-server JSON-RPC-over-HTTP request objects by +method. They apply to single requests and batch requests. | 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. | +| `method` | string | Yes | Exact JSON-RPC method name, such as `reports.search`. `*` is accepted only as the all-methods sentinel. Other globs are rejected. | -Generic JSON-RPC policy `params` matchers are not supported. Allow rules match only the JSON-RPC method. - -Example JSON-RPC allow rules: +JSON-RPC rules do not support `params` matchers. For a batch, OpenShell +evaluates each call independently and denies the full batch if one call is +denied. Client-to-server JSON-RPC response frames in POST bodies are denied. +Server-to-client messages on HTTP response bodies are relayed but are not parsed +for policy enforcement. Use MCP `tool` rules for MCP tool calls. ```yaml showLineNumbers={false} endpoints: @@ -544,129 +703,33 @@ endpoints: 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. Every configured key must be present with at least one matching value. Additional nonmatching duplicate values do not cancel a match. | - -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) - -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. - -| 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`. | - -Example GraphQL deny rules: - -```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* + - method: reports.delete ``` -##### JSON-RPC Deny Rule (`protocol: json-rpc`) +### Binary Object -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. +Identifies an executable that can use the associated endpoints. | 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`. - -Example JSON-RPC deny rules: - -```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 -``` +| `path` | string | Yes | Canonical filesystem path selector. `*` matches within one path segment and `**` crosses directory boundaries. For example, `/sandbox/.vscode-server/**` matches executables below that directory tree. | -### Binary Object +OpenShell identifies a process by the executable the kernel reports for it, +never by its command line. A script therefore runs as its interpreter, so a rule +for a `pip` script must list the Python interpreter rather than the script path. +OpenShell resolves symlinks in exact policy paths to their canonical targets, so +symlink spelling does not bypass canonical path checks. Glob selectors are not +symlink-resolved and must match the canonical path. -Identifies an executable that is permitted to use the associated endpoints. -OpenShell resolves the executable's canonical identity and can match a trusted -ancestor process. Symlink spelling therefore does not bypass canonical path -checks. Trusted runtime configuration can disable binary identity enforcement, -so inspect the active runtime mode when diagnosing an unexpected result. +A binary selector also matches when it names an ancestor of the calling process, +so programs launched by a listed binary can use the rule. Trusted runtime +configuration can disable binary identity enforcement, so inspect the active +runtime mode when diagnosing an unexpected result. With identity enforcement enabled, OpenShell pins each executable or ancestor path used for authorization to its first observed SHA256 digest. A changed @@ -674,22 +737,16 @@ digest, missing evidence, or conflicting evidence denies access. Command-line paths do not authorize access. Restart the sandbox runtime after intentionally replacing an executable at an authorized path so it can establish a new pin. -| Field | Type | Required | Description | -|---|---|---|---| -| `path` | string | Yes | Canonical filesystem path selector. `*` matches within one path segment and `**` crosses directory boundaries. For example, `/sandbox/.vscode-server/**` matches executables below 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. Request middleware runs before provider -credential injection. Response middleware that advertises +A map of up to 10 middleware configs selected after network and request policy +admit an HTTP request or WebSocket upgrade. Request middleware runs before +provider credential injection. Response middleware that advertises `HTTP_RESPONSE/PRE_RETURN` runs on the final upstream response before return. WebSocket-capable bindings continue on client text messages after the upgrade. Selection is independent of the admitting network rule, and host matching alone does not select an operation the implementation did not advertise. Each matching -config runs once by ascending `order`; order values must be unique. +config runs once by ascending `order`, and order values must be unique. ```yaml showLineNumbers={false} network_middlewares: @@ -709,14 +766,46 @@ network_middlewares: |---|---|---|---| | `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. | +| `order` | integer | No | Execution priority. Lower values run first, and values must be unique across the policy. Defaults to `0`, so policies with multiple configs normally set 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. | +| `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 a required non-empty `include` list and an optional `exclude` list, 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 then bypasses the +middleware and emits a detection finding. + +Refer to [Supervisor Middleware](/extensibility/supervisor-middleware) for +registration, failure behavior, body limits, and operational guidance. + +## Matcher Semantics + +Different policy fields use different wildcard boundaries: -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. +| Matcher | Comparison | Behavior | +|---|---|---| +| Endpoint `host` | Case-insensitive DNS name or IP comparison. | DNS `*` matches one label and `**` matches one or more labels. Validation restricts wildcard placement. | +| Binary `path` | Canonical executable or trusted ancestor path. | Symlinks resolve to canonical identity. `*` matches within a path segment and `**` crosses directories. Identity enforcement depends on trusted runtime configuration. | +| REST or WebSocket request `path` | Case-sensitive URL path glob. | Both `*` and `**` can cross `/`, unlike common shell globs. | +| Query value | Case-sensitive decoded value glob. | In allow rules, every duplicate value for a configured key must match. | +| Middleware `endpoints` | Case-insensitive DNS name comparison. | Same as endpoint `host`. Brace alternates are rejected. | -See [Supervisor Middleware Configuration](/extensibility/supervisor-middleware/configure) for registration, failure behavior, and operational guidance. +For a deny rule with query conditions, every configured key must be present and +each key must have at least one matching value. A duplicate nonmatching value +does not cancel another value that matches the deny. ## Full Example @@ -749,6 +838,41 @@ network_policies: access: read-only allow_encoded_slash: true binaries: - - path: /usr/bin/npm - path: /usr/bin/node ``` + +## Parsing and Serialization + +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 own models. + +The parser requires `version: 1`, rejects duplicate mapping keys and YAML merge +keys, and accepts one document of at most 4 MiB. It also enforces these limits: + +- Nesting depth of 64 levels. +- 100,000 total AST nodes and 300,000 parser events. +- 4 MiB of cumulative scalar data. +- 100 alias expansions, with a 5:1 alias-to-anchor ratio. +- 10,000 entries in each mapping or sequence. + +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. + +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`, and any other unsupported +protobuf version is rejected. A protobuf port above 65535 is rejected rather +than clamped. + +The YAML representation keeps the field names shown on this page. Protobuf and +generated SDK clients use the `NetworkTlsMode`, `NetworkEnforcementMode`, and +`NetworkAccessPreset` enums for the `tls`, `enforcement`, and `access` fields. +Unspecified TLS keeps automatic handling, unspecified enforcement keeps the +audit default, and unspecified access selects no preset. Unknown enum numbers +are rejected before activation. From 0e9821d8d36488fea25042c48111b5e0d194e378 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 17:13:38 +0000 Subject: [PATCH 07/40] docs(policy): align troubleshooting, advisor, and reference pages Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 385 +++++++++++------- docs/how-it-works/policies/default-policy.mdx | 137 ++++--- docs/how-it-works/policies/prover.mdx | 8 +- docs/reference/policy-updates.mdx | 274 ++++++++----- docs/sandboxes/troubleshoot-policies.mdx | 217 +++++----- 5 files changed, 625 insertions(+), 396 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 4c9895dd7a..6e9bb70c89 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -5,16 +5,63 @@ 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." keywords: "Generative AI, Cybersecurity, Policy Advisor, Policy, Sandbox, policy.local, Agent Policy" -position: 9 +position: 4 --- -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 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. +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 waits in the draft inbox for human review. +Opt-in [auto mode](#automatic-approval) approves a proposal without a reviewer +only when the policy prover finds no new risk and the rule produces no security +notes. + +## How It Works + +When policy advisor is enabled, the sandbox supervisor turns on these +agent-facing surfaces: + +- It installs `/etc/openshell/skills/policy_advisor.md` inside the sandbox. +- It installs `/etc/openshell/skills/policy-advisor/SKILL.md` as a short + Codex and 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 request-level `policy_denied` + response bodies so the agent can find the skill and local API. + +A proposal moves through this loop: + +1. A sandboxed process attempts a network request that policy denies. 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`. +2. The agent reads the policy advisor skill, inspects the current policy, and + optionally reads recent denial log lines. +3. The agent submits one or more `addRule` proposals to + `http://policy.local/v1/proposals`. +4. The gateway turns each proposal into the exact effective-policy candidate it + would apply, validates it, and runs the [policy prover](#automatic-approval) + against it. +5. A developer approves or rejects the proposal. In auto mode, the gateway + approves an eligible proposal without review. +6. The agent waits on `/v1/proposals/{chunk_id}/wait` until a decision is + available. + +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, so the agent +can narrow the next attempt to the specific concern the prover flagged. ## 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: +Policy advisor is disabled by default. Enable it globally when you want every +sandbox on the selected gateway to expose the agent proposal surface: ```shell openshell settings set --global \ @@ -37,7 +84,9 @@ Check the effective setting for a sandbox: 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: +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: ```shell openshell settings delete --global \ @@ -45,72 +94,63 @@ 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. +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. -| 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: +Review pending proposals from the host: ```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: +The output shows the chunk ID, status, rationale, binary, endpoint summary, +prover result, application error (if any), and candidate hash. For request-level +proposals, the endpoint summary includes the protocol, method, and path: -```shell -openshell settings set \ - --key proposal_approval_mode \ - --value auto +```text +Endpoints: api.github.com:443 [L7 rest, allow PUT /repos/NVIDIA/OpenShell/contents/docs/**] ``` -The shorthand at create time writes the sandbox-scoped setting for you: +Approve only when the structured rule matches the access you intend to grant: ```shell -openshell sandbox create --approval-mode auto +openshell rule approve --chunk-id ``` -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. +Reject with guidance when the rule is too broad or points at the wrong target: -## How It Works +```shell +openshell rule reject \ + --chunk-id \ + --reason "Scope this to docs/ paths only." +``` -When policy advisor is enabled, the sandbox supervisor turns on three agent-facing surfaces: +The rejection reason is returned to the agent through `policy.local`. The agent +can use it to draft a narrower proposal. -- 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. +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. -The loop has eight steps: +### Review Tokens -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. +Each stored proposal records its candidate policy, prover result, any +application error, and a review token tied to the live base policy, provider +rules, and non-secret credential metadata. Provider rules remain immutable +inputs. ```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"] + B --> C["Validate merge, request 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"] @@ -120,39 +160,105 @@ flowchart TD I --> F ``` -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. +`openshell rule approve` fetches the current review token and submits it with +the approval. Before applying the proposal, the gateway recomputes the token +from live inputs. When the token is unchanged, it reuses the stored prover +result. When policy, provider, or credential inputs changed after the proposal +was displayed, the gateway evaluates and stores the refreshed candidate, leaves +the proposal pending, and asks you to review it again. Run `rule get` before +retrying. Bulk approval binds each selected proposal to its own review token in +the same way, and edits and deduplicated resubmissions follow the same path. -## What Gets Proposed +Merge, policy-shape, provider-composition, credential, or prover failures are +shown as application errors and cannot be approved. -OpenShell has two proposal paths: +## Automatic Approval -| Path | Source | Typical rule shape | +Every proposal, whether mechanistic or agent-authored, is routed through the +policy prover. The gateway also recalculates security notes from the current +draft rule. The `proposal_approval_mode` setting decides whether proposals that +pass both checks still require human review: + +| Proposal | `manual` or unset | `auto` | |---|---|---| -| 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. | +| Empty prover delta and no security notes | Waits 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 | Waits in the draft inbox. | Remains pending for human review. | + +`manual` is the default. Enable auto mode at gateway scope when you want every +sandbox on this gateway to auto-approve eligible proposals: + +```shell +openshell settings set --global \ + --key proposal_approval_mode \ + --value auto \ + --yes +``` + +Enable it for one sandbox when no global value is set: + +```shell +openshell settings set \ + --key proposal_approval_mode \ + --value auto +``` + +The create-time shorthand writes the sandbox-scoped setting for you: + +```shell +openshell sandbox create --approval-mode auto +``` -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. +Only `manual` and `auto` are accepted, and typos such as `autom` are rejected +when you set the value. Stale or unknown values found in storage are treated as +`manual` at runtime as a defense-in-depth measure. Gateway scope wins over +sandbox scope, so a reviewer can pin `manual` for a fleet by setting it +globally. Per-sandbox values apply only when no global value is set. -### How proposal provenance works +Under `auto` mode, approved proposals appear under +`openshell rule get --status approved` with auto-approval audit +fields. Under `manual` mode, every accepted proposal appears as pending +regardless of the prover verdict or security notes. -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?” +### What the Prover Checks -| 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. | +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. -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 prover asks four formal questions about the proposed change. Each "yes" is +one categorical finding, and any finding blocks auto-approval: + +| 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 request 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. | -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. +Findings are categorical, with no severity tier. The reviewer reads the category +and the structured evidence to decide. -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: +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 proposal pending in auto mode. -- 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`. +The full reasoning model is in +[`crates/openshell-prover/README.md`](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-prover/README.md). +Provider profiles composed in through [Profiles](/providers/profiles) are part +of the effective policy the prover reasons over. + +## What Agents Can Propose + +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 request samples are available, it can draft REST method and path rules. Otherwise it drafts an endpoint-only rule. | +| 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. | -For REST APIs, prefer L7 rules over broad L4 access. A good proposal allows one method and the smallest safe path: +For REST APIs, prefer request rules over broad endpoint access. A good proposal +allows one method and the smallest safe path: ```json { @@ -191,68 +297,56 @@ 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 [Configure Sandbox Policies](/sandboxes/policies) or [Policy Schema Reference](/reference/policy-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. +The `policy.local` proposal 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 workflow](/sandboxes/manage-policies). Omitting +`protocol` remains supported and keeps the explicit proxy's default TLS +termination and HTTP authority checks. Other fields, such as WebSocket +credential rewrite, GraphQL operation matching, endpoint path scoping, and +provider-owned policy bundles, are also outside the proposal surface. + +### Private and Blocked Destinations + +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 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. + +### Proposal Provenance + +OpenShell records internally whether policy advisor introduced an endpoint or a +binary. Provenance is not a policy YAML field. When an advisor proposal overlaps +an endpoint that a user or provider declared explicitly, the explicit +declaration wins and a provider rule stays immutable. For example, a GitHub +provider can declare read access to `api.github.com:443`, and an approved +proposal for `PUT /repos/NVIDIA/OpenShell/contents/docs/**` on the same endpoint +is stored in the sandbox policy layer. + +Provenance does not hide a real conflict. Overlapping declarations still fail +validation if they disagree on settings that must have one value, such as TLS +mode, `allowed_ips`, an equally specific protocol or parser contract, credential +binding, or enforcement mode. Compatible allow and deny rules can combine. + +Advisor-introduced endpoints and binaries do not establish exact-host SSRF +trust, which requires an explicit exact endpoint and matching binary in the same +rule. A proposal cannot extend a provider's binary identity to a different +binary. For example, if a provider declares `api.github.com:443` for +`/usr/bin/gh` and the advisor proposes the same endpoint for `/usr/bin/curl`, +the proposed rule for `curl` remains subject to the normal SSRF checks. ## Agent API @@ -268,28 +362,39 @@ or a network policy rule: | `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. | -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`. +If policy advisor is disabled, every route returns `404 feature_disabled`, the +skill is not installed for new sandboxes, and request-level deny bodies do not +advertise `policy.local` routes or include `agent_guidance`. -## What to Expect +## Audit Events Approved network rules hot-reload without restarting the sandbox. Connections attached to the previous policy generation close at the reload boundary, so a -retry opens against the current generation. Refer to -[Understand Reloads](/sandboxes/policies#understand-reloads) for HTTP, -WebSocket, tunnel, and long-lived stream behavior. +retry opens against the current generation. Refer to [How Changes Take +Effect](/sandboxes/policies#how-changes-take-effect) for HTTP, WebSocket, +tunnel, and long-lived stream behavior. -Policy advisor emits audit events into the sandbox log. Use these lines to trace the full loop: +Policy advisor emits audit events into the sandbox log. Use these lines to +trace the full loop: ```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 `HTTP:* DENIED`, `CONFIG:PROPOSED`, `CONFIG:APPROVED` or +`CONFIG:REJECTED`, `CONFIG:LOADED`, and the final allowed request if the agent +retries successfully. Every auto-approval emits `CONFIG:APPROVED` with +`auto=true`, `source=`, `prover_delta=empty`, and +`resolved_from=`, so operators can reconstruct why an +approval ran without human review. ## Next Steps -- Use [Configure Sandbox Policies](/sandboxes/policies) for manual policy updates and L7 rule syntax. -- Use [Policy Schema Reference](/reference/policy-schema) for full YAML field details. +- Use [Manage Sandbox Policies](/sandboxes/manage-policies) for manual policy + changes. +- Use [Network Policy Recipes](/sandboxes/network-policy-recipes) and the + [Policy Schema Reference](/reference/policy-schema) for rule syntax outside + the proposal surface. - Use the [Standalone Policy Prover](/reference/policy-prover) for optional boundary checks and its explicit modeled-domain limits. - Use [Logging](/observability/logging) to interpret OCSF shorthand log entries. diff --git a/docs/how-it-works/policies/default-policy.mdx b/docs/how-it-works/policies/default-policy.mdx index 7cfd84b23a..b7a5bee359 100644 --- a/docs/how-it-works/policies/default-policy.mdx +++ b/docs/how-it-works/policies/default-policy.mdx @@ -1,77 +1,112 @@ --- # 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: "Policy source selection and the restrictive fallback used when a sandbox has no explicit or embedded 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: 8 --- -OpenShell uses the restrictive fallback only when a sandbox has no stored policy -and image policy discovery finds no policy. Creating a sandbox without -`--policy` does not by itself prove that the fallback is active. +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. -## Policy Selection +## When the Default Applies -At creation, an explicit `--policy` file takes precedence over the -`OPENSHELL_SANDBOX_POLICY` environment variable. Either source supplies the -sandbox-authored policy stored by the gateway. +OpenShell uses the restrictive default only when a 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](/sandboxes/policies#where-the-active-policy-comes-from) for the complete +selection order. -When the gateway has no stored sandbox policy, the supervisor checks the image's -current embedded path, `/etc/openshell/policy.yaml`, and then the legacy path, -`/etc/navigator/policy.yaml`. A valid embedded policy initializes the missing -stored policy. A missing embedded policy selects the restrictive fallback. An -invalid embedded policy keeps the first workload unstarted for configuration -repair; it does not silently select the fallback. +## Default Filesystem Access -An active gateway-global policy replaces sandbox policy selection and suppresses -provider-added network grants. Otherwise, attached providers can add their -network rules to the selected sandbox policy. Refer to -[How OpenShell Selects a Policy](/sandboxes/policies#how-openshell-selects-a-policy) for -the selection order. +The default policy includes the sandbox working directory as read-write and +grants read-only access to these paths: -## Fallback Filesystem Access +- `/bin` +- `/usr` +- `/lib` +- `/proc` +- `/dev/urandom` +- `/etc` +- `/var/log` -The restrictive fallback includes the sandbox working directory and grants -read-only access to these paths: +It grants read-write access to `/tmp` and `/dev/null`. Landlock user-policy +compatibility is `best_effort`. -- `/bin`. -- `/usr`. -- `/lib`. -- `/proc`. -- `/dev/urandom`. -- `/etc`. -- `/var/log`. +## Default Network Access -It grants read-write access to `/tmp` and `/dev/null`. Landlock user-policy -compatibility defaults to `best_effort`. +The default policy defines no network rules or middleware, so all outbound +network access is denied. Attached providers can still add their network rules +to the effective policy. Provider rules are runtime composition, not fields in +the default policy, so inspect the base and effective views separately to +identify a provider-derived grant. + +## Default Process Identity + +The default policy leaves process identity 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. Other drivers apply their configured non-root +identity. + +## 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. It 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`. Filesystem paths cannot be +removed from a running sandbox, 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 to +the stored policy. -The runtime can enrich filesystem paths required for operation before applying -the user ruleset. On current Linux isolation paths, a separate mandatory -Landlock baseline protects the private `/.openshell` hierarchy and requires the -supported baseline ABI. The user-policy `best_effort` setting does not disable -that mandatory protection or promise support on a kernel that cannot install it. +### GPU Sandboxes -## Fallback Network Access +On the Docker and VM compute drivers, a sandbox that requests a GPU receives +additional paths when the corresponding GPU device is present: -The fallback defines no network policies or middleware. Outbound network access -is denied until sandbox-authored or provider-composed rules permit a destination -and executable. +| 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` | -Provider additions are runtime composition, not fields in the fallback policy. -Inspect the base and effective views separately when you need to identify a -provider-derived grant. +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 to +the stored policy. -## Fallback Process Identity +### Protected Paths -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. Other drivers apply their configured non-root identity. +On current Linux isolation paths, a mandatory Landlock baseline protects the +private `/.openshell` hierarchy 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. ## Inspect the Selected Policy -Inspect the sandbox-authored base and the gateway's effective representation: +Inspect the sandbox's base policy and the gateway's effective representation: ```shell openshell policy get --base diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index 85c00d8d77..c658a86206 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -5,7 +5,7 @@ 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" -position: 4 +position: 9 --- `openshell-prover` checks whether the authority in a local candidate policy is @@ -16,6 +16,12 @@ 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. +[Policy advisor](/sandboxes/policy-advisor#what-the-prover-checks) runs a +separate prover check automatically on each proposal, reporting whether the +proposal adds new reach compared with the current policy. Use this command when +you need to check a complete policy against a fixed boundary, for example in +CI. + ## Install the Prover The standard OpenShell installer includes `openshell-prover`: diff --git a/docs/reference/policy-updates.mdx b/docs/reference/policy-updates.mdx index 77e7e48a5e..4df0d8f733 100644 --- a/docs/reference/policy-updates.mdx +++ b/docs/reference/policy-updates.mdx @@ -1,25 +1,91 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Incremental Policy Update Reference" -sidebar-title: "Policy Updates" -description: "Command grammar, scope requirements, merge behavior, previews, and revision status for incremental sandbox policy updates." -keywords: "Generative AI, Cybersecurity, Policy, CLI, Incremental Update, Hot Reload" -position: 4 +title: "Policy Command Reference" +sidebar-title: "Commands" +description: "Flags, output, update syntax, merge behavior, and wait results for the openshell policy commands." +keywords: "Generative AI, Cybersecurity, Policy, CLI, Incremental Update, Hot Reload, Revision" +position: 7 --- -`openshell policy update` merges explicit operations into a sandbox's current -`network_policies` map. It does not edit filesystem, Landlock, process, or -middleware sections. Use `policy set` with an exported base policy for those -broader changes. +This reference describes the `openshell policy` commands and the related +sandbox commands that read or set a policy. For step-by-step workflows, refer to +[Manage Sandbox Policies](/sandboxes/manage-policies). -This reference describes the command's flags, input formats, and update -behavior. For the policy lifecycle and editing workflow, see -[Configure Sandbox Policies](/sandboxes/policies). Examples use `my-sandbox` -for an existing sandbox; adapt rule names, executable paths, and destinations -to its current base policy. +Every sandbox-scoped command accepts an optional sandbox name. When you omit it, +the CLI uses the last sandbox you used on the current gateway and workspace, and +prints the name it selected. The global `--gateway` and `--workspace` flags +select where the command runs. -## Command Flags +## Inspect a Policy + +`openshell policy get [NAME]` shows the current policy for a sandbox, a stored +revision, or the gateway-global policy. + +| Flag | Default | Purpose | +|---|---|---| +| `--base` | Off | Include the base policy, without provider-contributed rules. | +| `--full` | Off | Include the effective policy, including provider-contributed rules. Cannot be combined with `--base`. | +| `--rev ` | `0` | Show a stored revision. `0` shows the current effective policy, or the latest revision with `--global`. | +| `--global` | Off | Show the gateway-global policy. The sandbox name is ignored. | +| `-o`, `--output ` | `table` | `table` or `json`. | + +Without `--base` or `--full`, the command prints only metadata such as version, +hash, status, and policy source. With either flag, the table output prints the +metadata, a `---` separator, and the policy as YAML. The JSON output adds a +`policy` object with the same field names as the YAML, so +`jq '.policy'` extracts a complete policy file. + +A `Source` value of `global` means a gateway-global policy replaces the sandbox +policy. Reading the global policy requires the platform administrator role. + +## List Revisions + +`openshell policy list [NAME]` lists policy revisions for a sandbox or for the +gateway-global policy. + +| Flag | Default | Purpose | +|---|---|---| +| `--page-size ` | `20` | Maximum revisions per page. `0` selects 100, and the maximum is 1000. | +| `--page-token ` | Empty | Continuation token from a previous page. | +| `--global` | Off | List gateway-global policy revisions. The sandbox name is ignored. | +| `-o`, `--output ` | `table` | `table`, `yaml`, or `json`. | + +The table shows each revision's version, hash, status, creation time in epoch +milliseconds, and load error. Revision status is one of the following: + +| Status | Meaning | +|---|---| +| `Pending` | The gateway saved the revision, and the sandbox has not loaded it yet. | +| `Loaded` | The sandbox loaded the revision. Global revisions are marked loaded when set. | +| `Failed` | The sandbox rejected the revision, or the stored payload no longer passes the current schema. | +| `Superseded` | A newer revision was saved before this one loaded. | + +## Replace a Policy + +`openshell policy set [NAME] --policy ` replaces the complete policy for +a running sandbox or sets the gateway-global policy. + +| Flag | Default | Purpose | +|---|---|---| +| `--policy ` | Required | Path to the complete policy YAML file. | +| `--wait` | Off | Wait for the sandbox to load the revision. Refer to [Wait Results](#wait-results). Not supported with `--global`. | +| `--timeout ` | `60` | Timeout for `--wait`. | +| `--global` | Off | Apply the file as the gateway-global policy for every sandbox. | +| `--yes` | Off | Skip the confirmation prompt for `--global`. Required in non-interactive sessions. | + +The file replaces every section of the policy, so prepare it from the current +base as described in [Replace the Complete +Policy](/sandboxes/manage-policies#replace-the-complete-policy). The gateway +rejects a sandbox-scoped `set` while a gateway-global policy is active. Setting +the global policy requires the platform administrator role and takes effect +immediately. + +## Update Network Rules + +`openshell policy update [NAME]` merges explicit operations into a sandbox's +current `network_policies` map. It does not edit filesystem, Landlock, process, +or middleware sections, and it does not support `--global`. One command can contain several compatible operations. The gateway applies the batch atomically and persists at most one new revision. @@ -31,9 +97,9 @@ batch atomically and persists at most one new revision. | `--remove-rule ` | Remove a complete named `network_policies` entry. | | `--add-allow ` | Append a REST or WebSocket method and path allow rule. | | `--add-deny ` | Append a REST or WebSocket method and path deny rule. | -| `--binary ` | Add binaries with endpoints, or declare the complete stored binary scope for an L7 append. Repeat as needed. | -| `--rule-name ` | Name one new endpoint rule or select the existing rule for an L7 append. Required for `--add-allow` and `--add-deny`. | -| `--any-binary` | Declare that the existing L7 target stores an empty binary scope. Cannot be combined with `--binary`. | +| `--binary ` | Add binaries with endpoints, or declare the complete stored binary scope for a request-rule append. Repeat as needed. | +| `--rule-name ` | Name one new endpoint rule or select the existing rule for a request-rule append. Required for `--add-allow` and `--add-deny`. | +| `--any-binary` | Declare that the existing request-rule target stores an empty binary scope. Cannot be combined with `--binary`. | | `--endpoint-path ` | Select one existing endpoint by its exact stored path. Pass `''` to select no path. | | `--dry-run` | Fetch the current policy and preview the local merge without saving a revision. | | `--wait` | Poll for the submitted revision's result. Cannot be combined with `--dry-run`. | @@ -41,9 +107,9 @@ batch atomically and persists at most one new revision. `--any-binary` describes the target rule's stored merge scope. It is not an independent claim that every runtime identity mode admits every process. Prefer -explicit binary paths in authored rules and introductory workflows. +explicit binary paths in authored rules. -## Endpoint Specification +### Endpoint Specification `--add-endpoint` uses this grammar: @@ -56,19 +122,21 @@ host:port[:access[:protocol[:enforcement[:options]]]] | `host` | Required destination hostname. | | `port` | Required integer from 1 through 65535. | | `access` | `read-only`, `read-write`, or `full` for inspected endpoints. | -| `protocol` | `tcp`, `rest`, `websocket`, or `sql`. Use full policy YAML for GraphQL, MCP, and JSON-RPC. | -| `enforcement` | `enforce` or `audit`. Omission selects audit for inspected requests. | +| `protocol` | `tcp`, `rest`, or `websocket`. Use `policy set` with full policy YAML for GraphQL, MCP, and JSON-RPC. | +| `enforcement` | `enforce` or `audit`. Requires a protocol. Omission selects audit for inspected requests. | | `options` | Comma-separated options listed below. | -Endpoint options are: - | Option | Effect | |---|---| | `allowed-ip=` | Add a destination IP allowance. Repeat the option in the comma-separated list for multiple values. | -| `request-body-credential-rewrite` | Rewrite supported credential placeholders in inspected REST text bodies. | -| `websocket-credential-rewrite` | Rewrite supported placeholders in client WebSocket text messages on REST compatibility or WebSocket endpoints. | +| `request-body-credential-rewrite` | Rewrite supported credential placeholders in inspected REST text bodies. Requires `rest`. | +| `websocket-credential-rewrite` | Rewrite supported placeholders in client WebSocket text messages. Requires `rest` or `websocket`. | | `allow-uninspected-credentials` | Accept the security-sensitive exposure of provider credentials on an uninspected traffic path. | +The grammar has no `tls` option. Use `policy set` for `tls: skip`. When you omit +`--rule-name`, the CLI names a new rule `allow__`, with dots and +hyphens in the host replaced by underscores. + Read-only HTTP access for curl: ```shell @@ -89,7 +157,8 @@ openshell policy update my-sandbox \ --wait ``` -Read-only access to an internal HTTP API with an explicit destination IP allowance: +Read-only access to an internal HTTP API with an explicit destination IP +allowance: ```shell openshell policy update my-sandbox \ @@ -101,15 +170,12 @@ openshell policy update my-sandbox \ The empty access segment before `tcp` is required by the positional grammar. `protocol: tcp` rejects access, enforcement, and application-inspection options. -The standard runtime initializes policy DNS and transparent capture before the -workload starts, so a live update can add the first TCP endpoint. An alternate -backend must advertise the required capability and a ready substrate. An inspected REST or WebSocket endpoint needs an allow shape. For incremental creation, supply an access preset. Create the endpoint in one command, then add explicit allow or deny rules in a separate command. -## L7 Rule Specification +### Request Rule Specification `--add-allow` and `--add-deny` use this grammar: @@ -119,15 +185,16 @@ host:port[,port...]:METHOD:path_glob The host, complete port set, rule name, complete binary set, and optional endpoint path identify one existing REST or WebSocket endpoint. These flags do -not create an endpoint or change its scope. +not create an endpoint or change its scope. The CLI uppercases the method, and +the path must start with `/` or `**`. Quote specifications that contain `*`, `**`, `?`, or bracket classes. Request path globs are not shell path globs. Both `*` and `**` can cross `/` boundaries, `?` matches one character, and bracket classes are supported. The following examples target an existing `github_api` rule whose only binary -is `/usr/bin/gh` and whose REST endpoint is `api.github.com:443`. An allow append -permits POST requests to issue-creation paths: +is `/usr/bin/gh` and whose REST endpoint is `api.github.com:443`. An allow +append permits `POST` requests to issue-creation paths: ```shell openshell policy update my-sandbox \ @@ -137,7 +204,7 @@ openshell policy update my-sandbox \ --wait ``` -A deny append blocks POST requests to administration paths on that endpoint: +A deny append blocks `POST` requests to administration paths on that endpoint: ```shell openshell policy update my-sandbox \ @@ -164,48 +231,33 @@ Use `--endpoint-path` when a rule contains multiple endpoints with the same host and ports. This selector identifies the endpoint. It is separate from the request path appended by `--add-allow` or `--add-deny`. -## Complete Scope Requirements - -A network rule grants each listed binary access to each listed endpoint and -port, subject to application rules and other checks. A rule with two binaries -and two endpoints therefore includes four connection pairs: - -| Binary | Endpoint | -|---|---| -| `/usr/bin/curl` | `api.example.com:443` | -| `/usr/bin/curl` | `uploads.example.com:443` | -| `/usr/bin/python3` | `api.example.com:443` | -| `/usr/bin/python3` | `uploads.example.com:443` | - -If Python should reach only `api.example.com`, put that binary and endpoint in a -separate rule. Adding Python to the original rule would also authorize the -uploads endpoint. - -The CLI requires complete affected scope for L7 appends and for endpoint merges -that could create new pairs. For example, an endpoint that stores ports 443 and -8443 must be targeted with `443,8443`, even if the new method was observed only -on 443. Likewise, repeat every stored binary path or use `--any-binary` only -when the stored rule actually has an empty binary list. +### Complete Scope Requirements -Inspect the stored base before composing an append: +A network rule allows each listed binary to reach each listed endpoint, as +described in [Rules Pair Programs with +Destinations](/sandboxes/policies#rules-pair-programs-with-destinations). An +append that silently widened that scope could authorize new binary and endpoint +pairs, so the CLI requires complete affected scope for request-rule appends and +for endpoint merges that could create new pairs. -```shell -openshell policy get my-sandbox --base -``` +For example, an endpoint that stores ports 443 and 8443 must be targeted with +`443,8443`, even if the new method was observed only on 443. Likewise, repeat +every stored binary path, or use `--any-binary` only when the stored rule has an +empty binary list. -Copy the rule name, binary paths, endpoint path, and complete port set from that -view. Do not derive merge scope from `--full` when provider-owned rules are -present; those rules are not part of the sandbox base you can incrementally -edit. +Copy the rule name, binary paths, endpoint path, and complete port set from +`openshell policy get my-sandbox --base`. Do not derive merge scope from `--full` +when provider-owned rules are present. Those rules are not part of the sandbox +base you can incrementally edit. -The gateway rejects incomplete or ambiguous declarations before revision -persistence. Read the reported expected scope and correct the command. Do not -fill scope mechanically without confirming that the broader effect matches your +The gateway rejects incomplete or ambiguous declarations before it saves a +revision. Read the reported expected scope and correct the command. Do not fill +scope mechanically without confirming that the broader effect matches your intent. -## Remove Permissions +### Remove Permissions -Preview endpoint removal before applying it because `--remove-endpoint` is not +Preview endpoint removal before applying it, because `--remove-endpoint` is not scoped by `--rule-name`. It removes the matching host and port from every authored network rule: @@ -233,7 +285,7 @@ When a rule loses its final endpoint, OpenShell removes the rule instead of retaining an empty entry. Use `--remove-rule` when you intend to remove one specific map entry, and inspect the resulting base policy after either command. -## Preview a Merge +### Preview a Merge `--dry-run` shows the proposed policy without saving it. For example, this command previews a request-rule change to an existing `github_api` rule with @@ -249,11 +301,11 @@ openshell policy update my-sandbox \ The command validates argument shapes, connects to the gateway, fetches the current sandbox configuration, and applies the merge locally. It creates no -revision. An unavailable gateway therefore causes a connection failure. The -preview does not establish that a later submission will pass every effective -policy, provider, credential, or runtime validation. +revision, so an unavailable gateway causes a connection failure. The preview +does not establish that a later submission will pass every effective policy, +provider, credential, or runtime validation. -## Merge and Concurrency Behavior +### Merge and Concurrency Behavior All compatible flags in one command form one atomic batch. They succeed or fail together and persist at most one revision. `--add-endpoint` cannot share a batch @@ -269,51 +321,55 @@ human-readable `name` inside a YAML rule is not the `--rule-name` selector when the two differ. Use the map key shown by `policy get --base`. Incremental updates are unavailable while a gateway-global policy is active. -Delete the global override through the operator workflow before changing a -sandbox policy. +Delete the global policy before changing a sandbox policy. -## Wait and Status Semantics +## Delete the Global Policy -Without `--wait`, success means the gateway accepted the submission. It does not -mean the supervisor activated it. +`openshell policy delete --global` removes the gateway-global policy and +restores normal sandbox policy selection. Sandbox policies cannot be deleted, +so the command requires `--global`. -With `--wait`, the CLI polls until it observes a terminal outcome or timeout. A -zero exit can represent a loaded revision, a no-op, or a revision superseded by -another update. Always inspect current state before relying on the change: +| Flag | Purpose | +|---|---| +| `--global` | Required. Delete the gateway-global policy. | +| `--yes` | Skip the confirmation prompt. Required in non-interactive sessions. | -```shell -openshell policy list my-sandbox -openshell policy get my-sandbox --full -``` +Deleting the global policy marks its revisions `Superseded`. The gateway rejects +the deletion when the restored sandbox and provider configuration would be +invalid. -A wait timeout means the CLI stopped polling. It does not establish whether the -revision later loaded or failed. Inspect revision status and sandbox readiness -before resubmitting, because an automatic retry can race with a late result. +## Related Sandbox Commands -## Full Replacement +These sandbox commands also read or set a policy: -`openshell policy set` handles middleware changes, advanced protocol shapes, and complete -replacement. Start from the current base and preserve sections you do not intend -to change. Follow [Replace the -Policy](/sandboxes/policies#replace-the-policy) to prepare an editable -YAML file using the CLI's readable output. +| Command | Purpose | +|---|---| +| `openshell sandbox create --policy ` | Set the initial policy for a new sandbox. Overrides `OPENSHELL_SANDBOX_POLICY`. | +| `OPENSHELL_SANDBOX_POLICY=` | Environment variable that `sandbox create` reads when `--policy` is omitted. Other commands ignore it. | +| `openshell sandbox get [NAME] --policy-only` | Print only the effective policy as plain YAML, with no metadata. Cannot be combined with `--output`. | -For automation, the following alternative uses `jq` on the host to extract the -base policy without display metadata or provider-owned rules: +## Wait Results -```shell -set -o pipefail -openshell policy get my-sandbox --base --output json \ - | jq -e '.policy' > base-policy.json -``` +`policy set` and `policy update` accept `--wait`. Without it, success means +only that the gateway accepted the submission, not that the sandbox activated +it. With it, the CLI polls once per second until it observes a result: + +| Result | Exit code | +|---|---| +| The sandbox loaded the revision. | `0` | +| The policy was unchanged, so no revision was created. The CLI returns immediately. | `0` | +| A newer revision superseded this one before it loaded. | `0` | +| The sandbox rejected the revision. | `1` | +| The wait timed out. | `124` | -Edit `base-policy.json`, then submit the complete policy: +Because a zero exit can mean an unchanged or superseded revision, inspect +current state before relying on the change: ```shell -openshell policy set my-sandbox --policy base-policy.json --wait +openshell policy list my-sandbox +openshell policy get my-sandbox --full ``` -Refer to [Inspect the Current Policy](/sandboxes/policies#inspect-the-current-policy) -for effective export, and use -[Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) when submission -and activation results differ. +A timeout means the CLI stopped polling. It does not establish whether the +revision later loaded or failed. Inspect revision status and sandbox readiness +before resubmitting, because an automatic retry can race with a late result. diff --git a/docs/sandboxes/troubleshoot-policies.mdx b/docs/sandboxes/troubleshoot-policies.mdx index 064a4817b0..3052d65131 100644 --- a/docs/sandboxes/troubleshoot-policies.mdx +++ b/docs/sandboxes/troubleshoot-policies.mdx @@ -2,10 +2,10 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Troubleshoot Sandbox Policies" -sidebar-title: "Troubleshoot Policies" +sidebar-title: "Troubleshooting" description: "Identify policy failure stages and repair connection, request, credential, middleware, and activation problems." keywords: "Generative AI, Cybersecurity, Policy, Troubleshooting, Sandbox, Validation, Credentials, Middleware" -position: 8 +position: 5 --- Policy failures can occur while submitting a change, activating it, or processing @@ -30,72 +30,17 @@ Use what you observe and the latest revision status to locate the failure. | What you see | Failure stage | Existing access | What to do | |---|---|---|---| +| The client receives `policy_denied`, a credential error, or a middleware error. | Connection, request, credential, or middleware enforcement | The active network configuration remains. | Diagnose that control instead of widening the endpoint. | | The CLI reports an invalid file or argument before showing a revision. | Local command parsing | Unchanged. | Fix the file or arguments. | | The CLI reports that the candidate was rejected and no revision was created. | Gateway validation | Unchanged. | Fix the reported schema, scope, provider, credential, or compatibility error. | -| The sandbox remains `Provisioning` with `ConfigurationInvalid`. | Initial runtime activation | No workload has activated. | Repair the effective policy or provider configuration during the repair window. | -| A revision fails and new egress stops. | Runtime activation with `fail_closed` | No. Existing connections close. | Submit a valid repair and verify recovery. | -| A revision fails but earlier network access still works. | Runtime activation with `retain_last_valid` | The last valid configuration remains active. | Repair the candidate; working access does not mean the change loaded. | | `--wait` times out. | Status is not yet known. | Do not infer it from the timeout. | Inspect the revision and readiness before retrying. | -| The client receives `policy_denied`, a credential error, or a middleware error. | Request, credential, or middleware enforcement | The active network configuration remains. | Diagnose that control instead of widening the endpoint. | +| A revision fails and new egress stops. | Runtime activation with `fail_closed` | No. Existing connections close. | [Submit a valid repair](#repair-a-runtime-rejection) and verify recovery. | +| A revision fails but earlier network access still works. | Runtime activation with `retain_last_valid` | The last valid configuration remains active. | [Repair the candidate](#repair-a-runtime-rejection). Working access does not mean the change loaded. | +| The sandbox remains `Provisioning` with `ConfigurationInvalid`. | Initial runtime activation | No workload has activated. | [Repair the configuration](#repair-initial-configuration) during the repair window. | -The gateway checks a proposed policy before saving it. The runtime checks the -resulting configuration again when the sandbox starts or reloads it, because -the runtime also sees concurrent changes and other policy sources. The gateway -setting that selects runtime rejection behavior is -`[openshell.gateway] policy_validation_failure_mode`; refer to -[Gateway Configuration](/reference/gateway-config) for its deployment contract. - -## Repair Initial Configuration - -Before first workload activation, an invalid effective policy or provider bundle -keeps the sandbox in `Provisioning` with a `ConfigurationInvalid` condition. The -gateway gives you a 300-second repair window. An effective policy, settings, -provider, profile, or attachment change resets the window from its stored change -time, and the first rejection for that change receives a full window. Repeated -failures do not extend it. - -Inspect the diagnostic: - -```shell -openshell sandbox get my-sandbox --output json -``` - -Repair the complete policy or the conflicting provider configuration. A -sandbox that has never started its workload can also replace startup settings -during the repair window. - -If the window expires, the sandbox enters `Error` with reason -`ProvisioningTimedOut`. The gateway reclaims workload and supervisor compute but -retains the sandbox record and diagnostic. Repair the configuration, wait for -cleanup, then start it explicitly: - -```shell -openshell sandbox get my-sandbox --output json -openshell sandbox start my-sandbox -``` - -A start retry receives a new 300-second window. Editing configuration after -timeout does not restart compute by itself. - -## Repair a Runtime Rejection - -The default runtime failure mode is `fail_closed`. An invalid candidate blocks -network access and closes connections that used the previously active -configuration. Submit a valid replacement, wait, and verify the active revision: - -```shell -openshell policy set my-sandbox --policy repaired-policy.yaml --wait -openshell policy list my-sandbox -``` - -With `retain_last_valid`, a previous valid configuration remains active. Do not -interpret working network access as adoption of the failed candidate. If no -previous valid configuration exists, the effective behavior is still -fail-closed. - -An OCSF configuration event reports the candidate, validation rationale, -configured and effective failure modes, the active configuration version, and -whether a previous policy remains active. +OCSF policy events are INFO-level log records, regardless of their event +severity. A `--level warn` filter on `openshell logs` excludes them, so omit the +level filter when you look for policy decisions. ## Diagnose Connection Denials @@ -117,16 +62,16 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ readlink -f /usr/bin/curl ``` -Binary matching can also admit a trusted ancestor, and trusted runtime -configuration can disable identity enforcement. Inspect the logged process -identity and current runtime mode instead of assuming an empty binary list has -one universal meaning. Prefer explicit binary selectors. +OpenShell identifies the process by the executable the kernel reports, so a +script such as `pip` or `npm` appears as its interpreter, such as +`/usr/bin/python3.12` or `/usr/bin/node`. Binary matching can also admit a +trusted ancestor. An empty binary list matches no program unless trusted runtime +configuration disables identity enforcement. Compare the logged binary with the +policy's selectors before widening a rule. -The standard runtime initializes policy DNS and transparent capture before the -workload starts, even when the initial policy has no TCP endpoints. You can add -the first TCP endpoint through a live update. If the error reports missing TCP -support or connection-handling setup, the selected runtime is not ready to -enforce native TCP rules. Changing the policy cannot supply that runtime support. +If the error reports missing TCP support or connection-handling setup, the +selected runtime is not ready to enforce native TCP rules. Changing the policy +cannot supply that runtime support. ## Diagnose Request Denials @@ -136,13 +81,15 @@ endpoint path selector. For HTTP, the request authority must agree with the policy destination. A non-default port in an absolute-form URL or `Host` authority must match the -transport destination. OpenShell rejects authority mismatches even when the -endpoint otherwise permits the method and path. +transport destination. OpenShell rejects a mismatch with +`request_authority_mismatch`, even when the endpoint otherwise permits the +method and path. For a tunnel to `api.example.com:8443`, send +`Host: api.example.com:8443`. Check that rules intended to block requests use `enforcement: enforce`. -Omitted enforcement is audit mode. Audit forwards an applicable request-rule violation after logging -it, but it does not bypass parsing, destination, credentials, middleware, or -other independent checks. +Omitted enforcement is audit mode. Audit forwards an applicable request-rule +violation after logging it, but it does not bypass parsing, destination, +credentials, middleware, or other independent checks. Overlapping rules can contribute permissions. An applicable request deny wins over an allow, while the most-specific compatible endpoint selects the parser @@ -206,36 +153,116 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ ``` Compare the client error with a fresh event from -`openshell logs my-sandbox --since 5m --source sandbox`. A WARN -minimum level excludes OCSF entries because log filtering treats them as INFO, -regardless of their event severity. +`openshell logs my-sandbox --since 5m --source sandbox`. -## Restore an Earlier Base Revision +## Repair Initial Configuration -Choose a compatible revision from `openshell policy list my-sandbox`, then inspect its -base policy. For example, to retrieve revision 2: +Before first workload activation, an invalid effective policy or provider bundle +keeps the sandbox in `Provisioning` with a `ConfigurationInvalid` condition. The +gateway gives you a 300-second repair window. An effective policy, settings, +provider, profile, or attachment change resets the window from its stored change +time, and the first rejection for that change receives a full window. Repeated +failures do not extend it. + +Inspect the diagnostic: ```shell -openshell policy get my-sandbox --rev 2 --base +openshell sandbox get my-sandbox --output json ``` -Copy the YAML below the `---` separator into `previous-base.yaml`, excluding -the status metadata above it. Review the complete policy, then submit it as a -new revision: +Repair the complete policy or the conflicting provider configuration. A +sandbox that has never started its workload can also replace startup settings +during the repair window. An invalid image policy is repaired the same way, +because OpenShell does not fall back to the default policy. + +If the window expires, the sandbox enters `Error` with reason +`ProvisioningTimedOut`. The gateway reclaims workload and supervisor compute but +retains the sandbox record and diagnostic. Repair the configuration, wait for +cleanup, then start it explicitly: ```shell -openshell policy set my-sandbox --policy previous-base.yaml --wait +openshell sandbox get my-sandbox --output json +openshell sandbox start my-sandbox +``` + +A start retry receives a new 300-second window. Editing configuration after +timeout does not restart compute by itself. + +## Repair a Runtime Rejection + +The gateway checks a proposed policy before saving it. The runtime checks the +resulting configuration again when the sandbox starts or reloads it, because +the runtime also sees concurrent changes and other policy sources. The gateway +setting `[openshell.gateway] policy_validation_failure_mode` selects what +happens when the runtime rejects a candidate. Refer to +[Gateway Configuration](/reference/gateway-config) for its deployment contract. + +The default failure mode is `fail_closed`. An invalid candidate blocks network +access and closes connections that used the previously active configuration. +Submit a valid replacement, or [roll back to an earlier +revision](/sandboxes/manage-policies#roll-back-to-an-earlier-revision), then +wait and verify the active revision: + +```shell +openshell policy set my-sandbox --policy repaired-policy.yaml --wait openshell policy list my-sandbox ``` -This does not restore provider profiles, attachments, credentials, or global -configuration. If a global override is active, sandbox updates remain blocked. -Deleting that override can itself be rejected when the restored sandbox and -provider composition is invalid. +With `retain_last_valid`, a previous valid configuration remains active. Do not +interpret working network access as adoption of the failed candidate. If no +previous valid configuration exists, the effective behavior is still +fail-closed. + +An OCSF configuration event reports the candidate, validation rationale, +configured and effective failure modes, the active configuration version, and +whether a previous policy remains active. + +## Read Policy Load Errors + +When the sandbox 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. + +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. Use the category to decide 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. +- `invalid L7 policy configuration` reports a semantic request-rule error, and + `invalid L7 protocol configuration` reports a protocol configuration or alias + error. +- `ambiguous network endpoint selectors` reports conflicting endpoints. +- 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 +configuration or advance its generation. These bounds apply to load errors +only. Warnings for accepted policies, runtime request diagnostics, and gateway +policy parser messages have separate reporting behavior. + +## Error Reference + +These error codes and conditions identify policy-related failures: + +| Code or condition | Where it appears | Meaning | Diagnosis | +|---|---|---|---| +| `policy_denied` | HTTP 403 response body and sandbox log. | A request rule or missing rule blocked the request. | [Request denials](#diagnose-request-denials) | +| `request_authority_mismatch` | HTTP 403 response body and sandbox log. | The HTTP request authority differs from the authorized tunnel destination. | [Request denials](#diagnose-request-denials) | +| `credential_endpoint_mismatch` | HTTP 403 response body and sandbox log. | The policy allowed the request, but the provider credential is not bound to this host, port, or path. | [Credential denials](#diagnose-credential-denials) | +| `credential_placeholder_in_request_body` | HTTP 403 response body. | A request body contains an invalid or unavailable credential placeholder. | [Credential denials](#diagnose-credential-denials) | +| `ConfigurationInvalid` | Sandbox condition while `Provisioning`. | The initial effective policy or provider configuration failed validation. | [Initial configuration](#repair-initial-configuration) | +| `ProvisioningTimedOut` | Sandbox `Error` reason. | The repair window expired before the configuration became valid. | [Initial configuration](#repair-initial-configuration) | +| `feature_disabled` | `policy.local` response inside the sandbox. | Policy advisor is disabled for the sandbox. | [Enable Policy Advisor](/sandboxes/policy-advisor#enable-policy-advisor) | ## Next Steps -- Use [Configure Sandbox Policies](/sandboxes/policies) for the common update - and replacement workflow. +- Use [Manage Sandbox Policies](/sandboxes/manage-policies) to apply, verify, + and roll back policy changes. - Use the [Policy Schema Reference](/reference/policy-schema) for exact field defaults and validation constraints. +- Use [Logging](/observability/logging) to read OCSF policy events. From 6d894db6de3c850c003a9f1c50142097c421c73d Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 17:15:51 +0000 Subject: [PATCH 08/40] docs(policy): fix first policy tutorial and security guidance Signed-off-by: Johnny Greco --- docs/security/best-practices.mdx | 19 ++++---- docs/tutorials/first-network-policy.mdx | 63 ++++++++++++------------- 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index 20335c7c8b..8f4afc2e7f 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` and `tls: skip` endpoints relay the stream without inspecting payloads. 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, or `tls: skip` for proxy clients that must complete TLS with the upstream service. For WebSocket endpoints that must carry placeholder credentials in client text frames, add `websocket_credential_rewrite: true`. | ### Enforcement Mode (`audit` vs `enforce`) @@ -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,9 +296,10 @@ The following patterns weaken security without providing meaningful benefit. ## Related Topics -- [Policies](/how-it-works/policies/overview) 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. -- [Architecture](/about/architecture) for the system architecture. +- [Sandbox Policies](/sandboxes/policies) for how policies are evaluated and applied. +- [Manage Sandbox Policies](/sandboxes/manage-policies) for applying and iterating on sandbox policies. +- [Policy Schema](/reference/policy-schema) for the full field-by-field YAML reference. +- [Default Policy](/reference/default-policy) for the built-in default policy breakdown. +- [Gateway Auth](/reference/gateway-auth) for gateway authentication details. +- [Architecture](/about/how-it-works) for the system architecture. - NemoClaw [Security Best Practices](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) for entrypoint-level controls (capability drops, PATH hardening, build toolchain removal), policy presets, provider trust tiers, and posture profiles. diff --git a/docs/tutorials/first-network-policy.mdx b/docs/tutorials/first-network-policy.mdx index bed3335666..fc3ce27930 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:no matching 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](/reference/policy-schema). +- To allow other services, such as PyPI, npm, or your internal APIs, adapt the [Network Policy Recipes](/sandboxes/network-policy-recipes). @@ -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. The body begins with fields like these, followed by 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/repos/octocat/hello-world/issues [policy:github_api engine:opa] ``` -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 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). +- To understand how OpenShell evaluates network rules, refer to [Sandbox Policies](/sandboxes/policies). +- To walk through a full policy iteration with Claude Code, including diagnosing denials and applying fixes from outside the sandbox, refer to [GitHub Sandbox](/get-started/tutorials/github-sandbox). From fdbf537f39000db70b3aaa9c8e4dc32f40b8a4a3 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 18:47:56 +0000 Subject: [PATCH 09/40] docs(policy): keep overview high level and move network rules to their own page Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 2 +- docs/how-it-works/policies/overview.mdx | 269 ++++-------------- docs/how-it-works/policies/schema.mdx | 10 +- docs/reference/policy-updates.mdx | 6 +- docs/sandboxes/manage-policies.mdx | 12 +- ...k-policy-recipes.mdx => network-rules.mdx} | 185 ++++++++++-- docs/sandboxes/troubleshoot-policies.mdx | 2 +- docs/tutorials/first-network-policy.mdx | 2 +- 8 files changed, 224 insertions(+), 264 deletions(-) rename docs/sandboxes/{network-policy-recipes.mdx => network-rules.mdx} (74%) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 6e9bb70c89..26c0718140 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -392,7 +392,7 @@ approval ran without human review. - Use [Manage Sandbox Policies](/sandboxes/manage-policies) for manual policy changes. -- Use [Network Policy Recipes](/sandboxes/network-policy-recipes) and the +- Use [Network Rules](/sandboxes/network-rules) and the [Policy Schema Reference](/reference/policy-schema) for rule syntax outside the proposal surface. - Use the [Standalone Policy Prover](/reference/policy-prover) for optional diff --git a/docs/how-it-works/policies/overview.mdx b/docs/how-it-works/policies/overview.mdx index 1a8f2f31c8..92e1078daf 100644 --- a/docs/how-it-works/policies/overview.mdx +++ b/docs/how-it-works/policies/overview.mdx @@ -3,202 +3,46 @@ # SPDX-License-Identifier: Apache-2.0 title: "Sandbox Policies" sidebar-title: "Overview" -description: "Understand what sandbox policies control, how OpenShell evaluates network rules, where the active policy comes from, and how changes take effect." +description: "Understand what sandbox policies control, where a sandbox's policy comes from, and how policy changes take effect." keywords: "Generative AI, Cybersecurity, Policy, Network Policy, Sandbox, Security, Hot Reload" position: 1 --- -A sandbox policy is a declarative YAML document that defines what programs -inside an OpenShell sandbox can do. It controls which files they can read and -write, which user they run as, which network destinations each program can -reach, and which application requests it can send. OpenShell denies anything the -policy does not allow. +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](/get-started/tutorials/first-network-policy). -To create, change, and verify policies, refer to +To create and change policies, refer to [Manage Sandbox Policies](/sandboxes/manage-policies). ## What a Policy Controls -A policy contains up to five sections. Different parts of the sandbox runtime -enforce each section, and each section takes effect at a specific time: +A policy file has up to five 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 programs can read, or read and write. | Landlock LSM in the kernel. | At sandbox startup. | -| `landlock` | Whether an unsupported kernel or inaccessible path aborts startup. | Sandbox startup checks. | At sandbox startup. | -| `process` | User and group for sandbox processes. | Identity change before the workload starts. | At sandbox startup. | -| `network_policies` | Destinations each program can reach and requests it can send. | Sandbox supervisor proxy. | While the sandbox runs. | -| `network_middlewares` | Additional inspection or transformation of allowed traffic. | Sandbox supervisor proxy. | While the sandbox runs. | - -The following policy uses every section: - -```yaml showLineNumbers={false} -version: 1 - -# Startup: paths the workload can read, or read and write. -filesystem_policy: - include_workdir: true - read_only: [/usr, /lib, /etc] - read_write: [/tmp] - -# Startup: behavior when the kernel or a listed path cannot support a rule. -landlock: - compatibility: best_effort - -# Startup, optional: override the identity selected by the compute driver. -# process: -# run_as_user: "1500" -# run_as_group: "1500" - -# Live: which programs can reach which destinations. -network_policies: - github_api: - name: github-api - endpoints: - - host: api.github.com - port: 443 - protocol: rest - enforcement: enforce - access: read-only - binaries: - - path: /usr/bin/curl - -# Live: middleware applied to allowed traffic, selected by destination host. -network_middlewares: - regex-redactor: - name: Redact API tokens - middleware: openshell/regex - order: 10 - config: - mode: redact - on_error: fail_closed - endpoints: - include: ["api.github.com"] -``` - -When a policy allows network access, OpenShell also adds baseline filesystem -paths that sandbox processes need, such as `/usr`, `/lib`, and `/tmp`, so a -policy can focus on the paths specific to its workload. The -[Default Policy](/reference/default-policy) page lists those paths. The -[Policy Schema Reference](/reference/policy-schema) defines every field. - -## How Network Rules Work - -Every outbound connection from a sandbox passes through the sandbox supervisor, -which checks it against `network_policies`. If no rule allows the connection, -the supervisor denies it. - -### Rules Pair Programs with Destinations - -Each entry in `network_policies` is a named rule with a list of `endpoints` and -a list of `binaries`. The rule allows each listed binary to reach each listed -endpoint. A rule with two binaries and two endpoints therefore grants four -connection pairs: - -| Binary | Endpoint | -|---|---| -| `/usr/bin/curl` | `api.example.com:443` | -| `/usr/bin/curl` | `uploads.example.com:443` | -| `/usr/bin/python3` | `api.example.com:443` | -| `/usr/bin/python3` | `uploads.example.com:443` | - -If Python should reach only `api.example.com`, put that binary and endpoint in a -separate rule. - -OpenShell identifies the calling program by its canonical executable path, so a -symlink does not change which rule applies. A rule can also match a trusted -ancestor process, such as the agent that launched a tool. OpenShell pins each -authorized executable to the file digest it first observes, and denies access if -the file at that path later changes. - -List the executable that makes the connection, which is not always the command -you type. A script such as `pip` runs under its interpreter, so a rule for -`pip` must list the Python interpreter. Any program that a listed binary -launches can also use the rule. A rule with an empty `binaries` list matches no -program in a sandbox. - -### Connection and Request Checks - -OpenShell checks network traffic in two layers. The connection check applies to -every connection, and requires the destination host and port and the calling -binary to match a rule. For an endpoint with an inspected `protocol`, OpenShell then -parses each application request and checks it against the endpoint's `access` -preset or `rules`. - -The `protocol` field selects what OpenShell inspects after the connection check: - -| `protocol` | What OpenShell checks | -|---|---| -| `rest` | HTTP method, path, and query values. | -| `websocket` | The upgrade request and complete client text messages. | -| `graphql` | GraphQL operation type, operation name, and root fields. | -| `mcp` | MCP methods and tool names in client requests. | -| `json-rpc` | JSON-RPC method names in client requests. | -| `tcp` | Nothing beyond the connection. The program gets a native TCP socket and OpenShell does not inspect the payload. | -| Omitted | No request rules. OpenShell still terminates TLS, parses HTTP requests, and checks that each request's authority matches the destination. | - -Inspected protocols can allow reads while blocking writes on the same API, which -a connection-only rule cannot do. Use an inspected protocol when the request, -not only the destination, determines the risk. - -### Enforce and Audit - -The `enforcement` field on an inspected endpoint decides what happens when a -request violates the endpoint's rules: - -- `enforce` blocks the request and returns an OpenShell `policy_denied` response. -- `audit` logs the violation and forwards the request. Audit is the default. - -Audit mode lets you observe traffic before you enforce a rule, but it blocks -nothing. Set `enforcement: enforce` on every endpoint whose request rules must -block traffic. Audit applies only to request rules. It does not bypass the -destination, binary, credential, or middleware checks. - -### Overlapping Rules - -Network rules are not an ordered firewall list. Several rules can match the same -connection or request, and every matching allow contributes permission. An -applicable deny takes precedence over any allow, regardless of where either -appears in the file. - -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/**`. Requests -under `/repos/private/` are denied. When you restrict access, review every rule -that could allow the operation, including rules that providers contribute. - -### Request Flow - -For inspected HTTP traffic, OpenShell applies its checks in this order: - -```mermaid -flowchart TD - A["Sandbox tool"] --> B["Check destination
and process identity"] - B --> C["Check application request rules"] - C --> D["Run selected request middleware
Recheck transformed operations"] - D --> E["Resolve permitted credentials"] - E --> F["Upstream service"] - F --> G["Run selected response middleware"] - G --> A -``` - -Middleware runs only on traffic that network rules already allow. It can inspect -or transform requests before OpenShell injects provider credentials, inspect -responses before they reach the sandbox, and inspect complete client WebSocket -text messages. Built-in middleware is available to every policy. External -middleware must be registered with the gateway first. Refer to -[Supervisor Middleware](/extensibility/supervisor-middleware). - -### Network Access and Credentials - -A network rule that allows traffic does not authorize every provider credential -at that destination. OpenShell supplies a provider credential only to the -destinations that its provider profile or an explicit credential binding -allows. Provider-credentialed endpoints also require inspected traffic unless -the endpoint sets `allow_uninspected_credentials: true`. When a request fails a -credential check, correct the provider binding instead of widening the network -rule. Refer to [Provider Profiles](/providers/profiles). +| `filesystem_policy` | Paths that sandbox processes can read, or read and write. | Landlock LSM in the kernel. | At sandbox startup. | +| `landlock` | Whether an unsupported kernel or an inaccessible path stops the sandbox from starting. | Sandbox startup checks. | At sandbox startup. | +| `process` | User and group that sandbox processes run as. | Identity change before the workload starts. | At sandbox startup. | +| `network_policies` | Destinations each binary can reach, and the requests it can send. | Sandbox network proxy. | While the sandbox runs. | +| `network_middlewares` | Additional inspection or transformation of allowed network traffic. | Sandbox network proxy. | While the sandbox runs. | + +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](/sandboxes/network-rules) +explains how OpenShell evaluates rules and gives examples you can adapt. + +The startup sections are fixed once the sandbox starts. The network sections +can change while it runs. The [Policy Schema Reference](/reference/policy-schema) +describes the complete file format, and [Supervisor +Middleware](/extensibility/supervisor-middleware) explains how middleware +processes traffic. ## Where the Active Policy Comes From @@ -232,65 +76,56 @@ Policy](/sandboxes/manage-policies#inspect-the-current-policy) shows both views. A gateway administrator can apply one policy to every sandbox on the gateway. The global policy replaces each sandbox's policy. It is not a ceiling intersected with existing grants. While it is active, sandbox policy changes are -blocked and provider-contributed network rules are suppressed. Credential -authorization remains a separate check, so a global network grant does not make -a provider credential usable at a destination. - -Deleting the global policy restores normal policy selection and provider rules. -Refer to [Apply a Gateway-Wide +blocked and provider-contributed network rules are suppressed. Deleting the +global policy restores normal policy selection and provider rules. Refer to +[Apply a Gateway-Wide Policy](/sandboxes/manage-policies#apply-a-gateway-wide-policy) for the commands. ## How Changes Take Effect -Live sections and startup sections behave differently when a policy changes: +The network sections of a policy can change while a sandbox runs. The startup +sections cannot: | Change | Effect on a running sandbox | Required action | |---|---|---| -| Network rules | The sandbox receives the new rules. Connections that use the previous configuration close. | Apply the change, then retry the request. | +| 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 workdir, Landlock, or process settings | OpenShell rejects the change after the workload starts. | Recreate the sandbox. | -OpenShell calls each version of the active configuration a generation. When a -new generation becomes active, OpenShell closes HTTP keep-alive connections, -tunnels, upgrades, and long-lived streams that still use the previous one. A -parsed WebSocket relay closes with code `1012`. The client must reconnect so its -next request uses the new configuration. +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. -Every change passes validation before it becomes active: +OpenShell validates every change twice before it takes effect: ```mermaid flowchart TD - A["Review the proposed change"] --> B["Submit it to the gateway"] - B --> C{"Gateway accepts it?"} - C -->|No| D["Do not save the change
Keep the current configuration"] - C -->|Yes| E["Runtime checks the complete configuration"] - E --> F{"Can it become active?"} - F -->|Yes| G["Make it active
Close connections using the old version"] - F -->|No| H["Use the configured failure mode
or wait for startup repair"] - G --> I["Check status and retry the request"] - D --> A - H --> A + 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 gateway checks a proposed policy before saving it. The sandbox checks the -complete effective configuration again before activating it, because it also -sees provider changes and concurrent updates. If activation fails, the gateway's -`policy_validation_failure_mode` setting decides whether the sandbox blocks new -egress (`fail_closed`, the default) or keeps the last valid configuration -(`retain_last_valid`). [Troubleshoot Sandbox -Policies](/sandboxes/troubleshoot-policies) explains how to identify and repair -each kind of failure. +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 gateway's `policy_validation_failure_mode` setting 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. [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) +explains how to identify and repair each kind of failure. ## Next Steps +- Use [Network Rules](/sandboxes/network-rules) to learn how OpenShell + evaluates network rules and to adapt examples for common services. - Use [Manage Sandbox Policies](/sandboxes/manage-policies) to create, update, verify, and roll back policies. -- Use [Network Policy Recipes](/sandboxes/network-policy-recipes) for rules you - can adapt for REST APIs, package registries, WebSocket, GraphQL, MCP, - JSON-RPC, and native TCP. - Use [Policy Advisor](/sandboxes/policy-advisor) to let an agent propose narrow network changes for your review. - Use the [Policy Schema Reference](/reference/policy-schema) for every field, diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index f033531127..87f419ffd7 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -9,8 +9,7 @@ position: 6 --- This reference defines every field in the authored YAML and JSON policy format, -with defaults, matcher behavior, and validation constraints. For how policies -are evaluated and applied, refer to [Sandbox Policies](/sandboxes/policies). +with defaults, matcher behavior, and validation constraints. ## Top-Level Structure @@ -170,7 +169,8 @@ process: A map of named network rules. Each entry declares a set of endpoints and a set of binaries, and allows each listed binary to reach each listed endpoint. The map key is the rule's stable identifier and the selector for -`openshell policy update --rule-name`. +`openshell policy update --rule-name`. For how OpenShell evaluates these rules, +with examples, refer to [Network Rules](/sandboxes/network-rules). ### Network Policy Entry @@ -180,7 +180,7 @@ Each entry in the `network_policies` map has the following fields: |---|---|---|---| | `name` | string | No | Display name used in log output. Defaults to the map key. | | `endpoints` | list of endpoint objects | No | Destinations this entry permits. An omitted or empty list grants no destination. | -| `binaries` | list of binary objects | No | Executable selectors associated with the endpoints. In a sandbox, an omitted or empty list matches no program, so the entry allows nothing. Binaries are ignored only when trusted runtime configuration disables binary identity enforcement. | +| `binaries` | list of binary objects | No | Executable selectors associated with the endpoints. In a sandbox, an omitted or empty list matches no binary, so the entry allows nothing. Binaries are ignored only when trusted runtime configuration disables binary identity enforcement. | When present, `endpoints` and `binaries` must be lists of objects, even when they contain only one entry. `null` cannot replace a list or an object entry. @@ -727,7 +727,7 @@ symlink spelling does not bypass canonical path checks. Glob selectors are not symlink-resolved and must match the canonical path. A binary selector also matches when it names an ancestor of the calling process, -so programs launched by a listed binary can use the rule. Trusted runtime +so processes that a listed binary starts can use the rule. Trusted runtime configuration can disable binary identity enforcement, so inspect the active runtime mode when diagnosing an unexpected result. diff --git a/docs/reference/policy-updates.mdx b/docs/reference/policy-updates.mdx index 4df0d8f733..638ce7328a 100644 --- a/docs/reference/policy-updates.mdx +++ b/docs/reference/policy-updates.mdx @@ -9,8 +9,7 @@ position: 7 --- This reference describes the `openshell policy` commands and the related -sandbox commands that read or set a policy. For step-by-step workflows, refer to -[Manage Sandbox Policies](/sandboxes/manage-policies). +sandbox commands that read or set a policy. Every sandbox-scoped command accepts an optional sandbox name. When you omit it, the CLI uses the last sandbox you used on the current gateway and workspace, and @@ -234,8 +233,7 @@ request path appended by `--add-allow` or `--add-deny`. ### Complete Scope Requirements A network rule allows each listed binary to reach each listed endpoint, as -described in [Rules Pair Programs with -Destinations](/sandboxes/policies#rules-pair-programs-with-destinations). An +described in [Rule Structure](/sandboxes/network-rules#rule-structure). An append that silently widened that scope could authorize new binary and endpoint pairs, so the CLI requires complete affected scope for request-rule appends and for endpoint merges that could create new pairs. diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx index 0a62d2f44f..20e2cb0d8a 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/sandboxes/manage-policies.mdx @@ -5,7 +5,7 @@ title: "Manage Sandbox Policies" sidebar-title: "Manage Policies" description: "Create sandboxes with a policy, inspect and change sandbox policies, verify changes, roll back, and apply a gateway-wide policy." keywords: "Generative AI, Cybersecurity, Policy, Sandbox, CLI, Hot Reload, Revision" -position: 2 +position: 3 --- This page covers the tasks for working with sandbox policies: setting a policy @@ -36,7 +36,7 @@ openshell sandbox create --name my-sandbox Without either, the sandbox uses a policy from its image or the restrictive [default policy](/reference/default-policy). Write the file from the -[Network Policy Recipes](/sandboxes/network-policy-recipes) and the +[Network Rules](/sandboxes/network-rules) examples and the [Policy Schema Reference](/reference/policy-schema). ## Ship a Policy in an Image @@ -104,8 +104,8 @@ To view an earlier revision, add `--rev` with its version number, for example complete policy file. It edits only `network_policies` and preserves every other section. -To give a program access to a service, add an endpoint that names the -executable, the destination, and the permitted request types. For a sandbox +To give a binary in the sandbox access to a service, add an endpoint that names +the executable, the destination, and the permitted request types. For a sandbox containing `/usr/bin/curl`, this command adds read-only access to the GitHub API: @@ -304,8 +304,8 @@ affected sandboxes before and after the change. ## Next Steps -- Use [Network Policy Recipes](/sandboxes/network-policy-recipes) for rules you - can adapt to common services and protocols. +- Use [Network Rules](/sandboxes/network-rules) for example rules you can + adapt to common services and protocols. - Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) when a change fails to load or a request is denied unexpectedly. - Use the [Policy Command Reference](/reference/policy-updates) for every diff --git a/docs/sandboxes/network-policy-recipes.mdx b/docs/sandboxes/network-rules.mdx similarity index 74% rename from docs/sandboxes/network-policy-recipes.mdx rename to docs/sandboxes/network-rules.mdx index a8b6a240eb..7ca70faee6 100644 --- a/docs/sandboxes/network-policy-recipes.mdx +++ b/docs/sandboxes/network-rules.mdx @@ -1,40 +1,164 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Network Policy Recipes" -sidebar-title: "Network Recipes" -description: "Adapt network rules for REST APIs, package registries, internal services, WebSocket, GraphQL, MCP, JSON-RPC, native TCP, raw TLS, and provider credentials." +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, JSON-RPC, native TCP, raw TLS, and provider credentials." keywords: "Generative AI, Cybersecurity, Policy, Network, REST, TCP, WebSocket, GraphQL, MCP, JSON-RPC, PyPI, npm" -position: 3 +position: 2 --- -Each recipe on this page is a network rule for a common service or protocol, -with commands to verify it. For how OpenShell evaluates these rules, refer to -[How Network Rules Work](/sandboxes/policies#how-network-rules-work). +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. -## Use a Recipe +## How Network Rules Work -Each recipe states when to use it, gives the rule, shows how to verify it, and -lists its limits. Replace the example hosts, paths, and binaries with your own -values, and confirm that the sandbox contains the client executable. The -commands use `my-sandbox` as the sandbox name. +OpenShell checks every outbound connection from a sandbox against the +`network_policies` section of its policy, and denies any connection that no rule +allows. -When `openshell policy update` can express a rule, the recipe shows the command. -Otherwise, add the YAML entry under the `network_policies` section of a complete -policy file and apply it with `openshell policy set`, as described in [Replace -the Complete Policy](/sandboxes/manage-policies#replace-the-complete-policy). -Keep unrelated rules, and replace an existing rule with the same key only when -that is your intent. After you apply a rule, [verify the +### Rule Structure + +Each entry in `network_policies` is a named rule with 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 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/python3 +``` + +The rule allows these four combinations: + +| Binary | Endpoint | +|---|---| +| `/usr/bin/curl` | `api.example.com:443` | +| `/usr/bin/curl` | `uploads.example.com:443` | +| `/usr/bin/python3` | `api.example.com:443` | +| `/usr/bin/python3` | `uploads.example.com:443` | + +If `python3` should reach only `api.example.com`, put that binary and endpoint in +a separate rule. + +### Choose Which Binaries to List + +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. A +rule for `pip install` must list `/usr/bin/python3`, not `/usr/bin/pip`. When +OpenShell denies a connection, the sandbox log shows the binary it identified. +[Package Registries](#package-registries) shows rules for `pip`, `uv`, and +`npm`. + +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 resolves symlinks to the real executable path, so a symlink cannot +select a different rule. It also records a checksum of each executable the +first time a rule allows it, 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 `protocol`, 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. | [HTTP APIs](#http-apis) | +| `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](#allow-json-rpc-methods) | +| `tcp` | Nothing. The binary gets a native TCP connection, which suits clients such as databases. | [Native TCP](#allow-native-tcp) | +| Omitted | No request rules apply. OpenShell still terminates TLS and checks that each HTTP request is addressed to the allowed host, but it allows any method or path. | -- | + +Use an inspected endpoint when the request, not only the destination, +determines the risk, such as allowing reads but not writes on an API. + +### Enforce and Audit + +The `enforcement` field on an inspected endpoint decides what happens when a +request breaks the endpoint's request rules: + +- `enforce` blocks the request with an OpenShell `policy_denied` response. +- `audit` allows the request and logs the violation. This is the default. + +Use `audit` to see what a new rule would block, then switch the endpoint to +`enforce`. An endpoint in audit mode blocks no requests. [Observe a Rule Before +Enforcing It](#observe-a-rule-before-enforcing-it) shows this workflow. + +### 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. + +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. +[Block Specific Requests](#block-specific-requests) 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. [Provider +Credentials](#provider-credentials) shows an explicit binding, and [Provider +Profiles](/providers/profiles) explains profile endpoints. + +## Use the Examples + +Each example states when to use the rule, gives the rule, shows how to verify +it, and lists its limits. Replace the example hosts, paths, and binaries with +your own values, and confirm that the sandbox contains the client executable. +The commands use `my-sandbox` as the sandbox name. + +When `openshell policy update` can express a rule, the example shows the +command. Otherwise, add the YAML entry under the `network_policies` section of a +complete policy file and apply it with `openshell policy set`, as described in +[Replace the Complete +Policy](/sandboxes/manage-policies#replace-the-complete-policy). Keep unrelated +rules, and replace an existing rule with the same key only when that is your +intent. After you apply a rule, [verify the change](/sandboxes/manage-policies#verify-a-change) before testing traffic. ## HTTP APIs -These recipes use `protocol: rest`, which lets OpenShell check each HTTP +These examples use `protocol: rest`, which lets OpenShell check each HTTP request's method, path, and query values. ### Allow Read-Only Access -Use this rule when a program needs to read from an HTTP API but must not change +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`. @@ -252,8 +376,8 @@ you expect, change the endpoint to `enforcement: enforce`. Package managers download packages with `GET` requests, so a read-only REST rule allows installs while blocking uploads. List the executable that opens the connection. `pip` and `npm` are scripts, so their rules list the Python or Node -interpreter. Because a rule also matches programs that a listed binary launches, -listing an interpreter lets any program running under it reach the registry. +interpreter. Because a rule also applies to processes that a listed binary +starts, listing an interpreter lets any script it runs reach the registry. ### Allow PyPI Downloads @@ -381,7 +505,7 @@ instead of broadening the range when an address changes. ## Other Application Protocols -These recipes inspect protocols carried over HTTP. Each one requires explicit +These examples inspect protocols carried over HTTP. Each one requires explicit rules for the protocol's operations. ### Allow WebSocket Messages @@ -422,7 +546,9 @@ frames or upstream-to-client messages. Provider-credentialed endpoints remain on the parsed relay and reject binary frames unless `allow_uninspected_credentials: true` explicitly accepts the weaker boundary. Set `websocket_credential_rewrite: true` only when client text messages contain -OpenShell credential placeholders that must be resolved. +OpenShell credential placeholders that must be resolved. When new network rules +take effect, OpenShell closes the upgraded connection with close code `1012`, so +the client must reconnect. ### Allow GraphQL Operations @@ -588,7 +714,7 @@ Server-to-client messages are not parsed for policy enforcement. ## Uninspected Connections -These recipes allow a connection without inspecting its application payload. +These examples allow a connection without inspecting its application payload. OpenShell still checks the destination, port, and executable. Prefer an inspected protocol whenever the client supports it. @@ -689,11 +815,10 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ https://secure.example.com/ ``` -## Credentials on Endpoints +## Provider Credentials -A network rule that allows traffic does not authorize every provider credential -at that destination. These recipes control where OpenShell supplies provider -credentials. +These examples control where OpenShell supplies provider credentials, as +described in [Network Access and Credentials](#network-access-and-credentials). ### Bind a Provider Credential to an Endpoint @@ -730,6 +855,8 @@ signing, refer to [AWS SigV4](/providers/aws-sigv4). ## Next Steps +- Use [Manage Sandbox Policies](/sandboxes/manage-policies) to apply, verify, + and roll back policy changes. - Use the [Policy Schema Reference](/reference/policy-schema) for protocol defaults, field constraints, and matcher semantics. - Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) to diff --git a/docs/sandboxes/troubleshoot-policies.mdx b/docs/sandboxes/troubleshoot-policies.mdx index 3052d65131..01b5f2539c 100644 --- a/docs/sandboxes/troubleshoot-policies.mdx +++ b/docs/sandboxes/troubleshoot-policies.mdx @@ -65,7 +65,7 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ OpenShell identifies the process by the executable the kernel reports, so a script such as `pip` or `npm` appears as its interpreter, such as `/usr/bin/python3.12` or `/usr/bin/node`. Binary matching can also admit a -trusted ancestor. An empty binary list matches no program unless trusted runtime +trusted ancestor. An empty binary list matches no binary unless trusted runtime configuration disables identity enforcement. Compare the logged binary with the policy's selectors before widening a rule. diff --git a/docs/tutorials/first-network-policy.mdx b/docs/tutorials/first-network-policy.mdx index fc3ce27930..fba6136980 100644 --- a/docs/tutorials/first-network-policy.mdx +++ b/docs/tutorials/first-network-policy.mdx @@ -129,7 +129,7 @@ This tutorial uses `curl` and `read-only` access to keep things simple. When bui - 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](/reference/policy-schema). -- To allow other services, such as PyPI, npm, or your internal APIs, adapt the [Network Policy Recipes](/sandboxes/network-policy-recipes). +- To allow other services, such as PyPI, npm, or your internal APIs, adapt the examples in [Network Rules](/sandboxes/network-rules).
From 6acc0d763c3fb77e070aba1a064bdd3295844428 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 18:50:20 +0000 Subject: [PATCH 10/40] docs(policy): focus policy management on CLI workflows and remove command reference Signed-off-by: Johnny Greco --- docs/how-it-works/policies/default-policy.mdx | 2 +- docs/how-it-works/policies/prover.mdx | 2 +- docs/reference/policy-updates.mdx | 373 ------------------ docs/sandboxes/manage-policies.mdx | 238 +++++------ docs/sandboxes/network-rules.mdx | 4 +- 5 files changed, 104 insertions(+), 515 deletions(-) delete mode 100644 docs/reference/policy-updates.mdx diff --git a/docs/how-it-works/policies/default-policy.mdx b/docs/how-it-works/policies/default-policy.mdx index b7a5bee359..45e93cd036 100644 --- a/docs/how-it-works/policies/default-policy.mdx +++ b/docs/how-it-works/policies/default-policy.mdx @@ -5,7 +5,7 @@ title: "Default Policy and Baseline Paths" sidebar-title: "Default Policy" 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: 8 +position: 7 --- This reference describes the restrictive policy OpenShell uses when a sandbox has diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index c658a86206..bde17cc90e 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -5,7 +5,7 @@ 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" -position: 9 +position: 8 --- `openshell-prover` checks whether the authority in a local candidate policy is diff --git a/docs/reference/policy-updates.mdx b/docs/reference/policy-updates.mdx deleted file mode 100644 index 638ce7328a..0000000000 --- a/docs/reference/policy-updates.mdx +++ /dev/null @@ -1,373 +0,0 @@ ---- -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -title: "Policy Command Reference" -sidebar-title: "Commands" -description: "Flags, output, update syntax, merge behavior, and wait results for the openshell policy commands." -keywords: "Generative AI, Cybersecurity, Policy, CLI, Incremental Update, Hot Reload, Revision" -position: 7 ---- - -This reference describes the `openshell policy` commands and the related -sandbox commands that read or set a policy. - -Every sandbox-scoped command accepts an optional sandbox name. When you omit it, -the CLI uses the last sandbox you used on the current gateway and workspace, and -prints the name it selected. The global `--gateway` and `--workspace` flags -select where the command runs. - -## Inspect a Policy - -`openshell policy get [NAME]` shows the current policy for a sandbox, a stored -revision, or the gateway-global policy. - -| Flag | Default | Purpose | -|---|---|---| -| `--base` | Off | Include the base policy, without provider-contributed rules. | -| `--full` | Off | Include the effective policy, including provider-contributed rules. Cannot be combined with `--base`. | -| `--rev ` | `0` | Show a stored revision. `0` shows the current effective policy, or the latest revision with `--global`. | -| `--global` | Off | Show the gateway-global policy. The sandbox name is ignored. | -| `-o`, `--output ` | `table` | `table` or `json`. | - -Without `--base` or `--full`, the command prints only metadata such as version, -hash, status, and policy source. With either flag, the table output prints the -metadata, a `---` separator, and the policy as YAML. The JSON output adds a -`policy` object with the same field names as the YAML, so -`jq '.policy'` extracts a complete policy file. - -A `Source` value of `global` means a gateway-global policy replaces the sandbox -policy. Reading the global policy requires the platform administrator role. - -## List Revisions - -`openshell policy list [NAME]` lists policy revisions for a sandbox or for the -gateway-global policy. - -| Flag | Default | Purpose | -|---|---|---| -| `--page-size ` | `20` | Maximum revisions per page. `0` selects 100, and the maximum is 1000. | -| `--page-token ` | Empty | Continuation token from a previous page. | -| `--global` | Off | List gateway-global policy revisions. The sandbox name is ignored. | -| `-o`, `--output ` | `table` | `table`, `yaml`, or `json`. | - -The table shows each revision's version, hash, status, creation time in epoch -milliseconds, and load error. Revision status is one of the following: - -| Status | Meaning | -|---|---| -| `Pending` | The gateway saved the revision, and the sandbox has not loaded it yet. | -| `Loaded` | The sandbox loaded the revision. Global revisions are marked loaded when set. | -| `Failed` | The sandbox rejected the revision, or the stored payload no longer passes the current schema. | -| `Superseded` | A newer revision was saved before this one loaded. | - -## Replace a Policy - -`openshell policy set [NAME] --policy ` replaces the complete policy for -a running sandbox or sets the gateway-global policy. - -| Flag | Default | Purpose | -|---|---|---| -| `--policy ` | Required | Path to the complete policy YAML file. | -| `--wait` | Off | Wait for the sandbox to load the revision. Refer to [Wait Results](#wait-results). Not supported with `--global`. | -| `--timeout ` | `60` | Timeout for `--wait`. | -| `--global` | Off | Apply the file as the gateway-global policy for every sandbox. | -| `--yes` | Off | Skip the confirmation prompt for `--global`. Required in non-interactive sessions. | - -The file replaces every section of the policy, so prepare it from the current -base as described in [Replace the Complete -Policy](/sandboxes/manage-policies#replace-the-complete-policy). The gateway -rejects a sandbox-scoped `set` while a gateway-global policy is active. Setting -the global policy requires the platform administrator role and takes effect -immediately. - -## Update Network Rules - -`openshell policy update [NAME]` merges explicit operations into a sandbox's -current `network_policies` map. It does not edit filesystem, Landlock, process, -or middleware sections, and it does not support `--global`. - -One command can contain several compatible operations. The gateway applies the -batch atomically and persists at most one new revision. - -| Flag | Purpose | -|---|---| -| `--add-endpoint ` | Add or merge one endpoint and its declared binary scope. Repeat for multiple endpoints. | -| `--remove-endpoint ` | Remove the host and port match from every authored network rule. A multi-port endpoint keeps its other ports. | -| `--remove-rule ` | Remove a complete named `network_policies` entry. | -| `--add-allow ` | Append a REST or WebSocket method and path allow rule. | -| `--add-deny ` | Append a REST or WebSocket method and path deny rule. | -| `--binary ` | Add binaries with endpoints, or declare the complete stored binary scope for a request-rule append. Repeat as needed. | -| `--rule-name ` | Name one new endpoint rule or select the existing rule for a request-rule append. Required for `--add-allow` and `--add-deny`. | -| `--any-binary` | Declare that the existing request-rule target stores an empty binary scope. Cannot be combined with `--binary`. | -| `--endpoint-path ` | Select one existing endpoint by its exact stored path. Pass `''` to select no path. | -| `--dry-run` | Fetch the current policy and preview the local merge without saving a revision. | -| `--wait` | Poll for the submitted revision's result. Cannot be combined with `--dry-run`. | -| `--timeout ` | Set the `--wait` timeout. Defaults to 60 seconds. | - -`--any-binary` describes the target rule's stored merge scope. It is not an -independent claim that every runtime identity mode admits every process. Prefer -explicit binary paths in authored rules. - -### Endpoint Specification - -`--add-endpoint` uses this grammar: - -```text -host:port[:access[:protocol[:enforcement[:options]]]] -``` - -| Segment | Accepted values and behavior | -|---|---| -| `host` | Required destination hostname. | -| `port` | Required integer from 1 through 65535. | -| `access` | `read-only`, `read-write`, or `full` for inspected endpoints. | -| `protocol` | `tcp`, `rest`, or `websocket`. Use `policy set` with full policy YAML for GraphQL, MCP, and JSON-RPC. | -| `enforcement` | `enforce` or `audit`. Requires a protocol. Omission selects audit for inspected requests. | -| `options` | Comma-separated options listed below. | - -| Option | Effect | -|---|---| -| `allowed-ip=` | Add a destination IP allowance. Repeat the option in the comma-separated list for multiple values. | -| `request-body-credential-rewrite` | Rewrite supported credential placeholders in inspected REST text bodies. Requires `rest`. | -| `websocket-credential-rewrite` | Rewrite supported placeholders in client WebSocket text messages. Requires `rest` or `websocket`. | -| `allow-uninspected-credentials` | Accept the security-sensitive exposure of provider credentials on an uninspected traffic path. | - -The grammar has no `tls` option. Use `policy set` for `tls: skip`. When you omit -`--rule-name`, the CLI names a new rule `allow__`, with dots and -hyphens in the host replaced by underscores. - -Read-only HTTP access for curl: - -```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 -``` - -Native PostgreSQL access for psql: - -```shell -openshell policy update my-sandbox \ - --rule-name postgres \ - --binary /usr/bin/psql \ - --add-endpoint db.internal.example:5432::tcp \ - --wait -``` - -Read-only access to an internal HTTP API with an explicit destination IP -allowance: - -```shell -openshell policy update my-sandbox \ - --rule-name private_api \ - --binary /usr/bin/curl \ - --add-endpoint 'api.internal.example:443:read-only:rest:enforce:allowed-ip=10.20.0.0/16' \ - --wait -``` - -The empty access segment before `tcp` is required by the positional grammar. -`protocol: tcp` rejects access, enforcement, and application-inspection options. - -An inspected REST or WebSocket endpoint needs an allow shape. For incremental -creation, supply an access preset. Create the endpoint in one command, then add -explicit allow or deny rules in a separate command. - -### Request Rule Specification - -`--add-allow` and `--add-deny` use this grammar: - -```text -host:port[,port...]:METHOD:path_glob -``` - -The host, complete port set, rule name, complete binary set, and optional -endpoint path identify one existing REST or WebSocket endpoint. These flags do -not create an endpoint or change its scope. The CLI uppercases the method, and -the path must start with `/` or `**`. - -Quote specifications that contain `*`, `**`, `?`, or bracket classes. Request -path globs are not shell path globs. Both `*` and `**` can cross `/` boundaries, -`?` matches one character, and bracket classes are supported. - -The following examples target an existing `github_api` rule whose only binary -is `/usr/bin/gh` and whose REST endpoint is `api.github.com:443`. An allow -append permits `POST` requests to issue-creation paths: - -```shell -openshell policy update my-sandbox \ - --rule-name github_api \ - --binary /usr/bin/gh \ - --add-allow 'api.github.com:443:POST:/repos/*/issues' \ - --wait -``` - -A deny append blocks `POST` requests to administration paths on that endpoint: - -```shell -openshell policy update my-sandbox \ - --rule-name github_api \ - --binary /usr/bin/gh \ - --add-deny 'api.github.com:443:POST:/admin/**' \ - --wait -``` - -For an existing `realtime` rule with only `/usr/bin/node` and a WebSocket -endpoint at `realtime.example.com:443`, a text-message deny uses the -`WEBSOCKET_TEXT` method. The path matches the stored upgrade request path, not -message content: - -```shell -openshell policy update my-sandbox \ - --rule-name realtime \ - --binary /usr/bin/node \ - --add-deny 'realtime.example.com:443:WEBSOCKET_TEXT:/v1/admin/**' \ - --wait -``` - -Use `--endpoint-path` when a rule contains multiple endpoints with the same host -and ports. This selector identifies the endpoint. It is separate from the -request path appended by `--add-allow` or `--add-deny`. - -### Complete Scope Requirements - -A network rule allows each listed binary to reach each listed endpoint, as -described in [Rule Structure](/sandboxes/network-rules#rule-structure). An -append that silently widened that scope could authorize new binary and endpoint -pairs, so the CLI requires complete affected scope for request-rule appends and -for endpoint merges that could create new pairs. - -For example, an endpoint that stores ports 443 and 8443 must be targeted with -`443,8443`, even if the new method was observed only on 443. Likewise, repeat -every stored binary path, or use `--any-binary` only when the stored rule has an -empty binary list. - -Copy the rule name, binary paths, endpoint path, and complete port set from -`openshell policy get my-sandbox --base`. Do not derive merge scope from `--full` -when provider-owned rules are present. Those rules are not part of the sandbox -base you can incrementally edit. - -The gateway rejects incomplete or ambiguous declarations before it saves a -revision. Read the reported expected scope and correct the command. Do not fill -scope mechanically without confirming that the broader effect matches your -intent. - -### Remove Permissions - -Preview endpoint removal before applying it, because `--remove-endpoint` is not -scoped by `--rule-name`. It removes the matching host and port from every -authored network rule: - -```shell -openshell policy update my-sandbox \ - --remove-endpoint api.example.com:443 \ - --dry-run - -openshell policy update my-sandbox \ - --remove-endpoint api.example.com:443 \ - --wait -``` - -To remove one named rule instead: - -```shell -openshell policy update my-sandbox \ - --remove-rule github_readonly \ - --wait -``` - -Removing an endpoint from a multi-port endpoint removes only the named port. -When removal leaves an endpoint with no ports, OpenShell removes that endpoint. -When a rule loses its final endpoint, OpenShell removes the rule instead of -retaining an empty entry. Use `--remove-rule` when you intend to remove one -specific map entry, and inspect the resulting base policy after either command. - -### Preview a Merge - -`--dry-run` shows the proposed policy without saving it. For example, this -command previews a request-rule change to an existing `github_api` rule with -only `/usr/bin/gh` and a REST endpoint at `api.github.com:443`: - -```shell -openshell policy update my-sandbox \ - --rule-name github_api \ - --binary /usr/bin/gh \ - --add-allow 'api.github.com:443:GET:/repos/**' \ - --dry-run -``` - -The command validates argument shapes, connects to the gateway, fetches the -current sandbox configuration, and applies the merge locally. It creates no -revision, so an unavailable gateway causes a connection failure. The preview -does not establish that a later submission will pass every effective policy, -provider, credential, or runtime validation. - -### Merge and Concurrency Behavior - -All compatible flags in one command form one atomic batch. They succeed or fail -together and persist at most one revision. `--add-endpoint` cannot share a batch -with `--add-allow` or `--add-deny` because their scope flags have different -meanings. - -Concurrent writers use optimistic retry. The gateway reapplies the complete -operation batch to the latest revision and validates the result again. A no-op -merge reports an unchanged version and creates no revision. - -Rule names identify stored map entries for update and removal. The optional -human-readable `name` inside a YAML rule is not the `--rule-name` selector when -the two differ. Use the map key shown by `policy get --base`. - -Incremental updates are unavailable while a gateway-global policy is active. -Delete the global policy before changing a sandbox policy. - -## Delete the Global Policy - -`openshell policy delete --global` removes the gateway-global policy and -restores normal sandbox policy selection. Sandbox policies cannot be deleted, -so the command requires `--global`. - -| Flag | Purpose | -|---|---| -| `--global` | Required. Delete the gateway-global policy. | -| `--yes` | Skip the confirmation prompt. Required in non-interactive sessions. | - -Deleting the global policy marks its revisions `Superseded`. The gateway rejects -the deletion when the restored sandbox and provider configuration would be -invalid. - -## Related Sandbox Commands - -These sandbox commands also read or set a policy: - -| Command | Purpose | -|---|---| -| `openshell sandbox create --policy ` | Set the initial policy for a new sandbox. Overrides `OPENSHELL_SANDBOX_POLICY`. | -| `OPENSHELL_SANDBOX_POLICY=` | Environment variable that `sandbox create` reads when `--policy` is omitted. Other commands ignore it. | -| `openshell sandbox get [NAME] --policy-only` | Print only the effective policy as plain YAML, with no metadata. Cannot be combined with `--output`. | - -## Wait Results - -`policy set` and `policy update` accept `--wait`. Without it, success means -only that the gateway accepted the submission, not that the sandbox activated -it. With it, the CLI polls once per second until it observes a result: - -| Result | Exit code | -|---|---| -| The sandbox loaded the revision. | `0` | -| The policy was unchanged, so no revision was created. The CLI returns immediately. | `0` | -| A newer revision superseded this one before it loaded. | `0` | -| The sandbox rejected the revision. | `1` | -| The wait timed out. | `124` | - -Because a zero exit can mean an unchanged or superseded revision, inspect -current state before relying on the change: - -```shell -openshell policy list my-sandbox -openshell policy get my-sandbox --full -``` - -A timeout means the CLI stopped polling. It does not establish whether the -revision later loaded or failed. Inspect revision status and sandbox readiness -before resubmitting, because an automatic retry can race with a late result. diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx index 20e2cb0d8a..b0bac39e25 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/sandboxes/manage-policies.mdx @@ -3,31 +3,29 @@ # SPDX-License-Identifier: Apache-2.0 title: "Manage Sandbox Policies" sidebar-title: "Manage Policies" -description: "Create sandboxes with a policy, inspect and change sandbox policies, verify changes, roll back, and apply a gateway-wide policy." +description: "Use the OpenShell CLI to set, inspect, change, verify, and roll back sandbox policies, and to apply a gateway-wide policy." keywords: "Generative AI, Cybersecurity, Policy, Sandbox, CLI, Hot Reload, Revision" position: 3 --- -This page covers the tasks for working with sandbox policies: setting a policy -at creation, inspecting it, changing it on a running sandbox, verifying the -result, and rolling back. For how policies work, refer to -[Sandbox Policies](/sandboxes/policies). +This page shows how to use the OpenShell CLI to manage a sandbox's policy. Each +section covers one task, 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. -The command examples use `my-sandbox` as the sandbox name. When you omit the -name, the CLI uses the last sandbox you used. +The examples use `my-sandbox` as the sandbox name. ## Create a Sandbox with a Policy -Pass a policy file when you create a sandbox to set its initial policy, -including the filesystem, Landlock, and process settings that cannot change -after the workload starts: +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 ``` To use the same file for every sandbox you create, set -`OPENSHELL_SANDBOX_POLICY`. The CLI reads it whenever you omit `--policy`: +`OPENSHELL_SANDBOX_POLICY` instead of passing `--policy`: ```shell export OPENSHELL_SANDBOX_POLICY=./policy.yaml @@ -35,78 +33,63 @@ openshell sandbox create --name my-sandbox ``` Without either, the sandbox uses a policy from its image or the restrictive -[default policy](/reference/default-policy). Write the file from the -[Network Rules](/sandboxes/network-rules) examples and the -[Policy Schema Reference](/reference/policy-schema). +[default policy](/reference/default-policy). ## Ship a Policy in an Image -A sandbox image can include its own policy at `/etc/openshell/policy.yaml`. -When the gateway has no saved policy for a sandbox, the supervisor reads that -file and saves it as the sandbox's policy. The supervisor also checks the legacy -path `/etc/navigator/policy.yaml`. For example, add the file in a Dockerfile: +To include a policy in a sandbox image, add it at `/etc/openshell/policy.yaml`: ```dockerfile COPY policy.yaml /etc/openshell/policy.yaml ``` -A `--policy` file or `OPENSHELL_SANDBOX_POLICY` takes precedence over the image -policy. If the image policy is invalid, the workload does not start until you -repair the configuration. OpenShell does not fall back to the default policy. -Refer to [Repair Initial -Configuration](/sandboxes/troubleshoot-policies#repair-initial-configuration). +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](/sandboxes/troubleshoot-policies#repair-initial-configuration). OpenShell +does not fall back to the default policy. ## Inspect the Current Policy -Print the sandbox's base policy, without provider-contributed rules, or its -effective policy, which is what the sandbox enforces: +A sandbox's policy has two views. The base policy is the policy you manage for +the sandbox. The effective policy is the one the sandbox enforces. It combines +the base policy with rules from attached providers, or it is the gateway-global +policy when one is active. ```shell openshell policy get my-sandbox --base openshell policy get my-sandbox --full ``` -Each view prints status metadata, a `---` separator, and the policy YAML. -Without `--base` or `--full`, `policy get` prints only the metadata. A `Source` -value of `global` means a gateway-global policy is active, which blocks sandbox -policy changes. +Choose the view that fits your task: -Start edits from the base view so you do not copy provider-owned rules into -your configuration. Use the effective view to review everything that could allow -an operation, because several rules can grant permission together. +- To edit the policy, start from the base view. The effective view also contains + rules that attached providers manage. Those rules are not part of the + sandbox's own policy, so leave them out of the file you edit. +- To find out why a request is allowed or denied, use the effective view. + Several rules, including provider rules, can allow the same request, and only + the effective view shows all of them. -To export the effective policy as plain YAML, for example for review or a -[policy prover check](/reference/policy-prover): +To save the effective policy as a YAML file, for example to review it or to +check it with the [policy prover](/reference/policy-prover): ```shell openshell sandbox get my-sandbox --policy-only > effective-policy.yaml ``` -To list the policy revision history with each revision's load status: +To see each revision of the policy and whether the sandbox loaded it: ```shell openshell policy list my-sandbox ``` -| Status | Meaning | -|---|---| -| `Pending` | The gateway saved the revision, and the sandbox has not loaded it yet. | -| `Loaded` | The sandbox loaded the revision. | -| `Failed` | The sandbox rejected the revision. Refer to [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies). | -| `Superseded` | A newer revision was saved before this one loaded. | - -To view an earlier revision, add `--rev` with its version number, for example -`openshell policy get my-sandbox --rev 2 --base`. - ## Add or Remove Network Access -`openshell policy update` changes network rules on a running sandbox without a -complete policy file. It edits only `network_policies` and preserves every -other section. +`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. -To give a binary in the sandbox access to a service, add an endpoint that names -the executable, the destination, and the permitted request types. For a sandbox -containing `/usr/bin/curl`, this command adds read-only access to the GitHub +For example, to let `curl` in the sandbox make read-only requests to the GitHub API: ```shell @@ -117,12 +100,14 @@ openshell policy update my-sandbox \ --wait ``` -Here, `rest` enables HTTP request inspection, `read-only` permits `GET`, `HEAD`, -and `OPTIONS`, and `enforce` blocks other requests. The binary path must match -the executable inside the sandbox. +The endpoint sets the host and port, then the request rules. `read-only` allows +`GET`, `HEAD`, and `OPTIONS` requests, `rest` turns on HTTP request inspection, +and `enforce` blocks every other request. The `--binary` path must match the +executable inside the sandbox. -To allow an additional request on an existing rule, name the rule and repeat its -complete binary list from the base policy: +To allow another kind of request on an existing rule, add an allow rule. Name +the rule and repeat its complete binary list from the base policy, so the +command changes only that rule: ```shell openshell policy update my-sandbox \ @@ -132,31 +117,25 @@ openshell policy update my-sandbox \ --wait ``` -The CLI requires the complete scope of the target rule, so the command -identifies exactly which permissions change. Use `--add-deny` with the same -syntax to block a request. - -To remove access, remove the named rule: +`--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 ``` -`--remove-endpoint host:port` removes a destination from every rule that lists -it, so preview it first. - -To preview any update without saving it, use `--dry-run` instead of `--wait`. -The preview fetches the current policy and shows the merged result, but does -not run every check needed to activate it. The [Policy Command -Reference](/reference/policy-updates) covers the complete update syntax, scope -rules, and removal behavior. +To preview a change without applying it, replace `--wait` with `--dry-run`. A +preview does not guarantee that the sandbox will accept the change. Preview +`--remove-endpoint` in particular, because it removes the destination from every +rule that lists it. ## Replace the Complete Policy -Use `openshell policy set` for changes that `policy update` cannot make, such as -middleware, GraphQL, MCP, or JSON-RPC rules, `tls: skip`, credential fields, or -reorganizing rules. A full replacement supplies every section of the policy, -including settings you intend to keep, so start from the current base 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`, or credential settings. The new file replaces every section, so +start from the current base policy. @@ -166,13 +145,12 @@ Print the base policy: openshell policy get my-sandbox --base ``` -Copy only the YAML below the `---` separator into `policy.yaml`. Do not -redirect the entire output into the file, because the metadata above the -separator is not part of the policy. Keep the filesystem, Landlock, process, -and unrelated network settings, including baseline paths that OpenShell added at -startup, then edit the sections you intend to change. +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 its result: +Submit the edited file and wait for the result: ```shell openshell policy set my-sandbox --policy policy.yaml --wait @@ -180,8 +158,7 @@ openshell policy set my-sandbox --policy policy.yaml --wait -Extract the base policy as JSON without display metadata or provider-owned -rules: +Save the base policy as JSON: ```shell set -o pipefail @@ -189,7 +166,7 @@ openshell policy get my-sandbox --base --output json \ | jq -e '.policy' > base-policy.json ``` -Edit `base-policy.json`, then submit the complete policy: +Edit `base-policy.json`, then submit it and wait for the result: ```shell openshell policy set my-sandbox --policy base-policy.json --wait @@ -198,20 +175,20 @@ openshell policy set my-sandbox --policy base-policy.json --wait -The new policy must pass validation before it becomes active. Changes to -startup settings are subject to the limits in [How Changes Take -Effect](/sandboxes/policies#how-changes-take-effect). +OpenShell validates the new policy before it takes effect. Changes to the +filesystem, Landlock, and process sections have extra limits, described in the +next section. ## Change Filesystem and Process Settings Filesystem, Landlock, and process settings take effect only when the sandbox -starts. After the workload starts, OpenShell rejects removed filesystem paths -and changed workdir, Landlock, or process settings. It can save added -filesystem paths, but the running workload keeps its existing permissions. +starts. After that, OpenShell rejects removed filesystem paths and changes to +workdir, Landlock, or process settings. It accepts added filesystem paths, but +the running sandbox keeps its existing permissions. -To change these settings, export the base policy as described in -[Replace the Complete Policy](#replace-the-complete-policy), edit it, and create -a new sandbox with the file: +To change these settings, save the base policy as described in [Replace the +Complete Policy](#replace-the-complete-policy), edit it, and create a new +sandbox with the file: ```shell openshell sandbox delete my-sandbox @@ -220,71 +197,62 @@ openshell sandbox create --name my-sandbox --policy ./policy.yaml Deleting a sandbox stops its processes and removes its state, so copy out anything you need first. When the policy allows network access, OpenShell adds -baseline system paths at startup, so list only the paths your workload -requires. The [Default Policy](/reference/default-policy) page lists the -baseline paths. +the system paths that sandbox processes need, so list only the paths your +workload requires. [Default Policy](/reference/default-policy) lists those +paths. ## Verify a Change -A successful submission without `--wait` means only that the gateway accepted -the change. With `--wait`, the CLI polls until the revision reaches a result: +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. -| Result | Exit code | -|---|---| -| The sandbox loaded the revision. | `0` | -| The policy was unchanged, so no revision was created. | `0` | -| A newer revision superseded this one before it loaded. | `0` | -| The sandbox rejected the revision. | `1` | -| The wait timed out. | `124` | - -Because a zero exit can mean an unchanged or superseded revision, inspect the -revision history and the effective policy before you rely on new permissions: +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 ``` -A timeout means the CLI stopped polling. It does not tell you whether the -revision later loaded or failed, so check the revision status before you submit -again. +The latest revision should show `Loaded`, and the effective policy should +contain your change. -Then test one operation that should be allowed and one that should be blocked. +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. The [first network policy -tutorial](/get-started/tutorials/first-network-policy) demonstrates these -checks. +tool. ## Roll Back to an Earlier Revision -To restore an earlier policy, choose a compatible revision from -`openshell policy list my-sandbox` and print its base policy. For example, to -retrieve revision 2: +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 ``` -Copy the YAML below the `---` separator into `previous-base.yaml`, review the -complete policy, and submit it as a new revision: +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 -openshell policy list my-sandbox ``` -Rolling back a policy does not restore provider profiles, attachments, -credentials, or a gateway-global policy. Startup settings in the earlier -revision are still subject to the limits in [How Changes Take -Effect](/sandboxes/policies#how-changes-take-effect). +Rolling back restores only the policy. It does not restore provider profiles, +attachments, credentials, or a gateway-global policy, and the limits on +filesystem and process changes still apply. ## Apply a Gateway-Wide Policy A gateway administrator can apply one policy to every sandbox on the gateway. -The global policy replaces each sandbox's policy, blocks sandbox policy changes, -and suppresses provider-contributed network rules until you delete it. These -operations require the platform administrator role. +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 | |---|---| @@ -293,20 +261,14 @@ operations require the platform administrator role. | View global policy history. | `openshell policy list --global` | | Remove the global policy and restore normal policy selection. | `openshell policy delete --global` | -A global policy takes effect immediately, so `policy set --global` does not -accept `--wait`. The set and delete commands ask for confirmation unless you -pass `--yes`. Keep the prompt during interactive work, because each operation -changes the network access of every sandbox on the gateway. - -The gateway rejects deleting the global policy when the restored sandbox and -provider configuration would be invalid. Inspect the effective policy of -affected sandboxes before and after the change. +A global policy takes effect immediately. The set and delete commands ask for +confirmation, because each one changes the network access of every sandbox on +the gateway. Deleting the global policy fails if a sandbox's own policy and +provider rules would be invalid once restored. ## Next Steps -- Use [Network Rules](/sandboxes/network-rules) for example rules you can - adapt to common services and protocols. +- Use [Network Rules](/sandboxes/network-rules) for example rules you can adapt + to common services and protocols. - Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) when a change fails to load or a request is denied unexpectedly. -- Use the [Policy Command Reference](/reference/policy-updates) for every - `openshell policy` flag and the complete update syntax. diff --git a/docs/sandboxes/network-rules.mdx b/docs/sandboxes/network-rules.mdx index 7ca70faee6..efbb90c508 100644 --- a/docs/sandboxes/network-rules.mdx +++ b/docs/sandboxes/network-rules.mdx @@ -270,8 +270,8 @@ repos_except_private: ``` To add a deny rule to an existing REST rule without a complete policy file, use -`openshell policy update` with `--add-deny`, as described in the [Policy Command -Reference](/reference/policy-updates#request-rule-specification). +`openshell policy update` with `--add-deny`, as described in [Add or Remove +Network Access](/sandboxes/manage-policies#add-or-remove-network-access). Verify that the first request reaches the service and the second returns an OpenShell policy denial: From c33772f60646860cc40665cedbaad6e910090925 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 19:16:02 +0000 Subject: [PATCH 11/40] docs(policy): clarify policy views and sandbox deletion in management guide Signed-off-by: Johnny Greco --- docs/sandboxes/manage-policies.mdx | 35 +++++++++++++----------- docs/sandboxes/network-rules.mdx | 1 - docs/sandboxes/troubleshoot-policies.mdx | 3 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx index b0bac39e25..10db4c55ca 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/sandboxes/manage-policies.mdx @@ -13,8 +13,6 @@ section covers one task, 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. -The examples use `my-sandbox` as the sandbox name. - ## Create a Sandbox with a Policy Pass a policy file when you create a sandbox. Filesystem, Landlock, and process @@ -24,6 +22,10 @@ settings take effect only when the sandbox starts, so set them here: 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 +used most recently. + To use the same file for every sandbox you create, set `OPENSHELL_SANDBOX_POLICY` instead of passing `--policy`: @@ -51,24 +53,21 @@ 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 manage for -the sandbox. The effective policy is the one the sandbox enforces. It combines -the base policy with rules from attached providers, or it is the gateway-global -policy when one is active. +A sandbox's policy has two views. The base policy is the policy you set for the +sandbox. The effective policy is the policy the sandbox enforces, which is the +base policy plus any rules that attached providers add. When an administrator +sets a gateway-global policy, it replaces both, so the effective policy is 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 ``` -Choose the view that fits your task: - -- To edit the policy, start from the base view. The effective view also contains - rules that attached providers manage. Those rules are not part of the - sandbox's own policy, so leave them out of the file you edit. -- To find out why a request is allowed or denied, use the effective view. - Several rules, including provider rules, can allow the same request, and only - the effective view shows all of them. +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](/reference/policy-prover): @@ -190,13 +189,17 @@ To change these settings, save the base policy as described in [Replace the Complete Policy](#replace-the-complete-policy), edit it, and create a new sandbox with the file: + +Deleting a sandbox stops its processes and removes its state. Copy out anything +you need before you delete it. + + ```shell openshell sandbox delete my-sandbox openshell sandbox create --name my-sandbox --policy ./policy.yaml ``` -Deleting a sandbox stops its processes and removes its state, so copy out -anything you need first. When the policy allows network access, OpenShell adds +When the policy allows network access, OpenShell adds the system paths that sandbox processes need, so list only the paths your workload requires. [Default Policy](/reference/default-policy) lists those paths. diff --git a/docs/sandboxes/network-rules.mdx b/docs/sandboxes/network-rules.mdx index efbb90c508..8678bf1887 100644 --- a/docs/sandboxes/network-rules.mdx +++ b/docs/sandboxes/network-rules.mdx @@ -140,7 +140,6 @@ Profiles](/providers/profiles) explains profile endpoints. Each example states when to use the rule, gives the rule, shows how to verify it, and lists its limits. Replace the example hosts, paths, and binaries with your own values, and confirm that the sandbox contains the client executable. -The commands use `my-sandbox` as the sandbox name. When `openshell policy update` can express a rule, the example shows the command. Otherwise, add the YAML entry under the `network_policies` section of a diff --git a/docs/sandboxes/troubleshoot-policies.mdx b/docs/sandboxes/troubleshoot-policies.mdx index 01b5f2539c..c562b85014 100644 --- a/docs/sandboxes/troubleshoot-policies.mdx +++ b/docs/sandboxes/troubleshoot-policies.mdx @@ -16,8 +16,7 @@ before changing permissions. ## Identify the Failure Stage The following OpenShell CLI commands show the information needed to locate a -failure. Replace `my-sandbox` with the affected sandbox's name, then use the table -to find the relevant diagnosis: +failure: ```shell openshell sandbox get my-sandbox From ae8936adc402ad99667269b63777988e56768849 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 19:56:49 +0000 Subject: [PATCH 12/40] docs(policy): streamline network rule concepts and examples Signed-off-by: Johnny Greco --- docs/sandboxes/network-rules.mdx | 485 +++++++------------------------ 1 file changed, 102 insertions(+), 383 deletions(-) diff --git a/docs/sandboxes/network-rules.mdx b/docs/sandboxes/network-rules.mdx index 8678bf1887..df7e420bcd 100644 --- a/docs/sandboxes/network-rules.mdx +++ b/docs/sandboxes/network-rules.mdx @@ -3,8 +3,8 @@ # 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, JSON-RPC, native TCP, raw TLS, and provider credentials." -keywords: "Generative AI, Cybersecurity, Policy, Network, REST, TCP, WebSocket, GraphQL, MCP, JSON-RPC, PyPI, npm" +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 --- @@ -21,12 +21,13 @@ allows. ### Rule Structure -Each entry in `network_policies` is a named rule with 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. +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 lists two binaries and two endpoints: +For example, this rule is named `example_api` and lists two binaries and two +endpoints: ```yaml network_policies: @@ -53,15 +54,15 @@ The rule allows these four combinations: If `python3` should reach only `api.example.com`, put that binary and endpoint in a separate rule. -### Choose Which Binaries to List +### 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. A rule for `pip install` must list `/usr/bin/python3`, not `/usr/bin/pip`. When OpenShell denies a connection, the sandbox log shows the binary it identified. -[Package Registries](#package-registries) shows rules for `pip`, `uv`, and -`npm`. +[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 @@ -89,18 +90,18 @@ 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. | [HTTP APIs](#http-apis) | +| `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](#allow-json-rpc-methods) | +| `json-rpc` | JSON-RPC method name. | [JSON-RPC rules](/reference/policy-schema#json-rpc-rules) | | `tcp` | Nothing. The binary gets a native TCP connection, which suits clients such as databases. | [Native TCP](#allow-native-tcp) | | Omitted | No request rules apply. OpenShell still terminates TLS and checks that each HTTP request is addressed to the allowed host, but it allows any method or path. | -- | Use an inspected endpoint when the request, not only the destination, determines the risk, such as allowing reads but not writes on an API. -### Enforce and Audit +### Enforcement The `enforcement` field on an inspected endpoint decides what happens when a request breaks the endpoint's request rules: @@ -108,9 +109,17 @@ request breaks the endpoint's request rules: - `enforce` blocks the request with an OpenShell `policy_denied` response. - `audit` allows the request and logs the violation. This is the default. -Use `audit` to see what a new rule would block, then switch the endpoint to -`enforce`. An endpoint in audit mode blocks no requests. [Observe a Rule Before -Enforcing It](#observe-a-rule-before-enforcing-it) shows this workflow. +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 +``` + +Policy events are INFO-level log records, so do not filter them out with +`--level warn`. When the log shows only the violations you expect, switch the +endpoint to `enforce`. An endpoint in audit mode blocks no requests. ### Overlapping Rules @@ -123,7 +132,8 @@ 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. -[Block Specific Requests](#block-specific-requests) shows a deny rule. +[Allow Specific Methods and Paths](#allow-specific-methods-and-paths) shows a +deny rule. ### Network Access and Credentials @@ -131,31 +141,25 @@ 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. [Provider -Credentials](#provider-credentials) shows an explicit binding, and [Provider -Profiles](/providers/profiles) explains profile endpoints. +binding instead of widening the network rule. Refer to [Provider +Profiles](/providers/profiles) for profile endpoints, and to [Credential +Fields](/reference/policy-schema#credential-fields) for explicit bindings. -## Use the Examples +## Examples -Each example states when to use the rule, gives the rule, shows how to verify -it, and lists its limits. Replace the example hosts, paths, and binaries with -your own values, and confirm that the sandbox contains the client executable. +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 `openshell policy update` can express a rule, the example shows the -command. Otherwise, add the YAML entry under the `network_policies` section of a +command. 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](/sandboxes/manage-policies#replace-the-complete-policy). Keep unrelated -rules, and replace an existing rule with the same key only when that is your -intent. After you apply a rule, [verify the -change](/sandboxes/manage-policies#verify-a-change) before testing traffic. - -## HTTP APIs +Policy](/sandboxes/manage-policies#replace-the-complete-policy). After you apply +a rule, [verify the change](/sandboxes/manage-policies#verify-a-change) before +testing traffic. -These examples use `protocol: rest`, which lets OpenShell check each HTTP -request's method, path, and query values. - -### Allow Read-Only Access +### 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`. @@ -176,7 +180,6 @@ openshell policy update my-sandbox \ ```yaml github_readonly: - name: github-readonly endpoints: - host: api.github.com port: 443 @@ -204,15 +207,16 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ 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. -### Scope Access to a Repository +### Allow Specific Methods and Paths -Use this rule when an agent using the GitHub CLI needs repository API -operations for one repository. Replace `` and `` with the owner and -name, and keep only the executable paths used by your image: +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: - name: github-repository-api endpoints: - host: api.github.com port: 443 @@ -222,169 +226,43 @@ github_repository_api: - allow: method: "*" path: "/repos///**" + deny_rules: + - method: "*" + path: "/repos///hooks" + - method: "*" + path: "/repos///hooks/**" binaries: - path: /usr/bin/gh - path: /usr/local/bin/gh ``` -The rule grants all HTTP methods within that path, including writes. Narrow the -methods and paths if the agent needs only specific operations. It does not -grant Git transport access or GraphQL mutations. +In request paths, `*` matches within one path segment and `**` 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](/sandboxes/manage-policies#add-or-remove-network-access). Apply the rule with a GitHub provider attached. Its profile must permit -credential use at the destination, and the token must authorize the repository -operation. Provider-contributed read permissions remain available. Verify a -permitted operation on the selected repository, and confirm that a write to a -disposable second repository receives an OpenShell denial. Use `gh` for both -checks so the requests use the executable selected by this rule. - -In request paths, `*` can cross `/` boundaries. Quote globs in shell commands so -the local shell does not expand them. For a complete Git push example, see -[Grant GitHub Push Access to a Sandboxed Agent](/get-started/tutorials/github-sandbox). - -### Block Specific Requests - -Use a deny rule to carve an exception out of broader access. A matching deny -takes precedence over any allow, including allows from other rules. This -template allows reads under `/repos/` but blocks the private subtree. Replace -`api.example.com` and the paths with routes on an HTTP service you control: - -```yaml -repos_except_private: - name: repos-except-private - endpoints: - - host: api.example.com - port: 443 - protocol: rest - enforcement: enforce - rules: - - allow: - method: GET - path: /repos/** - deny_rules: - - method: GET - path: /repos/private/** - binaries: - - path: /usr/bin/curl -``` - -To add a deny rule to an existing REST rule without a complete policy file, use -`openshell policy update` with `--add-deny`, as described in [Add or Remove -Network Access](/sandboxes/manage-policies#add-or-remove-network-access). - -Verify that the first request reaches the service and the second returns an -OpenShell policy denial: - -```shell -openshell sandbox exec -n my-sandbox --no-login-shell -- \ - /usr/bin/curl --silent --show-error --fail \ - https://api.example.com/repos/public/project -openshell sandbox exec -n my-sandbox --no-login-shell -- \ - /usr/bin/curl --silent --show-error \ - https://api.example.com/repos/private/project -``` - -When unexpected access remains, compare `openshell policy get --base` with -`openshell policy get --full`. Provider-contributed rules appear only in the -effective view, and a gateway-global policy replaces sandbox and provider rules. - -### Match Query Values - -Use query matchers when the same path serves different resources selected by -query parameters. Matchers run on decoded, case-sensitive values. This template -requires `/usr/bin/curl` and an HTTP service you control: - -```yaml -download_query: - name: download-query - 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 -``` - -For an allow rule, every value for a duplicate query key must match. For a deny -rule, every configured key must be present with at least one matching value, -and an additional nonmatching duplicate does not cancel a matching value. Start -with a single query key when possible, then test the intended match and near -misses against your service. - -### Observe a Rule Before Enforcing It - -Use `enforcement: audit` to see which requests a rule would block before you -enforce it. Audit mode logs applicable request-rule violations and forwards the -requests. For a REST endpoint with `access: read-only`, a `POST` produces a -violation event but still reaches the upstream service if the other checks allow -it, so the resulting HTTP status comes from that service. - - - - -```shell -openshell policy update my-sandbox \ - --rule-name github_audit \ - --binary /usr/bin/curl \ - --add-endpoint api.github.com:443:read-only:rest:audit \ - --wait -``` - - - - -```yaml -github_audit: - name: github-audit - endpoints: - - host: api.github.com - port: 443 - protocol: rest - enforcement: audit - access: read-only - binaries: - - path: /usr/bin/curl -``` - - - - -Inspect policy events without a `--level warn` filter, because OCSF policy -events are INFO-level log records regardless of their event severity: - -```shell -openshell logs my-sandbox --since 5m --source sandbox -``` - -Look for an event identifying the request-policy violation even though the -request was forwarded. Audit does not bypass destination, executable, -credential, parser, or middleware checks. When the logs show only the violations -you expect, change the endpoint to `enforcement: enforce`. - -## Package Registries - -Package managers download packages with `GET` requests, so a read-only REST rule -allows installs while blocking uploads. List the executable that opens the -connection. `pip` and `npm` are scripts, so their rules list the Python or Node -interpreter. Because a rule also applies to processes that a listed binary -starts, listing an interpreter lets any script it runs reach the registry. +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](/get-started/tutorials/github-sandbox). ### Allow PyPI Downloads -Use this rule to let `pip` or `uv` install packages from PyPI: +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 Python interpreter, and any Python program in the sandbox can use it. +This rule lets `pip` and `uv` install packages from PyPI: ```yaml pypi: - name: pypi endpoints: - host: pypi.org port: 443 @@ -416,13 +294,13 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ ### Allow npm Installs -Use this rule to let `npm` install packages from the public npm registry. Node -images from the official Node.js project install `node` at -`/usr/local/bin/node`, so adjust the path for your image: +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. Node images from +the official Node.js project install `node` at `/usr/local/bin/node`, so adjust +the path for your image: ```yaml npm_registry: - name: npm-registry endpoints: - host: registry.npmjs.org port: 443 @@ -447,14 +325,12 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ /usr/bin/npm view @types/node version ``` -## Internal Services +### Restrict Destination Addresses OpenShell blocks connections to private network addresses by default to prevent -server-side request forgery (SSRF). Endpoints with an exact hostname are the -exception. They can reach the private addresses that their hostname resolves to. -Loopback, link-local, and cloud metadata addresses are always blocked. - -### Restrict Destination Addresses +server-side request forgery (SSRF). An endpoint with an exact hostname can still +reach the private addresses that its hostname resolves to, but loopback, +link-local, and cloud metadata addresses 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 @@ -476,7 +352,6 @@ openshell policy update my-sandbox \ ```yaml internal_api: - name: internal-api endpoints: - host: api.internal.example port: 443 @@ -493,20 +368,14 @@ internal_api: When an endpoint sets `allowed_ips`, every address its hostname resolves to must -fall within the list, including public addresses. `allowed_ips` constrains the -addresses accepted for a hostname but does not replace hostname authorization. -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. +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. -## Other Application Protocols - -These examples inspect protocols carried over HTTP. Each one requires explicit -rules for the protocol's operations. - ### Allow WebSocket Messages Use `protocol: websocket` for an RFC 6455 upgrade and client-to-server message @@ -516,7 +385,6 @@ upgraded path, while denying `/v1/admin/**`: ```yaml realtime: - name: realtime endpoints: - host: realtime.example.com port: 443 @@ -551,60 +419,15 @@ the client must reconnect. ### Allow GraphQL Operations -Use `protocol: graphql` for GraphQL-over-HTTP. This template requires -`/usr/bin/gh`, a GitHub credential with the required permissions, and the GitHub -GraphQL schema. It allows selected queries and `createIssue`, while denying -`deleteRepository`: - -```yaml -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 - fields: [createIssue] - deny_rules: - - operation_type: mutation - fields: [deleteRepository] - binaries: - - path: /usr/bin/gh -``` - -Test an allowed query and the denied mutation with the configured client. For -allow rules, every selected root field must match. For deny rules, one matching -root field blocks the request. A malformed, denied, or unregistered operation -denies an entire batched HTTP request. - -GraphQL field names are application-specific. Do not treat a copied field list -as a verified safety boundary. Review and test it against the authoritative -schema for the deployed service version. Hash-only persisted queries require -`persisted_queries: allow_registered` and a trusted `graphql_persisted_queries` -registry. - -For GraphQL-over-WebSocket, use `protocol: websocket`, allow the upgrade with a -`GET` rule, and add GraphQL operation rules for client operation messages. -Client operation messages fail closed when malformed or disallowed. Lifecycle -messages such as `connection_init`, `ping`, `pong`, and `complete` are allowed -without payload logging. - -### Combine REST and GraphQL on One Host - -Many APIs serve REST and GraphQL on the same host. A REST rule sees every -GraphQL call as `POST /graphql` and cannot tell a query from a mutation. Use the -endpoint `path` field to give each API its own endpoint in one rule: +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: - name: github-api endpoints: - host: api.github.com port: 443 @@ -629,8 +452,13 @@ github_api: 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`, and an MCP endpoint -cannot share a host and port with an endpoint that uses a different protocol. +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](/reference/policy-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 @@ -645,7 +473,6 @@ server you control. It allows initialization, tool discovery, and ```yaml mcp_server: - name: mcp-server endpoints: - host: mcp.example.com port: 443 @@ -676,52 +503,15 @@ 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](/reference/policy-schema#mcp-version-selection). Server responses and -SSE messages are relayed without MCP policy parsing. - -### Allow JSON-RPC Methods - -Use `protocol: json-rpc` for JSON-RPC-over-HTTP services other than MCP. This -template requires `/usr/bin/python3` with a JSON-RPC client and an HTTP service -you control. It allows `reports.list` and `reports.search`, while denying -`reports.delete`: - -```yaml -reports_rpc: - name: reports-rpc - endpoints: - - host: rpc.example.com - port: 443 - path: /rpc - protocol: json-rpc - enforcement: enforce - rules: - - allow: - method: reports.list - - allow: - method: reports.search - deny_rules: - - method: reports.delete - binaries: - - path: /usr/bin/python3 -``` - -Verify that `reports.list` succeeds and `reports.delete` is denied. Method names -are exact. Only `method: "*"` is accepted as an all-method sentinel, and other -globs are rejected. Parameter matchers are not supported. OpenShell evaluates -every call in a batch and denies the full batch if one call is denied. -Server-to-client messages are not parsed for policy enforcement. - -## Uninspected Connections - -These examples allow a connection without inspecting its application payload. -OpenShell still checks the destination, port, and executable. Prefer an -inspected protocol whenever the client supports it. +SSE messages are relayed without MCP policy parsing. An MCP endpoint cannot +share a host and port with an endpoint that uses a different protocol. ### Allow Native TCP Use `protocol: tcp` when the application needs ordinary DNS resolution and a -native TCP socket, such as a database client. The rule checks the hostname, -port, and executable but cannot inspect the application payload. +native TCP socket, such as a database client. OpenShell checks the hostname, +port, and executable but does not inspect the application payload, so prefer an +inspected protocol whenever the client supports one. @@ -739,7 +529,6 @@ openshell policy update my-sandbox \ ```yaml postgres: - name: postgres endpoints: - host: db.internal.example port: 5432 @@ -777,80 +566,10 @@ mapping expires, even while the endpoint remains allowed. Docker and Podman currently advertise IPv4 egress for policy DNS, so AAAA queries return a successful empty answer and dual-stack clients must continue with the A record. -### Allow a Raw TLS Stream - -Use `tls: skip` when a client that uses the sandbox proxy must complete TLS with -the upstream service itself, for example to present a client certificate for -mTLS. OpenShell checks the destination, port, and executable, then relays the -encrypted stream without terminating TLS, inspecting requests, or injecting -credentials: - -```yaml -mtls_service: - name: mtls-service - endpoints: - - host: secure.example.com - port: 443 - tls: skip - binaries: - - path: /usr/bin/curl -``` - -Omit `protocol` on a `tls: skip` endpoint, because OpenShell does not evaluate -request rules for the relayed stream. Every endpoint that shares the same host -and port must also use `tls: skip`. A provider-credentialed endpoint requires -`allow_uninspected_credentials: true`, and credential placeholders pass upstream -unchanged. Fail-closed middleware cannot select a skipped endpoint, and policy -advisor cannot propose one. Use `protocol: tcp` instead when the client opens -native connections without the sandbox proxy. - -Verify that the client completes the mTLS handshake. The client sees the -upstream service's own certificate rather than one issued by the sandbox CA: - -```shell -openshell sandbox exec -n my-sandbox --no-login-shell -- \ - /usr/bin/curl --silent --show-error --fail \ - --cert /sandbox/client.pem --key /sandbox/client.key \ - https://secure.example.com/ -``` - -## Provider Credentials - -These examples control where OpenShell supplies provider credentials, as -described in [Network Access and Credentials](#network-access-and-credentials). - -### Bind a Provider Credential to an Endpoint - -Use `credential_binding` when an attached provider's profile defines no -endpoints of its own, and you want its static credentials to be usable at a -destination that your sandbox policy allows. This example allows Google Cloud -Storage and binds the credentials from the attached `work-gcp` provider to that -endpoint: - -```yaml -gcp_storage: - name: gcp-storage - endpoints: - - host: storage.googleapis.com - port: 443 - protocol: rest - enforcement: enforce - access: full - credential_binding: - provider: work-gcp -``` - -The provider must be attached to the sandbox, and its profile must define no -endpoints. The binding is valid only in a sandbox policy, not in a -gateway-global policy. A request that uses the credential elsewhere is rejected -with `credential_endpoint_mismatch`. Refer to [Static Credential Endpoint -Binding](/providers/profiles#understand-static-credential-endpoint-binding). - -If a connection succeeds but credential resolution fails, inspect the provider -and binding diagnostics instead of granting a broader endpoint or setting -`allow_uninspected_credentials`. Request-body and WebSocket credential rewriting -apply only to their documented text formats and size limits. For AWS request -signing, refer to [AWS SigV4](/providers/aws-sigv4). +If a client that uses the sandbox proxy must complete TLS with the upstream +service itself, for example to present a client certificate, set `tls: skip` on +an endpoint without `protocol` instead. Refer to [Inspection +Fields](/reference/policy-schema#inspection-fields). ## Next Steps From 2cda23f1c40b33bbf276fa62d52e032e078f9104 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 19:58:37 +0000 Subject: [PATCH 13/40] docs(policy): correct request path wildcard semantics Signed-off-by: Johnny Greco --- docs/how-it-works/policies/schema.mdx | 8 ++++---- docs/sandboxes/manage-policies.mdx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 87f419ffd7..d59e005653 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -440,7 +440,7 @@ REST rules match HTTP requests by method, path, and optional query parameters. | Field | Type | Required | Description | |---|---|---|---| | `method` | string | Yes | HTTP method, such as `GET` or `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. | +| `path` | string | Yes | URL path glob. `*` matches zero or more characters within one path segment, and `**` matches zero or more characters across segments. `?` matches one character. Bracket classes such as `[0-9]` and `[!0]` are supported. | | `query` | map | No | Query parameter matchers keyed by decoded, case-sensitive name. A matcher is a glob string (`tag: "foo-*"`) or an object with `any` (`tag: { any: ["foo-*", "bar-*"] }`). | In an allow rule, every duplicate value for a configured query key must match. @@ -467,9 +467,9 @@ endpoints: any: ["v1.*", "v2.*"] deny_rules: - method: POST - path: "/repos/*/pulls/*/reviews" + path: "/repos/*/*/pulls/*/reviews" - method: "*" - path: "/repos/*/rulesets" + path: "/repos/*/*/rulesets" ``` ### WebSocket Rules @@ -799,7 +799,7 @@ Different policy fields use different wildcard boundaries: |---|---|---| | Endpoint `host` | Case-insensitive DNS name or IP comparison. | DNS `*` matches one label and `**` matches one or more labels. Validation restricts wildcard placement. | | Binary `path` | Canonical executable or trusted ancestor path. | Symlinks resolve to canonical identity. `*` matches within a path segment and `**` crosses directories. Identity enforcement depends on trusted runtime configuration. | -| REST or WebSocket request `path` | Case-sensitive URL path glob. | Both `*` and `**` can cross `/`, unlike common shell globs. | +| REST or WebSocket request `path` | Case-sensitive URL path glob. | `*` matches within one path segment and `**` crosses `/`. `/repos/**` does not match `/repos` itself. | | Query value | Case-sensitive decoded value glob. | In allow rules, every duplicate value for a configured key must match. | | Middleware `endpoints` | Case-insensitive DNS name comparison. | Same as endpoint `host`. Brace alternates are rejected. | diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx index 10db4c55ca..992aad0708 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/sandboxes/manage-policies.mdx @@ -112,7 +112,7 @@ command changes only that rule: openshell policy update my-sandbox \ --rule-name github_readonly \ --binary /usr/bin/curl \ - --add-allow 'api.github.com:443:POST:/repos/*/issues' \ + --add-allow 'api.github.com:443:POST:/repos/*/*/issues' \ --wait ``` From 14f560da8c742d691e81b681fdc6b4a7b280b62c Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 21:01:16 +0000 Subject: [PATCH 14/40] docs(policy): rewrite policy advisor guide for clarity Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 425 +++++++++-------------- docs/how-it-works/policies/prover.mdx | 6 +- docs/how-it-works/policies/schema.mdx | 5 +- docs/observability/logging.mdx | 2 +- docs/sandboxes/troubleshoot-policies.mdx | 2 +- docs/security/best-practices.mdx | 2 +- 6 files changed, 172 insertions(+), 270 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 26c0718140..d5054f2f97 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -1,67 +1,68 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Use Policy Advisor" +title: "Policy Advisor" sidebar-title: "Advisor" -description: "Let sandboxed agents propose narrow policy changes through policy.local while keeping developer approval in the loop." +description: "Let an agent in a sandbox propose the network rule it needs when OpenShell blocks a request, and review each proposal before it takes effect." keywords: "Generative AI, Cybersecurity, Policy Advisor, Policy, Sandbox, policy.local, Agent Policy" position: 4 --- -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 waits in the draft inbox for human review. -Opt-in [auto mode](#automatic-approval) approves a proposal without a reviewer -only when the policy prover finds no new risk and the rule produces no security -notes. - -## How It Works - -When policy advisor is enabled, the sandbox supervisor turns on these -agent-facing surfaces: - -- It installs `/etc/openshell/skills/policy_advisor.md` inside the sandbox. -- It installs `/etc/openshell/skills/policy-advisor/SKILL.md` as a short - Codex and 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 request-level `policy_denied` - response bodies so the agent can find the skill and local API. - -A proposal moves through this loop: - -1. A sandboxed process attempts a network request that policy denies. 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`. -2. The agent reads the policy advisor skill, inspects the current policy, and - optionally reads recent denial log lines. -3. The agent submits one or more `addRule` proposals to - `http://policy.local/v1/proposals`. -4. The gateway turns each proposal into the exact effective-policy candidate it - would apply, validates it, and runs the [policy prover](#automatic-approval) - against it. -5. A developer approves or rejects the proposal. In auto mode, the gateway - approves an eligible proposal without review. -6. The agent waits on `/v1/proposals/{chunk_id}/wait` until a decision is - available. - -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, so the agent -can narrow the next attempt to the specific concern the prover flagged. - -## 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: +When OpenShell blocks a network request, an agent working in the sandbox usually +cannot finish its task until someone changes the policy. The policy advisor lets +the agent propose the network rule it needs. You review each proposal and +approve or reject it. An approved rule takes effect in the running sandbox +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 only +when OpenShell's checks find that it adds no new risk. + +## 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, checks it for added risk, and adds it to + the sandbox's pending proposals. +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 the denials it observes in the +sandbox. These drafts go through the same checks and review as proposals from +the agent. + +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. + 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 + +Enable the policy advisor for every sandbox on the gateway: ```shell openshell settings set --global \ @@ -70,7 +71,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 \ @@ -78,15 +80,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 \ @@ -94,98 +91,73 @@ 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. +Enable the policy advisor before you create a sandbox. A running sandbox picks +up the change, but the agent learns about the policy advisor from its first +denied request, so it is most reliable when the setting is on from the start. ## Review Proposals -Review pending proposals from the host: +List the sandbox's pending proposals: ```shell openshell rule get --status pending ``` -The output shows the chunk ID, status, rationale, binary, endpoint summary, -prover result, application error (if any), and candidate hash. For request-level -proposals, the endpoint summary includes the protocol, method, and path: +Each proposal shows its ID on the `Chunk` line, the rule it would add, the +agent's stated reason, and the results of OpenShell's risk checks on the +`Security` and `Prover` lines. For an HTTP rule, the endpoint summary includes +the 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: +Approve a proposal only when the rule matches the access you intend to grant: ```shell -openshell rule approve --chunk-id +openshell rule approve --chunk-id ``` -Reject with guidance when the rule is too broad or points at the wrong target: +Reject a proposal that is too broad or targets the wrong service, and explain +why. OpenShell returns your reason to the agent, which can use it to submit a +narrower proposal: ```shell openshell rule reject \ - --chunk-id \ + --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. - -### Review Tokens - -Each stored proposal records its candidate policy, prover result, any -application error, and a review token tied to the live base policy, provider -rules, and non-secret credential metadata. Provider rules remain immutable -inputs. - -```mermaid -flowchart TD - A["Denied request creates a narrow proposal"] --> B["Gateway builds the exact effective-policy candidate"] - B --> C["Validate merge, request 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 -``` +You can also review proposals in the terminal UI. Run `openshell term`, select +the sandbox, and open its network rules. A rejected proposal shows your reason +on its `Guidance` line. + +### When a Proposal Changes Before Approval -`openshell rule approve` fetches the current review token and submits it with -the approval. Before applying the proposal, the gateway recomputes the token -from live inputs. When the token is unchanged, it reuses the stored prover -result. When policy, provider, or credential inputs changed after the proposal -was displayed, the gateway evaluates and stores the refreshed candidate, leaves -the proposal pending, and asks you to review it again. Run `rule get` before -retrying. Bulk approval binds each selected proposal to its own review token in -the same way, and edits and deduplicated resubmissions follow the same path. +OpenShell checks a proposal against the sandbox's policy, providers, and +credentials at the time it was submitted. If any of these change before you +approve the proposal, OpenShell does not apply the outdated version. It +recalculates the proposal against the current configuration, keeps it pending, +and asks you to review it again. Run `openshell rule get` to see the updated +proposal, then approve it. -Merge, policy-shape, provider-composition, credential, or prover failures are -shown as application errors and cannot be approved. +You cannot approve a proposal that fails validation, such as one that conflicts +with an existing rule. The proposal shows the error on its `Application` line +instead. -## Automatic Approval +## Approve Low-Risk Proposals Automatically -Every proposal, whether mechanistic or agent-authored, is routed through the -policy prover. The gateway also recalculates security notes from the current -draft rule. The `proposal_approval_mode` setting decides whether proposals that -pass both checks still require human review: +By default, every proposal waits for your review. In automatic mode, OpenShell +approves a proposal without review when both of these are true: -| Proposal | `manual` or unset | `auto` | -|---|---|---| -| Empty prover delta and no security notes | Waits 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 | Waits in the draft inbox. | Remains pending for human review. | +- The policy prover finds none of the risks described in [What the Prover + Checks](#what-the-prover-checks). +- The proposal has no security notes. Security notes flag private or internal + addresses, wildcard hosts, `allowed_ips` entries without a host, ephemeral + ports, and well-known database or service ports. -`manual` is the default. Enable auto mode at gateway scope when you want every -sandbox on this gateway to auto-approve eligible proposals: +Every other proposal still waits for your review. Turn on automatic mode for +every sandbox on the gateway: ```shell openshell settings set --global \ @@ -194,71 +166,40 @@ openshell settings set --global \ --yes ``` -Enable it for one sandbox when no global value is set: - -```shell -openshell settings set \ - --key proposal_approval_mode \ - --value auto -``` - -The create-time shorthand writes the sandbox-scoped setting for you: +To turn it on for one sandbox, set the key on that sandbox, or pass +`--approval-mode auto` when you create it: ```shell openshell sandbox create --approval-mode auto ``` -Only `manual` and `auto` are accepted, and typos such as `autom` are rejected -when you set the value. Stale or unknown values found in storage are treated as -`manual` at runtime as a defense-in-depth measure. Gateway scope wins over -sandbox scope, so a reviewer can pin `manual` for a fleet by setting it -globally. Per-sandbox values apply only when no global value is set. - -Under `auto` mode, approved proposals appear under -`openshell rule get --status approved` with auto-approval audit -fields. Under `manual` mode, every accepted proposal appears as pending -regardless of the prover verdict or security notes. +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. ### What the Prover 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 prover asks four formal questions about the proposed change. Each "yes" is -one categorical finding, and any finding blocks auto-approval: +The policy prover compares what the sandbox can reach with and without the +proposed rule. It reports a finding when the rule would give a binary new access +of one of these kinds: -| Category | Triggered when | +| Finding | The proposed rule would let a binary | |---|---| -| `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 request 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, with no severity tier. The reviewer reads the category -and the structured evidence to decide. - -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 proposal pending in auto mode. - -The full reasoning model is in +| `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-remote-https`, `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. | + +A proposal with any finding or security note 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. For the prover's full reasoning +model, refer to [`crates/openshell-prover/README.md`](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-prover/README.md). -Provider profiles composed in through [Profiles](/providers/profiles) are part -of the effective policy the prover reasons over. ## What Agents Can Propose -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 request samples are available, it can draft REST method and path rules. Otherwise it drafts an endpoint-only rule. | -| 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. | - -For REST APIs, prefer request rules over broad endpoint access. A good proposal -allows one method and the smallest safe path: +An agent proposes an endpoint and binary, optionally with REST method and path +rules. A good proposal allows one method on the narrowest path the task needs: ```json { @@ -297,104 +238,64 @@ allows one method and the smallest safe path: } ``` -The `policy.local` proposal 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 workflow](/sandboxes/manage-policies). Omitting -`protocol` remains supported and keeps the explicit proxy's default TLS -termination and HTTP authority checks. Other fields, such as WebSocket -credential rewrite, GraphQL operation matching, endpoint path scoping, and -provider-owned policy bundles, are also outside the proposal surface. - -### Private and Blocked Destinations - -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 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. - -### Proposal Provenance - -OpenShell records internally whether policy advisor introduced an endpoint or a -binary. Provenance is not a policy YAML field. When an advisor proposal overlaps -an endpoint that a user or provider declared explicitly, the explicit -declaration wins and a provider rule stays immutable. For example, a GitHub -provider can declare read access to `api.github.com:443`, and an approved -proposal for `PUT /repos/NVIDIA/OpenShell/contents/docs/**` on the same endpoint -is stored in the sandbox policy layer. - -Provenance does not hide a real conflict. Overlapping declarations still fail -validation if they disagree on settings that must have one value, such as TLS -mode, `allowed_ips`, an equally specific protocol or parser contract, credential -binding, or enforcement mode. Compatible allow and deny rules can combine. - -Advisor-introduced endpoints and binaries do not establish exact-host SSRF -trust, which requires an explicit exact endpoint and matching binary in the same -rule. A proposal cannot extend a provider's binary identity to a different -binary. For example, if a provider declares `api.github.com:443` for -`/usr/bin/gh` and the advisor proposes the same endpoint for `/usr/bin/curl`, -the proposed rule for `curl` remains subject to the normal SSRF checks. +Agents cannot propose `protocol: tcp` or `tls: skip`, because OpenShell cannot +inspect that traffic. WebSocket, GraphQL, and MCP rules, credential settings, +and endpoint path selectors are also outside what an agent can propose. Add +those rules yourself, as described in [Manage Sandbox +Policies](/sandboxes/manage-policies). + +Agents also cannot add `allowed_ips`. If a proposed host resolves to a private +address, OpenShell still blocks the connection after approval until you add an +`allowed_ips` entry yourself. Loopback, link-local, and cloud metadata addresses +are always blocked, and you cannot approve a proposal that targets them. + +An approved proposal becomes part of the sandbox's own policy and never changes +rules that providers contribute. When it overlaps an existing rule, its allow +rules combine with that rule. It cannot change settings that must have a single +value, such as TLS handling or `allowed_ips`. A proposed rule also does not +inherit trust from a provider rule for a different binary. ## 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 removed. | +| `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 request-level deny bodies do not -advertise `policy.local` routes or include `agent_guidance`. +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`. -## Audit Events +## Logs -Approved network rules hot-reload without restarting the sandbox. Connections -attached to the previous policy generation close at the reload boundary, so a -retry opens against the current generation. Refer to [How Changes Take -Effect](/sandboxes/policies#how-changes-take-effect) for HTTP, WebSocket, -tunnel, and long-lived stream behavior. +An approved rule takes effect in the running sandbox without a restart. +OpenShell closes connections opened under the previous rules, so the agent's +retry uses the new rule. The `/wait` endpoint reports `policy_reloaded: true` +once the sandbox has loaded the approved rule. -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. Every auto-approval emits `CONFIG:APPROVED` with -`auto=true`, `source=`, `prover_delta=empty`, and -`resolved_from=`, so operators can reconstruct why an -approval ran without human review. +Look for the denied request (`HTTP:* DENIED`), then `CONFIG:PROPOSED`, +`CONFIG:APPROVED` or `CONFIG:REJECTED`, `CONFIG:LOADED`, and the retried +request. An automatic approval logs `CONFIG:APPROVED` with `auto=true`, the +proposal's source (`agent_authored` or `mechanistic`, for drafts that OpenShell +created from denials), and where the approval mode setting came from. ## Next Steps -- Use [Manage Sandbox Policies](/sandboxes/manage-policies) for manual policy - changes. +- Use [Manage Sandbox Policies](/sandboxes/manage-policies) to change policies + yourself. - Use [Network Rules](/sandboxes/network-rules) and the - [Policy Schema Reference](/reference/policy-schema) for rule syntax outside - the proposal surface. -- Use the [Standalone Policy Prover](/reference/policy-prover) for optional - boundary checks and its explicit modeled-domain limits. -- Use [Logging](/observability/logging) to interpret OCSF shorthand log entries. + [Policy Schema Reference](/reference/policy-schema) for rules that agents + cannot propose. +- Use the [Standalone Policy Prover](/reference/policy-prover) to check a + complete policy against a boundary you define. diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index bde17cc90e..5d46cf1413 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -16,9 +16,9 @@ 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. -[Policy advisor](/sandboxes/policy-advisor#what-the-prover-checks) runs a -separate prover check automatically on each proposal, reporting whether the -proposal adds new reach compared with the current policy. Use this command when +The [policy advisor](/sandboxes/policy-advisor#what-the-prover-checks) runs a +separate prover check automatically on each proposal to find whether the +proposal adds new access compared with the current policy. Use this command when you need to check a complete policy against a fixed boundary, for example in CI. diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index d59e005653..63c4ccc504 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -215,8 +215,9 @@ access and narrows only which requests receive provider credentials. `allowed_ips` controls server-side request forgery (SSRF) protection. Exact user-declared hostname endpoints may resolve to RFC 1918 private addresses -without this field. Wildcard, hostless, and policy-advisor-proposed endpoints -still require `allowed_ips` for private resolved addresses. When an endpoint +without this field. Wildcard endpoints, hostless endpoints, and endpoints added +through the policy advisor still require `allowed_ips` for private resolved +addresses. When an endpoint sets `allowed_ips`, every resolved address must fall within the list, including public addresses. A hostless allowlist is valid only on the explicit proxy path, matches any hostname on the port, and cannot be combined with `protocol: tcp`. 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/sandboxes/troubleshoot-policies.mdx b/docs/sandboxes/troubleshoot-policies.mdx index c562b85014..9836fc1953 100644 --- a/docs/sandboxes/troubleshoot-policies.mdx +++ b/docs/sandboxes/troubleshoot-policies.mdx @@ -256,7 +256,7 @@ These error codes and conditions identify policy-related failures: | `credential_placeholder_in_request_body` | HTTP 403 response body. | A request body contains an invalid or unavailable credential placeholder. | [Credential denials](#diagnose-credential-denials) | | `ConfigurationInvalid` | Sandbox condition while `Provisioning`. | The initial effective policy or provider configuration failed validation. | [Initial configuration](#repair-initial-configuration) | | `ProvisioningTimedOut` | Sandbox `Error` reason. | The repair window expired before the configuration became valid. | [Initial configuration](#repair-initial-configuration) | -| `feature_disabled` | `policy.local` response inside the sandbox. | Policy advisor is disabled for the sandbox. | [Enable Policy Advisor](/sandboxes/policy-advisor#enable-policy-advisor) | +| `feature_disabled` | `policy.local` response inside the sandbox. | The policy advisor is disabled for the sandbox. | [Enable the Policy Advisor](/sandboxes/policy-advisor#enable-the-policy-advisor) | ## Next Steps diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index 8f4afc2e7f..be254db410 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -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. | From fb4f501ff408af7626a8684e362293bedb2df2a5 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 21:25:04 +0000 Subject: [PATCH 15/40] docs(policy): clarify policy advisor scope, setup, and review Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 91 ++++++++++++-------------- 1 file changed, 41 insertions(+), 50 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index d5054f2f97..3fbf8409d6 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -8,11 +8,15 @@ keywords: "Generative AI, Cybersecurity, Policy Advisor, Policy, Sandbox, policy position: 4 --- -When OpenShell blocks a network request, an agent working in the sandbox usually -cannot finish its task until someone changes the policy. The policy advisor lets -the agent propose the network rule it needs. You review each proposal and -approve or reject it. An approved rule takes effect in the running sandbox -without a restart. +The policy advisor lets an agent in a sandbox propose a new network rule when +OpenShell blocks one of its network requests, and you approve or reject each +proposal. 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 @@ -32,8 +36,12 @@ A proposal goes through these steps: 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, checks it for added risk, and adds it to - the sandbox's pending proposals. +3. OpenShell validates the proposal and checks it for added risk in two ways. + The policy prover 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 [What the Prover Checks](#what-the-prover-checks). 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. @@ -62,6 +70,13 @@ so the agent can find and use it: ## 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 @@ -91,36 +106,25 @@ openshell settings delete --global \ --yes ``` -Enable the policy advisor before you create a sandbox. A running sandbox picks -up the change, but the agent learns about the policy advisor from its first -denied request, so it is most reliable when the setting is on from the start. - ## Review Proposals -List the sandbox's pending 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 rule get --status pending ``` -Each proposal shows its ID on the `Chunk` line, the rule it would add, the -agent's stated reason, and the results of OpenShell's risk checks on the -`Security` and `Prover` lines. For an HTTP rule, the endpoint summary includes -the method and path: - -```text -Endpoints: api.github.com:443 [L7 rest, allow PUT /repos/NVIDIA/OpenShell/contents/docs/**] -``` - -Approve a proposal only when the rule matches the access you intend to grant: +Approve a proposal when its rule grants only the access you intend. Use the ID +from the proposal's `Chunk` line: ```shell openshell rule approve --chunk-id ``` -Reject a proposal that is too broad or targets the wrong service, and explain -why. OpenShell returns your reason to the agent, which can use it to submit a -narrower proposal: +Otherwise, reject it with a reason. OpenShell sends the reason to the agent, +which can submit a narrower proposal: ```shell openshell rule reject \ @@ -128,22 +132,10 @@ openshell rule reject \ --reason "Scope this to docs/ paths only." ``` -You can also review proposals in the terminal UI. Run `openshell term`, select -the sandbox, and open its network rules. A rejected proposal shows your reason -on its `Guidance` line. - -### When a Proposal Changes Before Approval - -OpenShell checks a proposal against the sandbox's policy, providers, and -credentials at the time it was submitted. If any of these change before you -approve the proposal, OpenShell does not apply the outdated version. It -recalculates the proposal against the current configuration, keeps it pending, -and asks you to review it again. Run `openshell rule get` to see the updated -proposal, then approve it. - -You cannot approve a proposal that fails validation, such as one that conflicts -with an existing rule. The proposal shows the error on its `Application` line -instead. +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. You can also review proposals in the terminal UI with +`openshell term`. ## Approve Low-Risk Proposals Automatically @@ -152,9 +144,10 @@ approves a proposal without review when both of these are true: - The policy prover finds none of the risks described in [What the Prover Checks](#what-the-prover-checks). -- The proposal has no security notes. Security notes flag private or internal - addresses, wildcard hosts, `allowed_ips` entries without a host, ephemeral - ports, and well-known database or service ports. +- OpenShell has not flagged the proposal's destination. OpenShell flags private + or internal addresses, wildcard hosts, `allowed_ips` entries without a host, + ephemeral ports, and well-known database or service ports, and shows each + flag on the proposal's `Security` line. Every other proposal still waits for your review. Turn on automatic mode for every sandbox on the gateway: @@ -190,11 +183,9 @@ of one of these kinds: | `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. | -A proposal with any finding or security note 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. For the prover's full reasoning -model, refer to -[`crates/openshell-prover/README.md`](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-prover/README.md). +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. ## What Agents Can Propose @@ -273,7 +264,7 @@ new sandboxes do not receive the guide, and denial responses do not mention ## Logs -An approved rule takes effect in the running sandbox without a restart. +An approved network rule takes effect in the running sandbox without a restart. OpenShell closes connections opened under the previous rules, so the agent's retry uses the new rule. The `/wait` endpoint reports `policy_reloaded: true` once the sandbox has loaded the approved rule. From c5e4b0c49dbe3d80d22f6dcb00eb8d8835f2742c Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 21:28:38 +0000 Subject: [PATCH 16/40] docs(policy): rewrite policy prover guide for clarity Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 2 +- docs/how-it-works/policies/prover.mdx | 326 +++++++++---------------- 2 files changed, 116 insertions(+), 212 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 3fbf8409d6..c765357607 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -288,5 +288,5 @@ created from denials), and where the approval mode setting came from. - Use [Network Rules](/sandboxes/network-rules) and the [Policy Schema Reference](/reference/policy-schema) for rules that agents cannot propose. -- Use the [Standalone Policy Prover](/reference/policy-prover) to check a +- Use the [Policy Prover](/reference/policy-prover) to check a complete policy against a boundary you define. diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index 5d46cf1413..bccc0b3ca8 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -1,49 +1,51 @@ --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -title: "Standalone Policy Prover" +title: "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" +description: "Check that a policy grants no more access than a boundary policy you define, for example before you apply a change or in CI." +keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, CI" position: 8 --- -`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 policy prover checks that a policy grants no more access than a limit you +set. You write the limit as a second policy, called the boundary. The prover +determines whether everything allowed by the policy you are testing, called the +candidate, is also allowed by the boundary. If the candidate allows something +the boundary does not, the prover shows an example. -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. +The prover checks only some parts of a policy: filesystem access, process +identity, Landlock settings, network destinations, and REST methods and paths. +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. + +Use the prover to check a policy before you apply it, for example in CI, so +that no change grants more than your organization allows. The prover is a +separate command, `openshell-prover`, that reads policy files on your machine. +It does not connect to a gateway, and it does not apply or approve policies. The [policy advisor](/sandboxes/policy-advisor#what-the-prover-checks) runs a -separate prover check automatically on each proposal to find whether the -proposal adds new access compared with the current policy. Use this command when -you need to check a complete policy against a fixed boundary, for example in -CI. +different prover check on each proposal. That check compares the proposal with +the sandbox's current policy instead of a boundary you write. ## Install the Prover -The standard OpenShell installer includes `openshell-prover`: +The 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. +To install only the prover, download its archive from +[OpenShell releases](https://github.com/NVIDIA/OpenShell/releases). The +[Support Matrix](/reference/support-matrix#standalone-policy-prover) lists the +available platforms. The prover runs on its own, without the `openshell` CLI, a +gateway, or a separate Z3 installation. -## Check a Policy Boundary +## Check a Policy -Create `boundary.yaml` with this boundary policy: +Create `boundary.yaml`, a boundary that allows reading `/usr` and `/etc`: ```yaml version: 1 @@ -53,7 +55,7 @@ filesystem_policy: - /etc ``` -Create `candidate.yaml` with this contained candidate: +Create `candidate.yaml`, a candidate that allows reading only `/usr`: ```yaml version: 1 @@ -62,16 +64,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 the policy that the prover checked. Now +change the candidate so that it also allows writing to `/tmp`, which the +boundary does not allow: ```yaml version: 1 @@ -82,196 +90,92 @@ 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 covers +everything the sandbox can reach. 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. In CI, 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 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 a Passing Result Means + +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 prover reports `unsupported` instead of guessing when the answer depends on +something it cannot see or does not check: + +- Network rules other than connection rules and REST rules, such as WebSocket, + GraphQL, MCP, and JSON-RPC rules. +- Filesystem comparisons between different paths, such as narrowing `/tmp` to + `/tmp/cache`. A symlink in the sandbox image could make the paths refer to + different locations. Removing a path, or changing it from read-write to + read-only, is supported. +- Binary rules that compare an exact path with a glob, because a symlink in the + sandbox image could change which executable the path refers to. Use matching + exact paths when you can. +- Settings that depend on the sandbox image, such as the working directory. +- Endpoints for the same host and port that set different `allowed_ips`, or + that mix connection-only and REST rules. +- Process identity changes other than changing a non-root user or group to + root. The prover reports a change to root as exceeding the boundary. +- Host, path, method, or binary values that contain non-ASCII characters. + +The prover also handles these cases in specific ways: + +- Changing Landlock from `hard_requirement` to `best_effort` exceeds the + boundary. The prover compares the settings, not what a kernel enforces. +- The prover checks network rules both with and without binary identity + enforcement, because a sandbox runtime can turn it off. +- The prover does not resolve hostnames on your machine. It checks + `allowed_ips` ranges together with hosts, ports, binaries, and REST rules. +- 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`. From 994763b38574f4d97833e9ef470067e9d6349c28 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 21:41:06 +0000 Subject: [PATCH 17/40] docs(policy): explain the two uses of the policy prover Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 10 ++-- docs/how-it-works/policies/prover.mdx | 73 +++++++++++++------------- 2 files changed, 44 insertions(+), 39 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index c765357607..9733d14416 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -172,9 +172,13 @@ every sandbox by setting `manual` on the gateway. ### What the Prover Checks -The policy prover compares what the sandbox can reach with and without the -proposed rule. It reports a finding when the rule would give a binary new access -of one of these kinds: +The policy advisor uses the same [policy prover](/reference/policy-prover) as +the `openshell-prover` command, but runs a different check. Instead of comparing +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: | Finding | The proposed rule would let a binary | |---|---| diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index bccc0b3ca8..44af225a02 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -3,47 +3,48 @@ # SPDX-License-Identifier: Apache-2.0 title: "Policy Prover" sidebar-title: "Prover" -description: "Check that a policy grants no more access than a boundary policy you define, for example before you apply a change or in CI." +description: "Use the policy prover to check that a policy grants no more access than a boundary policy you define, and learn how the policy advisor uses the same prover." keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, CI" position: 8 --- -The policy prover checks that a policy grants no more access than a limit you -set. You write the limit as a second policy, called the boundary. The prover -determines whether everything allowed by the policy you are testing, called the -candidate, is also allowed by the boundary. If the candidate allows something -the boundary does not, the prover shows an example. +The policy prover is OpenShell's policy verification engine. Instead of testing +individual requests, it uses the Z3 solver to reason about every request that a +policy could allow. OpenShell uses the prover in two ways, and each answers a +different question: -The prover checks only some parts of a policy: filesystem access, process -identity, Landlock settings, network destinations, and REST methods and paths. -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. - -Use the prover to check a policy before you apply it, for example in CI, so -that no change grants more than your organization allows. The prover is a -separate command, `openshell-prover`, that reads policy files on your machine. -It does not connect to a gateway, and it does not apply or approve policies. - -The [policy advisor](/sandboxes/policy-advisor#what-the-prover-checks) runs a -different prover check on each proposal. That check compares the proposal with -the sandbox's current policy instead of a boundary you write. - -## Install the Prover - -The OpenShell installer includes `openshell-prover`: - -```shell -curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh -openshell-prover --version -``` - -To install only the prover, download its archive from -[OpenShell releases](https://github.com/NVIDIA/OpenShell/releases). The -[Support Matrix](/reference/support-matrix#standalone-policy-prover) lists the -available platforms. The prover runs on its own, without the `openshell` CLI, a -gateway, or a separate Z3 installation. - -## Check a Policy +| | `openshell-prover check` | Policy advisor | +|---|---|---| +| Question | Does this policy stay within a limit that I set? | Does this proposal add risky new access? | +| When it runs | When you run the command, for example in CI. | Automatically, on every proposal. | +| What it compares | A candidate policy against a boundary policy that you write. | The sandbox's policy with and without the proposed rule. | +| What it reports | Access that the candidate allows beyond the boundary. | New access to link-local or cloud metadata addresses, uninspected traffic where a provider credential is available, new credentialed destinations, and new HTTP methods. | + +Because the two checks answer different questions, passing one does not imply +passing the other. A proposal that adds no risky access can still exceed your +boundary, and a policy within your boundary can still contain access that the +policy advisor would flag. + +This page covers the `openshell-prover check` command. For the policy advisor's +check, refer to [What the Prover +Checks](/sandboxes/policy-advisor#what-the-prover-checks). + +## Check a Policy Against a Boundary + +`openshell-prover check` determines whether everything allowed by the policy you +are testing, called the candidate, is also allowed by a boundary policy that you +write. If the candidate allows something the boundary does not, the prover +shows an example. Use it to check a policy before you apply it, for example in +CI, so that no change grants more than your organization allows. + +The check covers filesystem access, process identity, Landlock settings, network +destinations, and REST methods and paths. 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. + +The `openshell-prover` command is installed with OpenShell. It is a separate +command because it reads policy files on your machine and does not need a +gateway. It does not apply or approve policies. Create `boundary.yaml`, a boundary that allows reading `/usr` and `/etc`: From 11bd3f445fd44aae74a887e5a97a78ea4cb0f790 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 22:33:26 +0000 Subject: [PATCH 18/40] docs(policy): describe policy prover uses, boundaries, and coverage Signed-off-by: Johnny Greco --- docs/how-it-works/policies/prover.mdx | 134 +++++++++++++++++--------- 1 file changed, 89 insertions(+), 45 deletions(-) diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index 44af225a02..1b9a3f0e0d 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -3,20 +3,26 @@ # SPDX-License-Identifier: Apache-2.0 title: "Policy Prover" sidebar-title: "Prover" -description: "Use the policy prover to check that a policy grants no more access than a boundary policy you define, and learn how the policy advisor uses the same prover." -keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, CI" -position: 8 +description: "Understand how OpenShell uses the policy prover, and check that a policy grants no more access than a maximum policy you define." +keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, CI, Subagent" +position: 4 --- -The policy prover is OpenShell's policy verification engine. Instead of testing -individual requests, it uses the Z3 solver to reason about every request that a -policy could allow. OpenShell uses the prover in two ways, and each answers a -different question: +The policy prover is OpenShell's policy verification engine. It translates +policies into logical formulas and uses the Z3 SMT solver to search for a +request that answers a specific question, such as whether any request exists +that one policy allows and another denies. Because the solver searches every +possible request instead of testing a sample, a result covers all requests, not +only the ones you thought to try. + +OpenShell uses the prover in two ways: the `openshell-prover check` command, and +the [policy advisor](/sandboxes/policy-advisor), which checks every network rule +that an agent proposes. Each answers a different question: | | `openshell-prover check` | Policy advisor | |---|---|---| -| Question | Does this policy stay within a limit that I set? | Does this proposal add risky new access? | -| When it runs | When you run the command, for example in CI. | Automatically, on every proposal. | +| Question | Does this policy stay within a maximum policy that I set? | Does this proposed rule add risky new access? | +| When it runs | When an agent or a person runs the command, for example before applying a policy or in CI. | Automatically, each time an agent in a sandbox proposes a new network rule through the policy advisor. | | What it compares | A candidate policy against a boundary policy that you write. | The sandbox's policy with and without the proposed rule. | | What it reports | Access that the candidate allows beyond the boundary. | New access to link-local or cloud metadata addresses, uninspected traffic where a provider credential is available, new credentialed destinations, and new HTTP methods. | @@ -34,13 +40,20 @@ Checks](/sandboxes/policy-advisor#what-the-prover-checks). `openshell-prover check` determines whether everything allowed by the policy you are testing, called the candidate, is also allowed by a boundary policy that you write. If the candidate allows something the boundary does not, the prover -shows an example. Use it to check a policy before you apply it, for example in -CI, so that no change grants more than your organization allows. +shows an example. + +The boundary is usually the most access that an environment allows. For +example, an enterprise can define a maximum policy for all of its agents. A +parent agent that creates a sandbox for a subagent can check that the policy it +proposes for the subagent does not exceed the parent's own maximum allowed +policy. Run the check before you apply a policy, or in CI, so that no change +grants more than the boundary allows. The check covers filesystem access, process identity, Landlock settings, network -destinations, and REST methods and paths. 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. +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 `openshell-prover` command is installed with OpenShell. It is a separate command because it reads policy files on your machine and does not need a @@ -143,40 +156,71 @@ as `unsupported_policy_shape` or `solver_timeout`. Also check that `openshell-prover check --help` for all options, including `--timeout`, which changes the default 10-second time limit. -## What a Passing Result Means +## 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 prover reports `unsupported` instead of guessing when the answer depends on -something it cannot see or does not check: - -- Network rules other than connection rules and REST rules, such as WebSocket, - GraphQL, MCP, and JSON-RPC rules. -- Filesystem comparisons between different paths, such as narrowing `/tmp` to - `/tmp/cache`. A symlink in the sandbox image could make the paths refer to - different locations. Removing a path, or changing it from read-write to - read-only, is supported. -- Binary rules that compare an exact path with a glob, because a symlink in the - sandbox image could change which executable the path refers to. Use matching - exact paths when you can. -- Settings that depend on the sandbox image, such as the working directory. -- Endpoints for the same host and port that set different `allowed_ips`, or - that mix connection-only and REST rules. -- Process identity changes other than changing a non-root user or group to - root. The prover reports a change to root as exceeding the boundary. -- Host, path, method, or binary values that contain non-ASCII characters. - -The prover also handles these cases in specific ways: - -- Changing Landlock from `hard_requirement` to `best_effort` exceeds the - boundary. The prover compares the settings, not what a kernel enforces. -- The prover checks network rules both with and without binary identity - enforcement, because a sandbox runtime can turn it off. -- The prover does not resolve hostnames on your machine. It checks - `allowed_ips` ranges together with hosts, ports, binaries, and REST rules. -- 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`. +The check 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 that could match the same host and port, including wildcard hosts, + set different `allowed_ips`. +- 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`. Other request protocols, such as WebSocket, GraphQL, +MCP, and JSON-RPC, return `unsupported`. From 9b44cab870d4ccd00486822fe2134b82c5b7b71f Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 22:35:16 +0000 Subject: [PATCH 19/40] docs(policy): place prover before advisor and troubleshooting last Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 2 +- docs/sandboxes/troubleshoot-policies.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 9733d14416..4d8f6a11b9 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -5,7 +5,7 @@ title: "Policy Advisor" sidebar-title: "Advisor" description: "Let an agent in a sandbox propose the network rule it needs when OpenShell blocks a request, and review each proposal before it takes effect." keywords: "Generative AI, Cybersecurity, Policy Advisor, Policy, Sandbox, policy.local, Agent Policy" -position: 4 +position: 5 --- The policy advisor lets an agent in a sandbox propose a new network rule when diff --git a/docs/sandboxes/troubleshoot-policies.mdx b/docs/sandboxes/troubleshoot-policies.mdx index 9836fc1953..523fcfb57d 100644 --- a/docs/sandboxes/troubleshoot-policies.mdx +++ b/docs/sandboxes/troubleshoot-policies.mdx @@ -5,7 +5,7 @@ title: "Troubleshoot Sandbox Policies" sidebar-title: "Troubleshooting" description: "Identify policy failure stages and repair connection, request, credential, middleware, and activation problems." keywords: "Generative AI, Cybersecurity, Policy, Troubleshooting, Sandbox, Validation, Credentials, Middleware" -position: 5 +position: 8 --- Policy failures can occur while submitting a change, activating it, or processing From af638bb6b4895647178ff61383358a628c98bac8 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 22:37:37 +0000 Subject: [PATCH 20/40] docs(policy): remove unsupported CI guidance from prover page Signed-off-by: Johnny Greco --- docs/how-it-works/policies/prover.mdx | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index 1b9a3f0e0d..6ec582c928 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -4,7 +4,7 @@ title: "Policy Prover" sidebar-title: "Prover" description: "Understand how OpenShell uses the policy prover, and check that a policy grants no more access than a maximum policy you define." -keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, CI, Subagent" +keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, Subagent" position: 4 --- @@ -22,7 +22,7 @@ that an agent proposes. Each answers a different question: | | `openshell-prover check` | Policy advisor | |---|---|---| | Question | Does this policy stay within a maximum policy that I set? | Does this proposed rule add risky new access? | -| When it runs | When an agent or a person runs the command, for example before applying a policy or in CI. | Automatically, each time an agent in a sandbox proposes a new network rule through the policy advisor. | +| When it runs | When an agent or a person runs the command, for example before applying a policy. | Automatically, each time an agent in a sandbox proposes a new network rule through the policy advisor. | | What it compares | A candidate policy against a boundary policy that you write. | The sandbox's policy with and without the proposed rule. | | What it reports | Access that the candidate allows beyond the boundary. | New access to link-local or cloud metadata addresses, uninspected traffic where a provider credential is available, new credentialed destinations, and new HTTP methods. | @@ -46,8 +46,8 @@ The boundary is usually the most access that an environment allows. For example, an enterprise can define a maximum policy for all of its agents. A parent agent that creates a sandbox for a subagent can check that the policy it proposes for the subagent does not exceed the parent's own maximum allowed -policy. Run the check before you apply a policy, or in CI, so that no change -grants more than the boundary allows. +policy. Run the check before you apply a policy, so that no change grants more +than the boundary allows. The check covers filesystem access, process identity, Landlock settings, network connections, and REST requests. If a policy uses anything else, such as GraphQL @@ -140,8 +140,8 @@ The prover reports one of these results: | `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. In CI, treat every other -result as a failure. For example, a candidate with a GraphQL rule returns: +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 @@ -149,9 +149,9 @@ coverage: domains=filesystem,network_l4,network_rest,process,landlock reason: candidate policy rule 'g' uses protocol 'graphql'; only L4 TCP and REST are modeled ``` -For 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 +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. From 749e57ce19a26bc556c091d23890231299f3a9d4 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 22:48:35 +0000 Subject: [PATCH 21/40] docs(policy): tighten policy prover introduction Signed-off-by: Johnny Greco --- docs/how-it-works/policies/prover.mdx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index 6ec582c928..21b9d1135e 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -8,12 +8,10 @@ keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, Subagent" position: 4 --- -The policy prover is OpenShell's policy verification engine. It translates -policies into logical formulas and uses the Z3 SMT solver to search for a -request that answers a specific question, such as whether any request exists -that one policy allows and another denies. Because the solver searches every -possible request instead of testing a sample, a result covers all requests, not -only the ones you thought to try. +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 uses the prover in two ways: the `openshell-prover check` command, and the [policy advisor](/sandboxes/policy-advisor), which checks every network rule @@ -31,8 +29,9 @@ passing the other. A proposal that adds no risky access can still exceed your boundary, and a policy within your boundary can still contain access that the policy advisor would flag. -This page covers the `openshell-prover check` command. For the policy advisor's -check, refer to [What the Prover +This page covers the `openshell-prover check` command. [What the Boundary Check +Covers](#what-the-boundary-check-covers) describes what its model includes. For +the policy advisor's check, refer to [What the Prover Checks](/sandboxes/policy-advisor#what-the-prover-checks). ## Check a Policy Against a Boundary From 2bfb8f6bb130f5289de27c14d41ad8461c7b456d Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Thu, 24 Sep 2026 22:55:32 +0000 Subject: [PATCH 22/40] docs(policy): move policy change behavior into management guide Signed-off-by: Johnny Greco --- docs/how-it-works/policies/overview.mdx | 44 ++------------- docs/sandboxes/manage-policies.mdx | 71 ++++++++++++++++++------- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/docs/how-it-works/policies/overview.mdx b/docs/how-it-works/policies/overview.mdx index 92e1078daf..0bf210f115 100644 --- a/docs/how-it-works/policies/overview.mdx +++ b/docs/how-it-works/policies/overview.mdx @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 title: "Sandbox Policies" sidebar-title: "Overview" -description: "Understand what sandbox policies control, where a sandbox's policy comes from, and how policy changes take effect." +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: 1 --- @@ -39,7 +39,9 @@ from an API but not writing to it. [Network Rules](/sandboxes/network-rules) explains how OpenShell evaluates rules and gives examples you can adapt. The startup sections are fixed once the sandbox starts. The network sections -can change while it runs. The [Policy Schema Reference](/reference/policy-schema) +can change while it runs, as described in [How Changes Take +Effect](/sandboxes/manage-policies#how-changes-take-effect). The [Policy Schema +Reference](/reference/policy-schema) describes the complete file format, and [Supervisor Middleware](/extensibility/supervisor-middleware) explains how middleware processes traffic. @@ -82,44 +84,6 @@ global policy restores normal policy selection and provider rules. Refer to Policy](/sandboxes/manage-policies#apply-a-gateway-wide-policy) for the commands. -## How Changes Take Effect - -The network sections of a policy can change while a sandbox runs. The startup -sections cannot: - -| 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 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 gateway's `policy_validation_failure_mode` setting 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. [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) -explains how to identify and repair each kind of failure. - ## Next Steps - Use [Network Rules](/sandboxes/network-rules) to learn how OpenShell diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx index 992aad0708..81c0a4d5bf 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/sandboxes/manage-policies.mdx @@ -8,10 +8,10 @@ keywords: "Generative AI, Cybersecurity, Policy, Sandbox, CLI, Hot Reload, Revis position: 3 --- -This page shows how to use the OpenShell CLI to manage a sandbox's policy. Each -section covers one task, 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. +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 @@ -174,20 +174,54 @@ openshell policy set my-sandbox --policy base-policy.json --wait
-OpenShell validates the new policy before it takes effect. Changes to the -filesystem, Landlock, and process sections have extra limits, described in the -next section. +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 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"] +``` -## Change Filesystem and Process Settings +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 gateway's `policy_validation_failure_mode` setting 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. [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) +explains how to identify and repair each kind of failure. -Filesystem, Landlock, and process settings take effect only when the sandbox -starts. After that, OpenShell rejects removed filesystem paths and changes to -workdir, Landlock, or process settings. It accepts added filesystem paths, but -the running sandbox keeps its existing permissions. +## Change Filesystem and Process Settings -To change these settings, save the base policy as described in [Replace the -Complete Policy](#replace-the-complete-policy), edit it, and create a new -sandbox with the file: +Filesystem, Landlock, and process settings take effect only when a sandbox +starts, so changing them requires a new sandbox. To change these settings, save +the base policy as described in [Replace the Complete +Policy](#replace-the-complete-policy), edit it, and create a new sandbox with +the file: Deleting a sandbox stops its processes and removes its state. Copy out anything @@ -199,10 +233,9 @@ openshell sandbox delete my-sandbox openshell sandbox create --name my-sandbox --policy ./policy.yaml ``` -When the policy allows network access, OpenShell adds -the system paths that sandbox processes need, so list only the paths your -workload requires. [Default Policy](/reference/default-policy) lists those -paths. +When the policy allows network access, OpenShell adds the system paths that +sandbox processes need, so list only the paths your workload requires. [Default +Policy](/reference/default-policy) lists those paths. ## Verify a Change From f17c2e22f14477e5e82cdb688161733e42de7428 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 00:27:11 +0000 Subject: [PATCH 23/40] docs(policy): name prover check types and note expanding coverage Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 21 ++++--- docs/how-it-works/policies/prover.mdx | 85 ++++++++++++-------------- 2 files changed, 51 insertions(+), 55 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 4d8f6a11b9..0f90d24a65 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -37,11 +37,11 @@ A proposal goes through these steps: 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 compares what the sandbox could reach with and without the - rule. OpenShell also flags destinations that are often risky, such as + 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 [What the Prover Checks](#what-the-prover-checks). + 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. @@ -142,8 +142,8 @@ approve it. You can also review proposals in the terminal UI with By default, every proposal waits for your review. In automatic mode, OpenShell approves a proposal without review when both of these are true: -- The policy prover finds none of the risks described in [What the Prover - Checks](#what-the-prover-checks). +- 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 private or internal addresses, wildcard hosts, `allowed_ips` entries without a host, ephemeral ports, and well-known database or service ports, and shows each @@ -170,12 +170,13 @@ 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. -### What the Prover Checks +### Proposal Risk Check -The policy advisor uses the same [policy prover](/reference/policy-prover) as -the `openshell-prover` command, but runs a different check. Instead of comparing -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 +The policy advisor uses the [policy prover](/reference/policy-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: diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index 21b9d1135e..e96dd0c12b 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -11,42 +11,38 @@ position: 4 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. +and behavior represented by its model. OpenShell is actively extending the model +to cover more policy features. -OpenShell uses the prover in two ways: the `openshell-prover check` command, and -the [policy advisor](/sandboxes/policy-advisor), which checks every network rule -that an agent proposes. Each answers a different question: +OpenShell uses the prover for two types of checks: -| | `openshell-prover check` | Policy advisor | -|---|---|---| -| Question | Does this policy stay within a maximum policy that I set? | Does this proposed rule add risky new access? | -| When it runs | When an agent or a person runs the command, for example before applying a policy. | Automatically, each time an agent in a sandbox proposes a new network rule through the policy advisor. | -| What it compares | A candidate policy against a boundary policy that you write. | The sandbox's policy with and without the proposed rule. | -| What it reports | Access that the candidate allows beyond the boundary. | New access to link-local or cloud metadata addresses, uninspected traffic where a provider credential is available, new credentialed destinations, and new HTTP methods. | - -Because the two checks answer different questions, passing one does not imply -passing the other. A proposal that adds no risky access can still exceed your -boundary, and a policy within your boundary can still contain access that the -policy advisor would flag. - -This page covers the `openshell-prover check` command. [What the Boundary Check -Covers](#what-the-boundary-check-covers) describes what its model includes. For -the policy advisor's check, refer to [What the Prover -Checks](/sandboxes/policy-advisor#what-the-prover-checks). - -## Check a Policy Against a Boundary - -`openshell-prover check` determines whether everything allowed by the policy you -are testing, called the candidate, is also allowed by a boundary policy that you -write. If the candidate allows something the boundary does not, the prover -shows an example. - -The boundary is usually the most access that an environment allows. For -example, an enterprise can define a maximum policy for all of its agents. A -parent agent that creates a sandbox for a subagent can check that the policy it -proposes for the subagent does not exceed the parent's own maximum allowed -policy. Run the check before you apply a policy, so that no change grants more -than the boundary allows. +- 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](/sandboxes/policy-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](/sandboxes/policy-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 @@ -54,9 +50,8 @@ 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 `openshell-prover` command is installed with OpenShell. It is a separate -command because it reads policy files on your machine and does not need a -gateway. It does not apply or approve policies. +The `openshell-prover` CLI is installed with OpenShell. It 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`: @@ -150,10 +145,10 @@ reason: candidate policy rule 'g' uses protocol 'graphql'; only L4 TCP and REST 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. +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 @@ -162,10 +157,10 @@ 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 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 +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`. From 473c07ff32f93f1b195332d4d239a3c280b9fd9c Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 00:29:15 +0000 Subject: [PATCH 24/40] docs(policy): prefix prover and advisor sidebar labels Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 2 +- docs/how-it-works/policies/prover.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 0f90d24a65..242f9befc5 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Policy Advisor" -sidebar-title: "Advisor" +sidebar-title: "Policy Advisor" description: "Let an agent in a sandbox propose the network rule it needs when OpenShell blocks a request, and review each proposal before it takes effect." keywords: "Generative AI, Cybersecurity, Policy Advisor, Policy, Sandbox, policy.local, Agent Policy" position: 5 diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index e96dd0c12b..073a254c26 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -2,7 +2,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Policy Prover" -sidebar-title: "Prover" +sidebar-title: "Policy Prover" description: "Understand how OpenShell uses the policy prover, and check that a policy grants no more access than a maximum policy you define." keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, Subagent" position: 4 From f4c229da265f642e4b60471e53560621ea664b64 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 00:36:30 +0000 Subject: [PATCH 25/40] docs(policy): streamline policy schema reference Signed-off-by: Johnny Greco --- docs/how-it-works/policies/schema.mdx | 910 +++++++------------------- 1 file changed, 247 insertions(+), 663 deletions(-) diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 63c4ccc504..33b1ed1dbe 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -3,17 +3,16 @@ # SPDX-License-Identifier: Apache-2.0 title: "Policy Schema Reference" sidebar-title: "Schema" -description: "Field reference for the sandbox policy YAML, including defaults, matcher behavior, and validation constraints." +description: "Fields, defaults, and constraints for the sandbox policy schema." keywords: "Generative AI, Cybersecurity, Policy, Schema, YAML, Reference, Security" position: 6 --- -This reference defines every field in the authored YAML and JSON policy format, -with defaults, matcher behavior, and validation constraints. +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 file contains the following top-level fields: +## Top-Level Fields ```yaml showLineNumbers={false} version: 1 @@ -26,137 +25,71 @@ network_middlewares: { ... } | Field | Type | Required | Takes effect | Description | |---|---|---|---|---| -| `version` | integer | Yes | -- | Policy schema version. Must be `1`. | -| `filesystem_policy` | object | No | Startup | Controls which paths the workload can read and write. | -| `landlock` | object | No | Startup | Configures Landlock LSM enforcement behavior. | -| `process` | object | No | Startup | Sets the user and group that the workload runs as. | -| `network_policies` | map | No | Live | Declares which binaries can reach which network endpoints. | -| `network_middlewares` | map | No | Live | Attaches ordered middleware to allowed traffic by destination host. | - -The YAML root and each present top-level section listed as an object or map must -be a mapping. Each named network policy or middleware entry must also be a -mapping. - -Startup fields are installed before the workload process starts. After -activation, OpenShell rejects removals from the filesystem baseline and changes -to workdir inclusion, Landlock mode, and process identity. Additive filesystem -paths may be accepted into stored configuration but do not change the current -workload. Recreate the sandbox for an assured new startup configuration. A -sandbox that has never activated can repair startup fields while configuration -admission is pending or rejected. - -Live fields can activate on a running sandbox after the complete effective -candidate validates. `openshell policy update` edits only `network_policies`. -Use `openshell policy set` with an editable base for middleware changes or -complete replacement. Middleware changes can select built-ins or external -services already registered with the gateway. Adding or changing an external -service registration requires a gateway restart. +| `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. | -## Filesystem Policy +Startup fields take effect when a sandbox starts. Live fields can change while it +runs. Refer to [How Changes Take +Effect](/sandboxes/manage-policies#how-changes-take-effect). -Controls filesystem access inside the sandbox. Paths not listed in either -`read_only` or `read_write` are inaccessible, apart from the baseline paths -described in [Default Policy](/reference/default-policy). +## Filesystem Policy -| Field | Type | Required | Description | +| Field | Type | Default | Description | |---|---|---|---| -| `include_workdir` | bool | No | When `true`, adds the driver-resolved working directory to `read_write`. | -| `read_only` | list of strings | No | Paths the workload can read but not modify. Typically system directories such as `/usr`, `/lib`, and `/etc`. | -| `read_write` | list of strings | No | Paths the workload can read and write. Typically `/tmp`. | - -An absent `filesystem_policy` resolves to `include_workdir: true`. An explicitly -present `filesystem_policy: {}` keeps `include_workdir: false`. +| `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. | -Validation constraints: +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, apart from the baseline paths described in +[Default Policy](/reference/default-policy). -- 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 path must not exceed 4096 characters. -- The combined total of `read_only` and `read_write` paths must not exceed 256. - -Policies that violate these constraints are rejected with `INVALID_ARGUMENT` at -creation or update time. An invalid embedded image policy blocks initial -workload activation for configuration repair. Only a missing image policy -selects the restrictive fallback. - -Example: +Each path must be absolute, must not contain `..`, and must not exceed 4096 +characters. `read_write` cannot contain overly broad paths such as `/`. 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 -Configures [Landlock LSM](https://docs.kernel.org/security/landlock.html) -enforcement for the user filesystem ruleset. Landlock provides mandatory -filesystem access control in the kernel, below what UNIX permissions allow. - -| Field | Type | Required | Values | Description | -|---|---|---|---|---| -| `compatibility` | string | No | `best_effort`, `hard_requirement` | How OpenShell handles Landlock failures. Defaults to `best_effort`. | - -The runtime prepares the user filesystem ruleset as the workload identity. -Because it cannot distinguish an intentionally inaccessible path from a -misconfigured one, both modes skip an individual path that is missing or that -the workload cannot open, and apply the remaining rules. The modes differ when -no rule can be applied: - -| Value | No paths configured | Individual path missing or inaccessible | All paths missing or inaccessible | +| Field | Type | Default | Values | |---|---|---|---| -| `best_effort` | User ruleset skipped. | Skips the path and applies the remaining rules. | Emits a high-severity detection finding and runs without the user ruleset. | -| `hard_requirement` | User ruleset skipped. | Skips the path and applies the remaining rules. | Aborts sandbox startup. | - -A `hard_requirement` ruleset also aborts startup when the kernel cannot apply -it. Use `hard_requirement` when running without the user ruleset is -unacceptable. It does not guarantee that every listed path is enforced. The -`Landlock ruleset built` event in the sandbox log reports how many paths were -applied and skipped. +| `compatibility` | string | `best_effort` | `best_effort` or `hard_requirement` | -On current Linux isolation paths, OpenShell also installs a mandatory -capability-free Landlock baseline that protects `/.openshell` and requires ABI -v3. Sandbox startup fails on a kernel without ABI v3 in either mode. The user -`best_effort` value does not disable that baseline or make an unsupported kernel -compatible. +Both values skip individual paths that are missing or that the workload cannot +open. They differ when no path can be applied or the kernel cannot apply the +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 -Sets the OS-level identity for the workload 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`. | - -An explicit policy value must be `sandbox` or a numeric UID or 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 | User name or UID for the workload. | +| `run_as_group` | string | Driver default | Group name or GID for the workload. | -Example: +A value must be `sandbox` or a numeric ID from `1` through `4294967294`. +OpenShell rejects root. Each field is independent, so you can set one and let +the compute driver choose the other. Docker and Podman default to the image's +`USER`. ```yaml showLineNumbers={false} process: @@ -166,170 +99,66 @@ process: ## Network Policies -A map of named network rules. Each entry declares a set of endpoints and a set -of binaries, and allows each listed binary to reach each listed endpoint. The -map key is the rule's stable identifier and the selector for -`openshell policy update --rule-name`. For how OpenShell evaluates these rules, -with examples, refer to [Network Rules](/sandboxes/network-rules). - -### 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](/sandboxes/network-rules). | Field | Type | Required | Description | |---|---|---|---| -| `name` | string | No | Display name used in log output. Defaults to the map key. | -| `endpoints` | list of endpoint objects | No | Destinations this entry permits. An omitted or empty list grants no destination. | -| `binaries` | list of binary objects | No | Executable selectors associated with the endpoints. In a sandbox, an omitted or empty list matches no binary, so the entry allows nothing. Binaries are ignored only when trusted runtime configuration disables binary identity enforcement. | - -When present, `endpoints` and `binaries` must be lists of objects, even when -they contain only one entry. `null` cannot replace a list or an object entry. +| `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. | ### Endpoint Object -Each endpoint defines a reachable destination, how OpenShell inspects traffic -to it, and how provider credentials apply. The fields fall into four groups. +Endpoint fields fall into four groups. #### Destination Fields | Field | Type | Required | Description | |---|---|---|---| -| `host` | string | Conditional | Hostname or IP address. Required for `protocol: tcp`. | -| `port` | integer | Conditional | One TCP port. Use either `port` or `ports`. | -| `ports` | list of integers | Conditional | One or more TCP ports. Takes precedence over `port` when nonempty. Every endpoint needs at least one effective port. | -| `path` | string | No | HTTP path glob that selects between inspected endpoints sharing a host and port, such as `/repos/**` and `/graphql`. Empty means all paths. | -| `allowed_ips` | list of strings | No | CIDR or IP allowlist for resolved destination addresses. | - -Host wildcards are allowed only inside the first DNS label. `*.example.com`, -`**.example.com`, and intra-label patterns such as `*-aiplatform.googleapis.com` -are accepted. Bare `*` or `**`, top-level-domain wildcards such as `*.com`, and -wildcards outside the first label are rejected at load time. A non-TCP proxy -endpoint may omit `host` only when `allowed_ips` supplies the destination -constraint. - -When several inspected endpoints share a host and port, the endpoint whose -`path` most specifically matches the request selects the parser and request -rules. A request that matches no endpoint path is denied. `path` is not valid -with `protocol: tcp`. On an endpoint without `protocol`, it does not restrict -access and narrows only which requests receive provider credentials. - -`allowed_ips` controls server-side request forgery (SSRF) protection. Exact -user-declared hostname endpoints may resolve to RFC 1918 private addresses -without this field. Wildcard endpoints, hostless endpoints, and endpoints added -through the policy advisor still require `allowed_ips` for private resolved -addresses. When an endpoint -sets `allowed_ips`, every resolved address must fall within the list, including -public addresses. A hostless allowlist is valid only on the explicit proxy path, -matches any hostname on the port, 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`) addresses are rejected at load time. Those destinations, -including cloud metadata addresses, are always blocked. +| `host` | string | Conditional | Hostname or IP address. A `*` or `**` wildcard is allowed only in the first DNS label, such as `*.example.com`. | +| `port` | integer | Conditional | TCP port. Set `port` or `ports`. | +| `ports` | list of integers | Conditional | TCP ports. Takes precedence over `port`. | +| `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. | + +An endpoint can omit `host` only when it sets `allowed_ips`. Exact hostnames can +reach the private addresses they resolve to. Wildcard and hostless endpoints can +reach private addresses only through `allowed_ips`. Loopback, link-local, and +unspecified addresses, including cloud metadata addresses, are always blocked, +and `allowed_ips` entries that overlap them are rejected. #### Inspection Fields -| Field | Type | Required | Description | +| Field | Type | Default | Description | |---|---|---|---| -| `protocol` | string | No | `rest`, `websocket`, `graphql`, `mcp`, or `json-rpc` for request inspection. `tcp` for native TCP without inspection. Omit for no protocol-specific request rules. | -| `tls` | string | No | Omit for automatic TLS handling. `skip` disables TLS detection and termination for the endpoint. | -| `enforcement` | string | No | `enforce` blocks requests that violate the endpoint's rules. `audit` logs them and forwards the request. Defaults to `audit`. | -| `access` | string | No | Access preset: `read-only`, `read-write`, or `full`. Mutually exclusive with `rules`. Refer to [Access Presets](#access-presets). | -| `rules` | list of allow rule objects | No | Protocol-specific allow rules. Mutually exclusive with `access`. | -| `deny_rules` | list of deny rule objects | No | Protocol-specific deny rules. A matching deny takes precedence over any allow. | -| `allow_encoded_slash` | bool | No | When `true`, request parsing preserves `%2F` inside path segments instead of rejecting it. Use it for APIs such as npm scoped packages (`/@scope%2Fname`). Defaults to `false`. | - -`protocol: tcp` provides ordinary DNS resolution and a native TCP socket through -policy DNS and transparent capture. When `protocol` is omitted, OpenShell -defines no protocol-specific request rules, but the explicit proxy's default TLS -detection and HTTP destination checks still apply. A `websocket` endpoint can -also use GraphQL operation rules for GraphQL-over-WebSocket traffic. -Provider-credentialed endpoints require an inspected protocol unless -`allow_uninspected_credentials` is set. - -The proxy detects TLS by peeking at the first bytes of each connection and -terminates it for inspected HTTPS traffic, so most endpoints omit `tls`. Set -`tls: skip` for cases such as client-certificate mTLS or nonstandard protocols. -Provider-credentialed endpoints reject `tls: skip` unless -`allow_uninspected_credentials` is set, and OpenShell never injects credentials -into a skipped tunnel. Use `tls: skip` only on endpoints that omit `protocol`, -because OpenShell copies tunneled bytes without evaluating request rules. All -endpoints that overlap on the same host and port must agree on `tls` and on -`allowed_ips`. `skip` is the only accepted non-empty value. Every other value, -including the removed `terminate` and `passthrough` spellings, is rejected -before persistence or activation. - -Other `enforcement` values are rejected before activation. `audit` does not -bypass parsing, destination, credential, or middleware checks, and -`enforcement` is not valid with `protocol: tcp`. - -Other `access` values are rejected before activation. `access` is 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. +| `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](/sandboxes/network-rules#connection-and-request-checks). | +| `tls` | string | Automatic | `skip` relays traffic without terminating TLS. Use it only on endpoints without `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 -| Field | Type | Required | Description | +| Field | Type | Default | Description | |---|---|---|---| -| `credential_binding` | object | No | Binds static credentials from an attached provider whose profile defines no endpoints. Valid only in a sandbox-scoped policy. | -| `credential_binding.provider` | string | With `credential_binding` | Exact name of the provider instance attached to the sandbox. | -| `request_body_credential_rewrite` | bool | No | Rewrites credential placeholders in supported REST request bodies. Defaults to `false`. Mutually exclusive with `credential_signing`. | -| `websocket_credential_rewrite` | bool | No | Rewrites credential placeholders in client WebSocket text messages on `rest` or `websocket` endpoints. Defaults to `false`. | -| `allow_uninspected_credentials` | bool | No | Security-sensitive opt-in that lets a provider-credentialed endpoint omit request inspection or use `tls: skip`. Defaults to `false`. | -| `credential_signing` | string | No | Proxy-side request signing: `sigv4`, `sigv4:body`, or `sigv4:no_body`. Mutually exclusive with `request_body_credential_rewrite`. | -| `signing_service` | string | With `credential_signing` | AWS service name for SigV4 signing, such as `bedrock`, `s3`, or `sts`. | -| `signing_region` | string | No | AWS region override for SigV4 signing, such as `us-east-1`. | - -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). - -`request_body_credential_rewrite` applies to UTF-8 `application/json`, -`application/x-www-form-urlencoded`, and `text/*` request bodies on -`protocol: rest` endpoints. 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. - -`websocket_credential_rewrite` applies to 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. - -Policy proposals that set `allow_uninspected_credentials` require explicit -security-flagged approval. - -With `credential_signing`, the proxy strips the sandbox client's -`Authorization` header and re-signs the request with real provider credentials. -`sigv4` detects the payload mode from client headers, `sigv4:body` buffers and -hashes the body up to 10 MiB, and `sigv4:no_body` streams an unsigned payload. -When `signing_region` is omitted, OpenShell extracts the region from the -endpoint hostname. Set it for nonstandard AWS endpoints where the region cannot -be inferred. 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`. Refer to [AWS SigV4](/providers/aws-sigv4). - -This example allows the sandbox to reach Google Cloud Storage and binds the -static credentials from the attached `work-gcp` provider to that endpoint: +| `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](/providers/aws-sigv4) and [Static +Credential Endpoint +Binding](/providers/profiles#understand-static-credential-endpoint-binding). ```yaml showLineNumbers={false} network_policies: @@ -345,81 +174,52 @@ network_policies: #### Protocol Options -| Field | Type | Required | Description | +| Field | Type | Default | Description | |---|---|---|---| -| `persisted_queries` | string | No | GraphQL hash-only query behavior for `protocol: graphql` and GraphQL-over-WebSocket. `deny` (default) or `allow_registered`. | -| `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 options. Valid only with `protocol: mcp`. Omit the key to use all defaults. `mcp: null` is invalid. | -| `mcp.versions` | list of strings | No | Nonempty allowlist of supported MCP revisions. Omission allows only `2025-11-25`. Refer to [MCP Version Selection](#mcp-version-selection). | -| `mcp.max_body_bytes` | integer | No | Maximum MCP request body bytes buffered for inspection. Defaults to `65536`. | -| `mcp.strict_tool_names` | bool | No | Requires `tools/call` `params.name` values to match `^[A-Za-z0-9_.-]{1,128}$`. Defaults to `true`. | -| `mcp.allow_all_known_mcp_methods` | bool | No | Enables the endpoint MCP method profile. Defaults to `false`. Refer to [MCP Rules](#mcp-rules). | -| `json_rpc` | object | No | JSON-RPC options for `protocol: json-rpc`. | -| `json_rpc.max_body_bytes` | integer | No | Maximum JSON-RPC-over-HTTP request body bytes buffered for inspection. Defaults to `65536`. | - -Use `persisted_queries: allow_registered` only with a -`graphql_persisted_queries` registry. Set `mcp.strict_tool_names: false` only -for compatibility with MCP servers that intentionally use non-recommended tool -names. Wildcard `tool` matchers require it to remain enabled. - -#### Endpoint Validation - -OpenShell rejects an endpoint that breaks any of these rules: - -- `access` and `rules` are mutually exclusive. -- For `rest`, `websocket`, and `graphql`, at least one of `access` or `rules` is - required. On an endpoint without `protocol`, `access` and `rules` have no - effect. -- `mcp` and `json-rpc` reject `access` presets. `json-rpc` requires explicit - `rules` with `allow.method`. `mcp` requires `rules` unless - `mcp.allow_all_known_mcp_methods: true`. -- `deny_rules` require `protocol`. For protocols other than MCP, `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: []` 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: []` is rejected. Remove it if no denials are needed. -- When present, `rules` and `deny_rules` must be lists of objects. - -`protocol: tcp` has additional constraints: - -- It requires a valid DNS hostname. Hostless `allowed_ips`, IP-literal hosts, - trailing-dot names, and malformed DNS selectors are rejected. -- It requires at least one port and rejects request-level fields, including - `path`, `enforcement`, `access`, `rules`, `deny_rules`, request rewriting and - credential signing fields, and GraphQL, JSON-RPC, or MCP options. -- Its 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. -- Prefer exact hosts. A wildcard authorizes DNS queries for all matching names - and can provide a DNS-label exfiltration channel. -- The sandbox runtime must support policy DNS and transparent TCP capture before - it can activate a TCP endpoint. Docker and Podman provide this support. The - standard runtime initializes it unconditionally, so a running sandbox can add - its first TCP endpoint through a live update. +| `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. Required with `allow_registered`. | +| `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`, an endpoint without `rules` allows all MCP methods and tools except those that deny rules match, and 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. +- `rules` and `deny_rules` cannot be empty lists, and `rules` must contain at + least one allow rule. +- `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 that share a host and port must use the same `tls` and + `allowed_ips` values. + +Without `protocol`, `access` and `rules` have no effect. ### Access Presets -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 and path presets cannot authorize JSON-RPC safely. +REST, WebSocket, and GraphQL endpoints accept these presets. MCP and JSON-RPC +endpoints do not. -| Value | REST expansion | WebSocket expansion | GraphQL expansion | +| Value | REST | WebSocket | GraphQL | |---|---|---|---| -| `full` | All methods and paths. | WebSocket upgrade and all inspected client text-message paths. | All operation types. | -| `read-only` | `GET`, `HEAD`, `OPTIONS`. | WebSocket upgrade handshake only. | `query` operations. | -| `read-write` | `GET`, `HEAD`, `OPTIONS`, `POST`, `PUT`, `PATCH`. | WebSocket upgrade handshake and client text messages. | `query` and `mutation` operations. | +| `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. | ### Allow and Deny Rules -Each entry in `rules` wraps its matcher fields in an `allow` object. Each entry -in `deny_rules` contains the same matcher fields directly, without a wrapper. -Deny rules take precedence: a request that matches any deny rule is blocked -regardless of what the allow rules or access preset permit. +Each entry in `rules` wraps its matcher fields in `allow`. Each entry in +`deny_rules` lists the matcher fields directly. A request that matches any deny +rule is blocked, regardless of the allow rules or access preset. ```yaml showLineNumbers={false} rules: @@ -431,328 +231,186 @@ deny_rules: path: /repos/private/** ``` -The matcher fields depend on the endpoint's `protocol`, as described in the -following sections. +The matcher fields depend on the endpoint's `protocol`. ### REST Rules -REST rules match HTTP requests by method, path, and optional query parameters. - | Field | Type | Required | Description | |---|---|---|---| -| `method` | string | Yes | HTTP method, such as `GET` or `POST`. `*` matches any method. | -| `path` | string | Yes | URL path glob. `*` matches zero or more characters within one path segment, and `**` matches zero or more characters across segments. `?` matches one character. Bracket classes such as `[0-9]` and `[!0]` are supported. | -| `query` | map | No | Query parameter matchers keyed by decoded, case-sensitive name. A matcher is 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] }`. | -In an allow rule, every duplicate value for a configured query key must match. -In a deny rule, every configured key must be present with at least one matching -value, and an additional nonmatching duplicate does not cancel the match. +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} -endpoints: - - host: api.github.com - port: 443 - protocol: rest - enforcement: enforce - rules: - - allow: - method: GET - path: /**/info/refs* - query: - service: "git-*" - - allow: - method: POST - path: /**/git-upload-pack - query: - tag: - any: ["v1.*", "v2.*"] - deny_rules: - - method: POST - path: "/repos/*/*/pulls/*/reviews" - - method: "*" - path: "/repos/*/*/rulesets" +rules: + - allow: + method: GET + path: /api/v1/download + query: + version: + any: ["1.*", "2.*"] +deny_rules: + - method: "*" + path: "/repos/*/*/rulesets" ``` ### WebSocket Rules -WebSocket 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 -inspected or rewritten. - | Field | Type | Required | Description | |---|---|---|---| -| `method` | string | Yes | `GET` matches the upgrade handshake, `WEBSOCKET_TEXT` matches client text messages after the upgrade, and `*` matches both. | -| `path` | string | Yes | URL path glob from the original upgrade request, not message content. Same syntax as REST rules. | -| `query` | map | No | Query parameter matchers from the original upgrade request. Same syntax as REST 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. | + +OpenShell does not inspect binary frames or messages from the server. ```yaml showLineNumbers={false} -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: WEBSOCKET_TEXT - path: "/v1/admin/**" +rules: + - allow: + method: GET + path: /v1/realtime/** + - allow: + method: WEBSOCKET_TEXT + path: /v1/realtime/** +deny_rules: + - method: WEBSOCKET_TEXT + path: /v1/admin/** ``` ### GraphQL Rules -GraphQL 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. - | Field | Type | Required | Description | |---|---|---|---| | `operation_type` | string | Yes | `query`, `mutation`, `subscription`, or `*`. | -| `operation_name` | string | No | Operation-name glob. Omit to match any operation name. | -| `fields` | list of strings | No | Root-field globs. Omit to match all root fields. | +| `operation_name` | string | No | Operation name glob. | +| `fields` | list of strings | No | Top-level field globs. | -In an allow rule, every selected root field must match one configured glob. In -a deny rule, any matching root field blocks the request, and omitting `fields` -denies every operation that matches `operation_type` and `operation_name`. A -malformed, denied, or unregistered operation denies an entire batched HTTP -request. Hash-only persisted queries are denied unless the endpoint sets -`persisted_queries: allow_registered` with a trusted registry. - -```yaml showLineNumbers={false} -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] -``` - -For GraphQL-over-WebSocket, use `protocol: websocket`, add a separate `GET` -allow rule for the upgrade, and use GraphQL rules for client operation messages. -OpenShell classifies `graphql-transport-ws` `subscribe` messages and legacy -`graphql-ws` `start` messages as operations. Lifecycle messages such as -`connection_init`, `ping`, `pong`, and `complete` are allowed as control-plane -messages and are not payload-logged. Do not combine `method`, `path`, or `query` -with `operation_type`, `operation_name`, or `fields` in the same rule. When a -WebSocket endpoint has GraphQL operation policy, use GraphQL rules for client -messages instead of a raw `WEBSOCKET_TEXT` allow rule. +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: - method: GET - path: /graphql - - allow: - operation_type: subscription - fields: [messageAdded] - allow: operation_type: query - fields: [viewer] + - allow: + operation_type: mutation + fields: [createIssue] +deny_rules: + - operation_type: mutation + fields: [deleteRepository] ``` -### MCP 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 match sandbox-to-server MCP Streamable HTTP request bodies by MCP -method and optional tool selectors. +### MCP Rules | Field | Type | Required | Description | |---|---|---|---| -| `method` | string | Conditional | MCP method name, such as `initialize`, `tools/list`, `tools/call`, or an unknown extension method. Globs are accepted only for the `tools/` family, such as `tools/*`. Required unless `mcp.allow_all_known_mcp_methods` is `true`. Do not use `method: "*"`. | -| `tool` | string or matcher | No | Matcher for `tools/call` `params.name`. A glob string or `{ any: [...] }`. Omit to match every tool. | -| `params` | map | No | Lower-level matcher. MCP currently accepts only `params.name`. An explicit `params: null` is invalid. | - -OpenShell applies MCP rules with this behavior: - -- It parses the underlying JSON-RPC 2.0 envelope, validates known MCP request and - notification params, and preserves unknown extension methods as literal method - strings that rules can match. -- By default, explicit method rules are required, and rules with `tool` or - `params.name` must set `method: tools/call`. -- With `mcp.allow_all_known_mcp_methods: true`, rules can omit `method`, and tool - selectors are normalized to `tools/call`. If the endpoint also 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. -- By default, `tools/call` `params.name` must match the MCP-recommended tool-name - pattern. Wildcard `tool` matchers require `mcp.strict_tool_names` to remain - enabled. -- Tool argument matching is not supported. An allowed tool accepts any argument - payload. -- In a batch request, one denied call denies the full batch. -- JSON-RPC responses and server-to-client MCP messages on response bodies or SSE - streams are relayed but are not parsed for policy enforcement. - -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. Responses need no rules because MCP rules inspect only -client-to-server messages. This example allows both initialization messages and -selected tools. It omits `tools/list` because it assumes the client already -knows the tool names. Add that method when the client performs discovery: +| `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} -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 +rules: + - allow: + method: initialize + - allow: + method: notifications/initialized + - allow: + method: tools/call + tool: + any: [search_web, list_issues] +deny_rules: + - method: tools/call + tool: send_email ``` -Every MCP endpoint must 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 Version Selection -`mcp.versions` accepts exact revisions from the closed supported set: -`2025-03-26`, `2025-06-18`, and `2025-11-25`. Omit the key to allow only -`2025-11-25`. Omission never means the latest revision or all known revisions. -`versions: null`, `versions: []`, duplicate values, values with extra -whitespace, and revisions outside the supported set are invalid. At protobuf -ingress, an empty repeated `versions` field means omission and resolves to -`["2025-11-25"]`, because protobuf repeated fields do not preserve presence. - -An endpoint that omits `mcp.versions`, including one that omits the entire `mcp` -object, resolves immediately to the exact `2025-11-25` allowlist. Canonical -serialization and stored policy data contain that explicit list, so support for -a later revision cannot widen a normalized policy. OpenShell canonicalizes an -explicit list in semantic order. 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"] ``` -Except for a valid standalone `initialize` request, OpenShell selects the -revision from one `MCP-Protocol-Version` header before policy evaluation, and -repeats the check after middleware changes the request. When the header is -absent, it uses the MCP compatibility fallback `2025-03-26`, not the policy -default. A duplicate, empty, or unsupported header value returns -`400 Bad Request`. A supported revision that is not in the endpoint allowlist -returns `403 Forbidden`. This is a stateless per-request header check. OpenShell -does not negotiate a revision, bind revision-specific session state, or apply -the batch rules recorded in each revision's wire profile. - -The sessionless `2026-07-28` revision is not yet supported. If a client requires -an unsupported revision, omit both `protocol` and `mcp` to forgo MCP-specific -request enforcement while retaining the explicit proxy's default TLS handling -and HTTP destination checks. Use `protocol: tcp` or `tls: skip` only when the -client requires a raw stream and the weaker boundary is acceptable. +OpenShell reads the revision from each request's `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. ### JSON-RPC Rules -JSON-RPC rules match sandbox-to-server JSON-RPC-over-HTTP request objects by -method. They apply to single requests and batch requests. - | Field | Type | Required | Description | |---|---|---|---| -| `method` | string | Yes | Exact JSON-RPC method name, such as `reports.search`. `*` is accepted only as the all-methods sentinel. Other globs are rejected. | +| `method` | string | Yes | Exact method name, or `*` for all methods. Other globs are rejected. | -JSON-RPC rules do not support `params` matchers. For a batch, OpenShell -evaluates each call independently and denies the full batch if one call is -denied. Client-to-server JSON-RPC response frames in POST bodies are denied. -Server-to-client messages on HTTP response bodies are relayed but are not parsed -for policy enforcement. Use MCP `tool` rules for MCP tool calls. +Parameters are not matched. One denied call denies an entire batched request. ```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: reports.list - - allow: - method: reports.search - deny_rules: - - method: reports.delete +rules: + - allow: + method: reports.search +deny_rules: + - method: reports.delete ``` ### Binary Object -Identifies an executable that can use the associated endpoints. - | Field | Type | Required | Description | |---|---|---|---| -| `path` | string | Yes | Canonical filesystem path selector. `*` matches within one path segment and `**` crosses directory boundaries. For example, `/sandbox/.vscode-server/**` matches executables below that directory tree. | - -OpenShell identifies a process by the executable the kernel reports for it, -never by its command line. A script therefore runs as its interpreter, so a rule -for a `pip` script must list the Python interpreter rather than the script path. -OpenShell resolves symlinks in exact policy paths to their canonical targets, so -symlink spelling does not bypass canonical path checks. Glob selectors are not -symlink-resolved and must match the canonical path. - -A binary selector also matches when it names an ancestor of the calling process, -so processes that a listed binary starts can use the rule. Trusted runtime -configuration can disable binary identity enforcement, so inspect the active -runtime mode when diagnosing an unexpected result. - -With identity enforcement enabled, OpenShell pins each executable or ancestor -path used for authorization to its first observed SHA256 digest. A changed -digest, missing evidence, or conflicting evidence denies access. Command-line -paths do not authorize access. Restart the sandbox runtime after intentionally -replacing an executable at an authorized path so it can establish a new pin. +| `path` | string | Yes | Executable path or glob, such as `/usr/bin/curl` or `/sandbox/.venv/bin/*`. | + +A binary matches the executable that opens the connection or any of its parent +processes. Scripts run as their interpreter, so list `/usr/bin/python3` for a +Python script. OpenShell resolves symlinks in exact paths but not in globs. +OpenShell records each executable's checksum when a rule first allows it, and +denies later connections if the file changes. Refer to [Binary +Matching](/sandboxes/network-rules#binary-matching). ## Network Middleware -A map of up to 10 middleware configs selected after network and request policy -admit an HTTP request or WebSocket upgrade. Request middleware runs before -provider credential injection. Response middleware that advertises -`HTTP_RESPONSE/PRE_RETURN` runs on the final upstream response before return. -WebSocket-capable bindings continue on client text messages after the upgrade. -Selection is independent of the admitting network rule, and host matching alone -does not select an operation the implementation did not advertise. Each matching -config runs once by ascending `order`, and order values must be unique. +A map of up to 10 middleware configurations. Middleware runs on traffic that +network rules allow, in ascending `order`. + +| Field | Type | Required | Description | +|---|---|---|---| +| `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 use the same wildcards 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: @@ -763,62 +421,27 @@ 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`, so policies with multiple configs normally set 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 a required non-empty `include` list and an optional `exclude` list, 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 then bypasses the -middleware and emits a detection finding. - -Refer to [Supervisor Middleware](/extensibility/supervisor-middleware) for -registration, failure behavior, body limits, and operational guidance. - ## Matcher Semantics -Different policy fields use different wildcard boundaries: - -| Matcher | Comparison | Behavior | +| Matcher | Case-sensitive | Wildcards | |---|---|---| -| Endpoint `host` | Case-insensitive DNS name or IP comparison. | DNS `*` matches one label and `**` matches one or more labels. Validation restricts wildcard placement. | -| Binary `path` | Canonical executable or trusted ancestor path. | Symlinks resolve to canonical identity. `*` matches within a path segment and `**` crosses directories. Identity enforcement depends on trusted runtime configuration. | -| REST or WebSocket request `path` | Case-sensitive URL path glob. | `*` matches within one path segment and `**` crosses `/`. `/repos/**` does not match `/repos` itself. | -| Query value | Case-sensitive decoded value glob. | In allow rules, every duplicate value for a configured key must match. | -| Middleware `endpoints` | Case-insensitive DNS name comparison. | Same as endpoint `host`. Brace alternates are rejected. | - -For a deny rule with query conditions, every configured key must be present and -each key must have at least one matching value. A duplicate nonmatching value -does not cancel another value that matches the deny. +| Endpoint `host` and middleware hosts | No | `*` matches one DNS label, and `**` matches one or more labels. | +| Binary `path` | Yes | `*` matches within one path segment, and `**` matches across segments. | +| REST and WebSocket `path` | Yes | `*` matches within one path segment, and `**` matches across segments. `?` matches one character, and bracket classes such as `[0-9]` are supported. `/repos/**` does not match `/repos`. | +| Query values | Yes | `*` matches any characters. | ## Full Example -The following complete policy file grants enforced read-only GitHub API 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 @@ -826,11 +449,8 @@ 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 @@ -841,39 +461,3 @@ network_policies: binaries: - path: /usr/bin/node ``` - -## Parsing and Serialization - -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 own models. - -The parser requires `version: 1`, rejects duplicate mapping keys and YAML merge -keys, and accepts one document of at most 4 MiB. It also enforces these limits: - -- Nesting depth of 64 levels. -- 100,000 total AST nodes and 300,000 parser events. -- 4 MiB of cumulative scalar data. -- 100 alias expansions, with a 5:1 alias-to-anchor ratio. -- 10,000 entries in each mapping or sequence. - -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. - -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`, and any other unsupported -protobuf version is rejected. A protobuf port above 65535 is rejected rather -than clamped. - -The YAML representation keeps the field names shown on this page. Protobuf and -generated SDK clients use the `NetworkTlsMode`, `NetworkEnforcementMode`, and -`NetworkAccessPreset` enums for the `tls`, `enforcement`, and `access` fields. -Unspecified TLS keeps automatic handling, unspecified enforcement keeps the -audit default, and unspecified access selects no preset. Unknown enum numbers -are rejected before activation. From bae7fb8c19259b47a9c184effa598a83bbfe94b4 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 00:53:57 +0000 Subject: [PATCH 26/40] docs(policy): place default policy before schema reference Signed-off-by: Johnny Greco --- docs/how-it-works/policies/default-policy.mdx | 2 +- docs/how-it-works/policies/schema.mdx | 4 +- docs/sandboxes/troubleshoot-policies.mdx | 267 ------------------ 3 files changed, 3 insertions(+), 270 deletions(-) delete mode 100644 docs/sandboxes/troubleshoot-policies.mdx diff --git a/docs/how-it-works/policies/default-policy.mdx b/docs/how-it-works/policies/default-policy.mdx index 45e93cd036..1d37eb53fa 100644 --- a/docs/how-it-works/policies/default-policy.mdx +++ b/docs/how-it-works/policies/default-policy.mdx @@ -5,7 +5,7 @@ title: "Default Policy and Baseline Paths" sidebar-title: "Default Policy" 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: 7 +position: 6 --- This reference describes the restrictive policy OpenShell uses when a sandbox has diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 33b1ed1dbe..13db285ced 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -2,10 +2,10 @@ # 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" +sidebar-title: "Schema Reference" description: "Fields, defaults, and constraints for the sandbox policy schema." keywords: "Generative AI, Cybersecurity, Policy, Schema, YAML, Reference, Security" -position: 6 +position: 7 --- This reference defines the sandbox policy schema. A policy is a YAML file of at diff --git a/docs/sandboxes/troubleshoot-policies.mdx b/docs/sandboxes/troubleshoot-policies.mdx deleted file mode 100644 index 523fcfb57d..0000000000 --- a/docs/sandboxes/troubleshoot-policies.mdx +++ /dev/null @@ -1,267 +0,0 @@ ---- -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 -title: "Troubleshoot Sandbox Policies" -sidebar-title: "Troubleshooting" -description: "Identify policy failure stages and repair connection, request, credential, middleware, and activation problems." -keywords: "Generative AI, Cybersecurity, Policy, Troubleshooting, Sandbox, Validation, Credentials, Middleware" -position: 8 ---- - -Policy failures can occur while submitting a change, activating it, or processing -traffic. The sandbox's state, policy history, and logs help identify which stage -failed and whether the previous policy is still active. Start with that evidence -before changing permissions. - -## Identify the Failure Stage - -The following OpenShell CLI commands show the information needed to locate a -failure: - -```shell -openshell sandbox get my-sandbox -openshell policy list my-sandbox -openshell policy get my-sandbox --full -openshell logs my-sandbox --since 10m --source sandbox -``` - -Use what you observe and the latest revision status to locate the failure. - -| What you see | Failure stage | Existing access | What to do | -|---|---|---|---| -| The client receives `policy_denied`, a credential error, or a middleware error. | Connection, request, credential, or middleware enforcement | The active network configuration remains. | Diagnose that control instead of widening the endpoint. | -| The CLI reports an invalid file or argument before showing a revision. | Local command parsing | Unchanged. | Fix the file or arguments. | -| The CLI reports that the candidate was rejected and no revision was created. | Gateway validation | Unchanged. | Fix the reported schema, scope, provider, credential, or compatibility error. | -| `--wait` times out. | Status is not yet known. | Do not infer it from the timeout. | Inspect the revision and readiness before retrying. | -| A revision fails and new egress stops. | Runtime activation with `fail_closed` | No. Existing connections close. | [Submit a valid repair](#repair-a-runtime-rejection) and verify recovery. | -| A revision fails but earlier network access still works. | Runtime activation with `retain_last_valid` | The last valid configuration remains active. | [Repair the candidate](#repair-a-runtime-rejection). Working access does not mean the change loaded. | -| The sandbox remains `Provisioning` with `ConfigurationInvalid`. | Initial runtime activation | No workload has activated. | [Repair the configuration](#repair-initial-configuration) during the repair window. | - -OCSF policy events are INFO-level log records, regardless of their event -severity. A `--level warn` filter on `openshell logs` excludes them, so omit the -level filter when you look for policy decisions. - -## Diagnose Connection Denials - -A connection denial occurs before application-request inspection. Check these -dimensions in the effective policy: - -- Destination hostname and port. -- Calling executable or trusted ancestor. -- Destination IP and SSRF restrictions. -- Whether the selected runtime supports policy DNS and transparent TCP capture - when `protocol: tcp` is used. -- Whether a gateway-global policy replaced the sandbox policy. - -Use canonical executable paths. Resolve symlinks inside the sandbox when the -logged path differs from the policy: - -```shell -openshell sandbox exec -n my-sandbox --no-login-shell -- \ - readlink -f /usr/bin/curl -``` - -OpenShell identifies the process by the executable the kernel reports, so a -script such as `pip` or `npm` appears as its interpreter, such as -`/usr/bin/python3.12` or `/usr/bin/node`. Binary matching can also admit a -trusted ancestor. An empty binary list matches no binary unless trusted runtime -configuration disables identity enforcement. Compare the logged binary with the -policy's selectors before widening a rule. - -If the error reports missing TCP support or connection-handling setup, the -selected runtime is not ready to enforce native TCP rules. Changing the policy -cannot supply that runtime support. - -## Diagnose Request Denials - -When the connection succeeds but OpenShell returns `policy_denied`, inspect the -protocol, enforcement mode, method or operation, request path, query values, and -endpoint path selector. - -For HTTP, the request authority must agree with the policy destination. A -non-default port in an absolute-form URL or `Host` authority must match the -transport destination. OpenShell rejects a mismatch with -`request_authority_mismatch`, even when the endpoint otherwise permits the -method and path. For a tunnel to `api.example.com:8443`, send -`Host: api.example.com:8443`. - -Check that rules intended to block requests use `enforcement: enforce`. -Omitted enforcement is audit mode. Audit forwards an applicable request-rule -violation after logging it, but it does not bypass parsing, destination, -credentials, middleware, or other independent checks. - -Overlapping rules can contribute permissions. An applicable request deny wins -over an allow, while the most-specific compatible endpoint selects the parser -and request-processing metadata. Inspect all matching rules rather than only the -YAML block you most recently edited. - -## Diagnose Credential Denials - -Network permission and credential permission are separate. A provider -credential is usable only within its attached profile or explicit binding. - -For `credential_endpoint_mismatch`, inspect the provider profile and the request -host, port, and path: - -```shell -openshell profile export -o yaml -openshell provider get -``` - -Update a custom profile only when the destination is an intended credential -recipient. Do not widen sandbox policy to hide a binding mismatch. Refer to -[Static Credential Endpoint Binding](/providers/profiles#understand-static-credential-endpoint-binding). - -If a request body contains an invalid or revoked credential placeholder, -OpenShell can return `credential_placeholder_in_request_body`. Remove stale -placeholder text or restore the provider metadata. Do not enable body rewriting -or `allow_uninspected_credentials` merely to forward unrelated conversation -text. - -## Diagnose Middleware Denials - -Middleware runs after network rules allow the traffic. It must match the -destination host and support the operation being inspected. A deny decision or -a `fail_closed` stage failure blocks the request even when the endpoint uses -audit mode. - -If a policy names an unregistered external service, register it in the gateway -configuration and restart the gateway before using it in a policy. Built-ins and -already registered services can be added to a running sandbox through `policy set`. - -Inspect the policy-local middleware config, gateway registration, advertised -operation and phase, host selector, order, payload limits, and `on_error` value. -Request middleware runs before provider credential injection. Response -middleware runs on the final upstream response before return. WebSocket support -covers selected client text messages, not binary or upstream-to-client frames. - -Refer to [Supervisor Middleware](/extensibility/supervisor-middleware) for -registration, transport, mutation, limit, and observability details. - -## Check the Client Environment - -A missing executable or TLS trust store can cause a request to fail before -OpenShell evaluates the intended operation. Confirm the sandbox is running and -contains the required client. For a sandbox using curl with the standard Ubuntu -certificate bundle, these commands check the executable and trust store: - -```shell -openshell sandbox exec -n my-sandbox --no-login-shell -- test -x /usr/bin/curl -openshell sandbox exec -n my-sandbox --no-login-shell -- \ - test -r /etc/ssl/certs/ca-certificates.crt -``` - -Compare the client error with a fresh event from -`openshell logs my-sandbox --since 5m --source sandbox`. - -## Repair Initial Configuration - -Before first workload activation, an invalid effective policy or provider bundle -keeps the sandbox in `Provisioning` with a `ConfigurationInvalid` condition. The -gateway gives you a 300-second repair window. An effective policy, settings, -provider, profile, or attachment change resets the window from its stored change -time, and the first rejection for that change receives a full window. Repeated -failures do not extend it. - -Inspect the diagnostic: - -```shell -openshell sandbox get my-sandbox --output json -``` - -Repair the complete policy or the conflicting provider configuration. A -sandbox that has never started its workload can also replace startup settings -during the repair window. An invalid image policy is repaired the same way, -because OpenShell does not fall back to the default policy. - -If the window expires, the sandbox enters `Error` with reason -`ProvisioningTimedOut`. The gateway reclaims workload and supervisor compute but -retains the sandbox record and diagnostic. Repair the configuration, wait for -cleanup, then start it explicitly: - -```shell -openshell sandbox get my-sandbox --output json -openshell sandbox start my-sandbox -``` - -A start retry receives a new 300-second window. Editing configuration after -timeout does not restart compute by itself. - -## Repair a Runtime Rejection - -The gateway checks a proposed policy before saving it. The runtime checks the -resulting configuration again when the sandbox starts or reloads it, because -the runtime also sees concurrent changes and other policy sources. The gateway -setting `[openshell.gateway] policy_validation_failure_mode` selects what -happens when the runtime rejects a candidate. Refer to -[Gateway Configuration](/reference/gateway-config) for its deployment contract. - -The default failure mode is `fail_closed`. An invalid candidate blocks network -access and closes connections that used the previously active configuration. -Submit a valid replacement, or [roll back to an earlier -revision](/sandboxes/manage-policies#roll-back-to-an-earlier-revision), then -wait and verify the active revision: - -```shell -openshell policy set my-sandbox --policy repaired-policy.yaml --wait -openshell policy list my-sandbox -``` - -With `retain_last_valid`, a previous valid configuration remains active. Do not -interpret working network access as adoption of the failed candidate. If no -previous valid configuration exists, the effective behavior is still -fail-closed. - -An OCSF configuration event reports the candidate, validation rationale, -configured and effective failure modes, the active configuration version, and -whether a previous policy remains active. - -## Read Policy Load Errors - -When the sandbox 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. - -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. Use the category to decide 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. -- `invalid L7 policy configuration` reports a semantic request-rule error, and - `invalid L7 protocol configuration` reports a protocol configuration or alias - error. -- `ambiguous network endpoint selectors` reports conflicting endpoints. -- 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 -configuration or advance its generation. These bounds apply to load errors -only. Warnings for accepted policies, runtime request diagnostics, and gateway -policy parser messages have separate reporting behavior. - -## Error Reference - -These error codes and conditions identify policy-related failures: - -| Code or condition | Where it appears | Meaning | Diagnosis | -|---|---|---|---| -| `policy_denied` | HTTP 403 response body and sandbox log. | A request rule or missing rule blocked the request. | [Request denials](#diagnose-request-denials) | -| `request_authority_mismatch` | HTTP 403 response body and sandbox log. | The HTTP request authority differs from the authorized tunnel destination. | [Request denials](#diagnose-request-denials) | -| `credential_endpoint_mismatch` | HTTP 403 response body and sandbox log. | The policy allowed the request, but the provider credential is not bound to this host, port, or path. | [Credential denials](#diagnose-credential-denials) | -| `credential_placeholder_in_request_body` | HTTP 403 response body. | A request body contains an invalid or unavailable credential placeholder. | [Credential denials](#diagnose-credential-denials) | -| `ConfigurationInvalid` | Sandbox condition while `Provisioning`. | The initial effective policy or provider configuration failed validation. | [Initial configuration](#repair-initial-configuration) | -| `ProvisioningTimedOut` | Sandbox `Error` reason. | The repair window expired before the configuration became valid. | [Initial configuration](#repair-initial-configuration) | -| `feature_disabled` | `policy.local` response inside the sandbox. | The policy advisor is disabled for the sandbox. | [Enable the Policy Advisor](/sandboxes/policy-advisor#enable-the-policy-advisor) | - -## Next Steps - -- Use [Manage Sandbox Policies](/sandboxes/manage-policies) to apply, verify, - and roll back policy changes. -- Use the [Policy Schema Reference](/reference/policy-schema) for exact field - defaults and validation constraints. -- Use [Logging](/observability/logging) to read OCSF policy events. From 780e12af9ff2f3c4d299e583267cba027c498214 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 00:55:58 +0000 Subject: [PATCH 27/40] docs(policy): fold troubleshooting into policy management guide Signed-off-by: Johnny Greco --- docs/sandboxes/manage-policies.mdx | 67 +++++++++++++++++++++++++++--- docs/sandboxes/network-rules.mdx | 4 +- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx index 81c0a4d5bf..d9725c0fe4 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/sandboxes/manage-policies.mdx @@ -3,7 +3,7 @@ # 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, and to apply a gateway-wide policy." +description: "Use the OpenShell CLI to set, inspect, change, verify, and roll back sandbox policies, apply a gateway-wide policy, and troubleshoot policy problems." keywords: "Generative AI, Cybersecurity, Policy, Sandbox, CLI, Hot Reload, Revision" position: 3 --- @@ -48,7 +48,7 @@ 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](/sandboxes/troubleshoot-policies#repair-initial-configuration). OpenShell +policy](#a-new-sandbox-stays-in-provisioning). OpenShell does not fall back to the default policy. ## Inspect the Current Policy @@ -212,8 +212,8 @@ rules and other changes that arrive at the same time. If the sandbox cannot load a revision, the gateway's `policy_validation_failure_mode` setting 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. [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) -explains how to identify and repair each kind of failure. +stays active. [A Change Fails to Load](#a-change-fails-to-load) explains how to +repair a rejected change. ## Change Filesystem and Process Settings @@ -302,9 +302,64 @@ confirmation, because each one changes the network access of every sandbox on the gateway. 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 every denied connection and request with the destination, the +binary, and the reason. Check the sandbox log first: + +```shell +openshell logs my-sandbox --since 10m --source sandbox +``` + +Policy events are INFO-level log records, so do not filter the log with +`--level warn`. 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 the 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 + +If a new sandbox's policy or provider configuration is invalid, including an +invalid image policy, the sandbox stays in `Provisioning` with a +`ConfigurationInvalid` condition and its workload does not start. Check the +diagnostic: + +```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](/sandboxes/network-rules) for example rules you can adapt to common services and protocols. -- Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) when a - change fails to load or a request is denied unexpectedly. +- Use the [Policy Schema Reference](/reference/policy-schema) for every field, + default, and constraint. diff --git a/docs/sandboxes/network-rules.mdx b/docs/sandboxes/network-rules.mdx index df7e420bcd..f94d0e587c 100644 --- a/docs/sandboxes/network-rules.mdx +++ b/docs/sandboxes/network-rules.mdx @@ -577,5 +577,5 @@ Fields](/reference/policy-schema#inspection-fields). and roll back policy changes. - Use the [Policy Schema Reference](/reference/policy-schema) for protocol defaults, field constraints, and matcher semantics. -- Use [Troubleshoot Sandbox Policies](/sandboxes/troubleshoot-policies) to - separate connection, request, credential, middleware, and activation errors. +- Use [Troubleshoot](/sandboxes/manage-policies#troubleshoot) when a request is + denied unexpectedly. From e9619f714760c568103e5ff2d0e7e103e92a3eaf Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 01:25:52 +0000 Subject: [PATCH 28/40] docs(policy): correct tutorial log samples and GitHub push policy steps The first policy tutorial said the 403 body begins with error, policy, and rule, but the proxy serializes the body with sorted keys. Its log samples also showed the wrong CONNECT deny reason for a sandbox without network rules, and the L7 deny sample omitted the :443 authority, the `l7` engine, and the reason tag that the shorthand formatter emits. The GitHub tutorial filtered denials with `--level warn`, which hides the INFO level OCSF policy events, and showed the retired key=value log format. Its hand-written policy also omitted /bin from the restrictive default, so `policy set` would reject the file for removing a filesystem path on a live sandbox. Start from `policy get --base` and add only the network rules. Signed-off-by: Johnny Greco --- docs/tutorials/first-network-policy.mdx | 8 +++--- docs/tutorials/github-sandbox.mdx | 35 ++++++++++++++----------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/tutorials/first-network-policy.mdx b/docs/tutorials/first-network-policy.mdx index fba6136980..9aa9b3467d 100644 --- a/docs/tutorials/first-network-policy.mdx +++ b/docs/tutorials/first-network-policy.mdx @@ -86,7 +86,7 @@ openshell logs demo --since 5m --source sandbox You see a line like: ```text -[1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> api.github.com:443 [policy:- engine:opa] [reason:no matching 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. @@ -157,10 +157,10 @@ curl -s -X POST https://api.github.com/repos/octocat/hello-world/issues \ -d '{"title":"oops"}' ``` -The proxy returns a `403` response with a JSON body. The body begins with fields like these, followed by details about the denied request: +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",...} +{...,"error":"policy_denied",...,"policy":"github_api",...,"rule":"POST /repos/octocat/hello-world/issues",...} ``` 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. @@ -174,7 +174,7 @@ openshell logs demo --since 5m --source sandbox ``` ```text -[1775014140.412] [sandbox] [OCSF ] [ocsf] HTTP:POST [MED] DENIED POST http://api.github.com/repos/octocat/hello-world/issues [policy:github_api engine:opa] +[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] ``` 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. diff --git a/docs/tutorials/github-sandbox.mdx b/docs/tutorials/github-sandbox.mdx index afacbfb3de..5fcac5f319 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: From 12d061a32411a3c33a887e93f3de4a0bd6ef19cd Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 02:00:13 +0000 Subject: [PATCH 29/40] docs(policy): improve flow and terminology across policy pages Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 31 +++++++++++---------- docs/how-it-works/policies/overview.mdx | 28 ++++++++++--------- docs/how-it-works/policies/prover.mdx | 8 +++--- docs/how-it-works/policies/schema.mdx | 4 +-- docs/sandboxes/manage-policies.mdx | 36 ++++++++++++++----------- 5 files changed, 57 insertions(+), 50 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 242f9befc5..5629eed1f8 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 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 review each proposal before it takes effect." +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: 5 --- @@ -38,10 +38,11 @@ A proposal goes through these steps: 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). + 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. @@ -176,10 +177,9 @@ The policy advisor uses the [policy prover](/reference/policy-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: +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: | Finding | The proposed rule would let a binary | |---|---| @@ -189,8 +189,8 @@ kinds: | `capability_expansion` | Use a new HTTP method at a host and port where it already uses a provider credential. | 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. +even in automatic mode. When you reject a proposal, the agent receives the +findings with your reason, so it can narrow its next attempt. ## What Agents Can Propose @@ -263,17 +263,16 @@ The agent uses these endpoints at `http://policy.local`: | `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. | +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. + 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`. ## Logs -An approved network rule takes effect in the running sandbox without a restart. -OpenShell closes connections opened under the previous rules, so the agent's -retry uses the new rule. The `/wait` endpoint reports `policy_reloaded: true` -once the sandbox has loaded the approved rule. - To follow a proposal in the sandbox log, run: ```shell diff --git a/docs/how-it-works/policies/overview.mdx b/docs/how-it-works/policies/overview.mdx index 0bf210f115..00cadccdcf 100644 --- a/docs/how-it-works/policies/overview.mdx +++ b/docs/how-it-works/policies/overview.mdx @@ -31,6 +31,10 @@ different part of the sandbox and takes effect at a specific time: | `network_policies` | Destinations each binary can reach, and the requests it can send. | Sandbox network proxy. | While the sandbox runs. | | `network_middlewares` | Additional inspection or transformation of allowed network traffic. | 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](/sandboxes/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 @@ -38,13 +42,9 @@ can also restrict the requests those binaries send, for example to allow reading from an API but not writing to it. [Network Rules](/sandboxes/network-rules) explains how OpenShell evaluates rules and gives examples you can adapt. -The startup sections are fixed once the sandbox starts. The network sections -can change while it runs, as described in [How Changes Take -Effect](/sandboxes/manage-policies#how-changes-take-effect). The [Policy Schema -Reference](/reference/policy-schema) -describes the complete file format, and [Supervisor -Middleware](/extensibility/supervisor-middleware) explains how middleware -processes traffic. +The [Policy Schema Reference](/reference/policy-schema) describes every field in +each section, and [Supervisor Middleware](/extensibility/supervisor-middleware) +explains how middleware processes traffic. ## Where the Active Policy Comes From @@ -52,7 +52,7 @@ 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 gateway-global policy set by an administrator. +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. @@ -73,15 +73,15 @@ your own configuration. Inspect the effective policy when you need to know what the sandbox can reach. [Inspect the Current Policy](/sandboxes/manage-policies#inspect-the-current-policy) shows both views. -### Gateway-Global Policy +### Global Policy A gateway administrator can apply one policy to every sandbox on the gateway. The global policy replaces each sandbox's policy. It is not a ceiling intersected with existing grants. While it is active, sandbox policy changes are blocked and provider-contributed network rules are suppressed. Deleting the global policy restores normal policy selection and provider rules. Refer to -[Apply a Gateway-Wide -Policy](/sandboxes/manage-policies#apply-a-gateway-wide-policy) for the +[Apply a Global +Policy](/sandboxes/manage-policies#apply-a-global-policy) for the commands. ## Next Steps @@ -90,7 +90,9 @@ commands. evaluates network rules and to adapt examples for common services. - Use [Manage Sandbox Policies](/sandboxes/manage-policies) to create, update, verify, and roll back policies. -- Use [Policy Advisor](/sandboxes/policy-advisor) to let an agent propose narrow - network changes for your review. +- Use the [Policy Prover](/reference/policy-prover) to check that a policy + grants no more access than a boundary you define. +- Use the [Policy Advisor](/sandboxes/policy-advisor) to let an agent propose + narrow network rules for your review. - Use the [Policy Schema Reference](/reference/policy-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 073a254c26..5399e2e19a 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -3,7 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 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 maximum policy you define." +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 --- @@ -160,9 +160,9 @@ 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`. +`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 diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 13db285ced..d87cf95f83 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -357,8 +357,8 @@ mcp: OpenShell reads the revision from each request's `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. +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. ### JSON-RPC Rules diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx index d9725c0fe4..7d36d267bd 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/sandboxes/manage-policies.mdx @@ -3,7 +3,7 @@ # 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 gateway-wide policy, and troubleshoot policy problems." +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 --- @@ -55,10 +55,10 @@ does not fall back to the default policy. A sandbox's policy has two views. The base policy is the policy you set for the sandbox. The effective policy is the policy the sandbox enforces, which is the -base policy plus any rules that attached providers add. When an administrator -sets a gateway-global policy, it replaces both, so the effective policy is the -global policy. `--base` shows the base policy, and `--full` shows the effective -policy: +base policy plus any rules that attached providers add. When a gateway +administrator sets a global policy, it replaces both, so the effective policy is +the global policy. `--base` shows the base policy, and `--full` shows the +effective policy: ```shell openshell policy get my-sandbox --base @@ -218,25 +218,27 @@ 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. To change these settings, save -the base policy as described in [Replace the Complete -Policy](#replace-the-complete-policy), edit it, and create a new sandbox with -the file: +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](/reference/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 ``` -When the policy allows network access, OpenShell adds the system paths that -sandbox processes need, so list only the paths your workload requires. [Default -Policy](/reference/default-policy) lists those paths. - ## Verify a Change Without `--wait`, a successful command means only that the gateway accepted the @@ -280,10 +282,10 @@ 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 gateway-global policy, and the limits on +attachments, credentials, or a global policy, and the limits on filesystem and process changes still apply. -## Apply a Gateway-Wide Policy +## 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 @@ -361,5 +363,9 @@ openshell sandbox start my-sandbox - Use [Network Rules](/sandboxes/network-rules) for example rules you can adapt to common services and protocols. +- Use the [Policy Prover](/reference/policy-prover) to check that a policy + grants no more access than a boundary you define. +- Use the [Policy Advisor](/sandboxes/policy-advisor) to let an agent propose + the network rules it needs for your review. - Use the [Policy Schema Reference](/reference/policy-schema) for every field, default, and constraint. From 99828036f04dfdbf66eb630b5800890c64b0cf84 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 02:00:47 +0000 Subject: [PATCH 30/40] docs(policy): correct network rule matching and protocol details Signed-off-by: Johnny Greco --- docs/sandboxes/network-rules.mdx | 192 ++++++++++++++++++------------- 1 file changed, 110 insertions(+), 82 deletions(-) diff --git a/docs/sandboxes/network-rules.mdx b/docs/sandboxes/network-rules.mdx index f94d0e587c..9838fe30fb 100644 --- a/docs/sandboxes/network-rules.mdx +++ b/docs/sandboxes/network-rules.mdx @@ -39,7 +39,7 @@ network_policies: port: 443 binaries: - path: /usr/bin/curl - - path: /usr/bin/python3 + - path: /usr/bin/wget ``` The rule allows these four combinations: @@ -48,30 +48,33 @@ The rule allows these four combinations: |---|---| | `/usr/bin/curl` | `api.example.com:443` | | `/usr/bin/curl` | `uploads.example.com:443` | -| `/usr/bin/python3` | `api.example.com:443` | -| `/usr/bin/python3` | `uploads.example.com:443` | +| `/usr/bin/wget` | `api.example.com:443` | +| `/usr/bin/wget` | `uploads.example.com:443` | -If `python3` should reach only `api.example.com`, put that binary and endpoint in -a separate rule. +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. A -rule for `pip install` must list `/usr/bin/python3`, not `/usr/bin/pip`. When -OpenShell denies a connection, the sandbox log shows the binary it identified. -[Allow PyPI Downloads](#allow-pypi-downloads) and [Allow npm -Installs](#allow-npm-installs) show rules that list interpreters. +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 resolves symlinks to the real executable path, so a symlink cannot -select a different rule. It also records a checksum of each executable the -first time a rule allows it, and denies later connections if the file at that -path changes. +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 @@ -80,8 +83,9 @@ 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 `protocol`, OpenShell also reads each request - sent over the connection and checks it against the endpoint's request rules. +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. @@ -95,8 +99,14 @@ an inspected endpoint. The `protocol` field selects what OpenShell inspects: | `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](/reference/policy-schema#json-rpc-rules) | -| `tcp` | Nothing. The binary gets a native TCP connection, which suits clients such as databases. | [Native TCP](#allow-native-tcp) | -| Omitted | No request rules apply. OpenShell still terminates TLS and checks that each HTTP request is addressed to the allowed host, but it allows any method or path. | -- | +| `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. Use an inspected endpoint when the request, not only the destination, determines the risk, such as allowing reads but not writes on an API. @@ -106,9 +116,14 @@ determines the risk, such as allowing reads but not writes on an API. The `enforcement` field on an inspected endpoint decides what happens when a request breaks the endpoint's request rules: -- `enforce` blocks the request with an OpenShell `policy_denied` response. +- `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: @@ -117,16 +132,19 @@ events in the sandbox log: openshell logs my-sandbox --since 5m --source sandbox ``` -Policy events are INFO-level log records, so do not filter them out with -`--level warn`. When the log shows only the violations you expect, switch the -endpoint to `enforce`. An endpoint in audit mode blocks no requests. +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. +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 @@ -151,8 +169,8 @@ 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 `openshell policy update` can express a rule, the example shows the -command. Otherwise, add the YAML under the `network_policies` section of a +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](/sandboxes/manage-policies#replace-the-complete-policy). After you apply @@ -258,8 +276,8 @@ Agent](/get-started/tutorials/github-sandbox). 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 Python interpreter, and any Python program in the sandbox can use it. -This rule lets `pip` and `uv` install packages from PyPI: +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: @@ -275,15 +293,15 @@ pypi: enforcement: enforce access: read-only binaries: - - path: /usr/bin/python3 + - path: /usr/bin/python3.12 - path: /usr/local/bin/uv ``` -OpenShell resolves `/usr/bin/python3` to its versioned interpreter, such as -`/usr/bin/python3.12`. If your image uses another interpreter, such as a -uv-managed Python, list its canonical path or a glob that matches it, such as -`/sandbox/.uv/python/*/bin/python3*`. Globs are not symlink-resolved. For a -private package index, replace the hosts with your index's hosts. +`/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: @@ -295,9 +313,9 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ ### 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. Node images from -the official Node.js project install `node` at `/usr/local/bin/node`, so adjust -the path for your image: +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: @@ -315,8 +333,9 @@ npm_registry: 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 add -an allow rule for the audit request if you need it. +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: @@ -329,8 +348,10 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ 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, but loopback, -link-local, and cloud metadata addresses are always blocked. +reach the private addresses that its hostname resolves to, unless the endpoint +comes from an approved [policy advisor](/sandboxes/policy-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 @@ -378,10 +399,10 @@ instead of broadening the range when an address changes. ### Allow WebSocket Messages -Use `protocol: websocket` for an RFC 6455 upgrade and client-to-server message -policy. 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/**`: +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: @@ -409,13 +430,16 @@ Use your Node client to test a successful upgrade and text message on `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 upstream-to-client messages. Provider-credentialed endpoints remain -on the parsed relay and reject binary frames unless -`allow_uninspected_credentials: true` explicitly accepts the weaker boundary. -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 the upgraded connection with close code `1012`, so -the client must reconnect. +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 @@ -467,9 +491,10 @@ 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 `/usr/bin/python3` with an MCP client and a Streamable HTTP -server you control. It allows initialization, tool discovery, and -`read_status`, while denying `delete_resource`: +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: @@ -493,7 +518,7 @@ mcp_server: - method: tools/call tool: delete_resource binaries: - - path: /usr/bin/python3 + - path: /usr/bin/python3.12 ``` Verify initialization and `read_status` before confirming that `delete_resource` @@ -503,15 +528,20 @@ 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](/reference/policy-schema#mcp-version-selection). Server responses and -SSE messages are relayed without MCP policy parsing. An MCP endpoint cannot -share a host and port with an endpoint that uses a different protocol. +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` when the application needs ordinary DNS resolution and a -native TCP socket, such as a database client. OpenShell checks the hostname, -port, and executable but does not inspect the application payload, so prefer an -inspected protocol whenever the client supports one. +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: @@ -519,7 +549,7 @@ inspected protocol whenever the client supports one. ```shell openshell policy update my-sandbox \ --rule-name postgres \ - --binary /usr/bin/psql \ + --binary '/usr/lib/postgresql/*/bin/psql' \ --add-endpoint db.internal.example:5432::tcp \ --wait ``` @@ -534,7 +564,7 @@ postgres: port: 5432 protocol: tcp binaries: - - path: /usr/bin/psql + - path: /usr/lib/postgresql/*/bin/psql ``` @@ -547,29 +577,27 @@ 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. -Docker and Podman support native TCP enforcement. The standard runtime prepares -DNS and TCP connection handling before the workload starts, even when the -initial policy has no TCP endpoints, so you can add the first endpoint without -recreating the sandbox. Other runtimes must provide the same support before -they can activate a TCP rule. +OpenShell prepares DNS and TCP handling when the sandbox starts, so you can add +a TCP endpoint to a running sandbox without recreating it. + +For a client that starts TLS as soon as it connects, or that must complete TLS +itself, for example to present a client certificate, set `tls: skip` so that +OpenShell relays the encrypted connection without terminating it. Clients that +negotiate TLS within their protocol, such as `psql`, do not need it. Refer to +[Inspection Fields](/reference/policy-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 does not inspect the payload's -authority. - -Policy DNS returns a supervisor-owned synthetic address with a bounded TTL. -Clients must honor that TTL and resolve the hostname again before reconnecting. -A client that caches the synthetic address indefinitely can fail after its -mapping expires, even while the endpoint remains allowed. Docker and Podman -currently advertise IPv4 egress for policy DNS, so AAAA queries return a -successful empty answer and dual-stack clients must continue with the A record. - -If a client that uses the sandbox proxy must complete TLS with the upstream -service itself, for example to present a client certificate, set `tls: skip` on -an endpoint without `protocol` instead. Refer to [Inspection -Fields](/reference/policy-schema#inspection-fields). +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. 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 From 743f39409eef038322c6b4c81dcdc68b33d119c2 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 02:03:17 +0000 Subject: [PATCH 31/40] docs(policy): align policy management steps with CLI behavior Signed-off-by: Johnny Greco --- docs/sandboxes/manage-policies.mdx | 114 +++++++++++++++++------------ 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/docs/sandboxes/manage-policies.mdx b/docs/sandboxes/manage-policies.mdx index 7d36d267bd..0975d70e66 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/sandboxes/manage-policies.mdx @@ -24,13 +24,13 @@ 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 -used most recently. +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=./policy.yaml +export OPENSHELL_SANDBOX_POLICY="$PWD/policy.yaml" openshell sandbox create --name my-sandbox ``` @@ -54,11 +54,10 @@ 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 policy the sandbox enforces, which is the -base policy plus any rules that attached providers add. When a gateway -administrator sets a global policy, it replaces both, so the effective policy is -the global policy. `--base` shows the base policy, and `--full` shows the -effective policy: +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 @@ -99,14 +98,17 @@ openshell policy update my-sandbox \ --wait ``` -The endpoint sets the host and port, then the request rules. `read-only` allows -`GET`, `HEAD`, and `OPTIONS` requests, `rest` turns on HTTP request inspection, -and `enforce` blocks every other request. The `--binary` path must match the -executable inside the sandbox. +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](/sandboxes/network-rules#binary-matching). To allow another kind of request on an existing rule, add an allow rule. Name -the rule and repeat its complete binary list from the base policy, so the -command changes only that rule: +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 \ @@ -116,25 +118,27 @@ openshell policy update my-sandbox \ --wait ``` -`--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: +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. Preview -`--remove-endpoint` in particular, because it removes the destination from every -rule that lists it. +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`, or credential settings. The new file replaces every section, so -start from the current base policy. +`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. @@ -178,9 +182,9 @@ 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: +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 | |---|---|---| @@ -188,7 +192,7 @@ startup sections do not: | 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 workdir, Landlock, or process settings | OpenShell rejects the change after the workload starts. | 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, @@ -209,11 +213,12 @@ flowchart TD 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 gateway's `policy_validation_failure_mode` setting 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. +a revision, the `policy_validation_failure_mode` option in the [gateway +configuration](/reference/gateway-config#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 @@ -239,6 +244,10 @@ 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 @@ -299,25 +308,30 @@ role: | View global policy history. | `openshell policy list --global` | | Remove the global policy and restore normal policy selection. | `openshell policy delete --global` | -A global policy takes effect immediately. The set and delete commands ask for -confirmation, because each one changes the network access of every sandbox on -the gateway. Deleting the global policy fails if a sandbox's own policy and -provider rules would be invalid once restored. +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 every denied connection and request with the destination, the -binary, and the reason. Check the sandbox log first: +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 ``` -Policy events are INFO-level log records, so do not filter the log with -`--level warn`. Compare the logged binary and destination with the effective -policy, and use the error code in the response to find the cause: +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 | |---|---|---| @@ -329,18 +343,22 @@ policy, and use the error code in the response to find the cause: ### A Change Fails to Load `openshell policy list my-sandbox` shows a revision that the sandbox rejected as -`Failed`, with the 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 +`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 -If a new sandbox's policy or provider configuration is invalid, including an -invalid image policy, the sandbox stays in `Provisioning` with a -`ConfigurationInvalid` condition and its workload does not start. Check the -diagnostic: +`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 From 10d32e6a091b8672c43217a0df8add48d51f33ca Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 02:06:05 +0000 Subject: [PATCH 32/40] docs(policy): correct policy advisor proposal and approval details Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 102 +++++++++++++++---------- 1 file changed, 60 insertions(+), 42 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 5629eed1f8..68a902fe96 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -9,9 +9,9 @@ position: 5 --- The policy advisor lets an agent in a sandbox propose a new network rule when -OpenShell blocks one of its network requests, and you approve or reject each -proposal. It handles network access only. A proposal can add a network rule, but -it cannot remove rules or change filesystem, Landlock, or process settings. +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 @@ -21,8 +21,8 @@ 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 only -when OpenShell's checks find that it adds no new risk. +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 @@ -50,9 +50,10 @@ A proposal goes through these steps: 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 the denials it observes in the -sandbox. These drafts go through the same checks and review as proposals from -the agent. +OpenShell also drafts proposals on its own from connections that it blocks, in +every sandbox, whether or not the policy advisor is enabled. 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. When the policy advisor is enabled, OpenShell adds the following to the sandbox so the agent can find and use it: @@ -124,8 +125,8 @@ from the proposal's `Chunk` line: openshell rule approve --chunk-id ``` -Otherwise, reject it with a reason. OpenShell sends the reason to the agent, -which can submit a narrower proposal: +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 rule reject \ @@ -135,23 +136,30 @@ openshell rule reject \ 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. You can also review proposals in the terminal UI with -`openshell term`. +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. -## Approve Low-Risk Proposals Automatically +## Approve Proposals Automatically By default, every proposal waits for your review. In automatic mode, OpenShell approves a proposal without review when both of these are true: - 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 private - or internal addresses, wildcard hosts, `allowed_ips` entries without a host, - ephemeral ports, and well-known database or service ports, and shows each - flag on the proposal's `Security` line. +- 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. -Every other proposal still waits for your review. Turn on automatic mode for -every sandbox on the gateway: +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. + +Turn on automatic mode for every sandbox on the gateway: ```shell openshell settings set --global \ @@ -164,7 +172,7 @@ To turn it on for one sandbox, set the key on that sandbox, or pass `--approval-mode auto` when you create it: ```shell -openshell sandbox create --approval-mode auto +openshell sandbox create --name --approval-mode auto ``` The accepted values are `manual`, the default, and `auto`. A gateway-wide value @@ -184,7 +192,7 @@ rule would give a binary new access of one of these kinds: | 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-remote-https`, `ssh`, or `nc` do, to a host where a provider credential is available. | +| `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. | @@ -194,8 +202,10 @@ findings with your reason, so it can narrow its next attempt. ## What Agents Can Propose -An agent proposes an endpoint and binary, optionally with REST method and path -rules. A good proposal allows one method on the narrowest path the task needs: +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 { @@ -235,21 +245,26 @@ rules. A good proposal allows one method on the narrowest path the task needs: ``` Agents cannot propose `protocol: tcp` or `tls: skip`, because OpenShell cannot -inspect that traffic. WebSocket, GraphQL, and MCP rules, credential settings, -and endpoint path selectors are also outside what an agent can propose. Add -those rules yourself, as described in [Manage Sandbox +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](/sandboxes/manage-policies). -Agents also cannot add `allowed_ips`. If a proposed host resolves to a private -address, OpenShell still blocks the connection after approval until you add an -`allowed_ips` entry yourself. Loopback, link-local, and cloud metadata addresses -are always blocked, and you cannot approve a proposal that targets them. +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. -An approved proposal becomes part of the sandbox's own policy and never changes -rules that providers contribute. When it overlaps an existing rule, its allow -rules combine with that rule. It cannot change settings that must have a single -value, such as TLS handling or `allowed_ips`. A proposed rule also does not -inherit trust from a provider rule for a different binary. +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 @@ -258,7 +273,7 @@ The agent uses these endpoints at `http://policy.local`: | Endpoint | Purpose | |---|---| | `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 removed. | +| `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. | @@ -269,7 +284,7 @@ previous rules, so the agent's retry uses the new rule. 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`. +`policy.local`. OpenShell still drafts proposals from blocked connections. ## Logs @@ -279,11 +294,14 @@ To follow a proposal in the sandbox log, run: openshell logs --since 10m ``` -Look for the denied request (`HTTP:* DENIED`), then `CONFIG:PROPOSED`, -`CONFIG:APPROVED` or `CONFIG:REJECTED`, `CONFIG:LOADED`, and the retried -request. An automatic approval logs `CONFIG:APPROVED` with `auto=true`, the -proposal's source (`agent_authored` or `mechanistic`, for drafts that OpenShell -created from denials), and where the approval mode setting came 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 From c4aad25036c4392eec3663e55a9c27a40ca28a38 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 02:15:13 +0000 Subject: [PATCH 33/40] docs(policy): correct policy section, default, and schema details Signed-off-by: Johnny Greco --- docs/how-it-works/policies/default-policy.mdx | 68 ++++++----- docs/how-it-works/policies/overview.mdx | 23 ++-- docs/how-it-works/policies/schema.mdx | 111 +++++++++++------- 3 files changed, 114 insertions(+), 88 deletions(-) diff --git a/docs/how-it-works/policies/default-policy.mdx b/docs/how-it-works/policies/default-policy.mdx index 1d37eb53fa..9ef7ab578f 100644 --- a/docs/how-it-works/policies/default-policy.mdx +++ b/docs/how-it-works/policies/default-policy.mdx @@ -8,18 +8,18 @@ keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Policy position: 6 --- -This reference describes the restrictive policy OpenShell uses when a sandbox has -no other policy, and the filesystem paths that OpenShell adds to sandbox +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. ## When the Default Applies -OpenShell uses the restrictive default only when a 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 +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](/sandboxes/policies#where-the-active-policy-comes-from) for the complete selection order. @@ -42,17 +42,18 @@ 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 their network rules -to the effective policy. Provider rules are runtime composition, not fields in -the default policy, so inspect the base and effective views separately to -identify a provider-derived grant. +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 honor a non-root OCI `USER`. When an image declares no user, they use -numeric UID and GID 1000. Other drivers apply their configured non-root -identity. +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 @@ -67,19 +68,20 @@ baseline paths to the sandbox's filesystem policy at startup: | Read-write | `/tmp`, `/dev/null` | OpenShell adds a baseline path only when it is available and your policy does -not already list it. It never changes the access of a path you list, so a +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`. Filesystem paths cannot be -removed from a running sandbox, so keep the added paths when you replace the -complete policy. +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 to -the stored policy. +certificates under `/run/openshell-supervisor-ca`. This grant is not saved in +the sandbox's policy. ### GPU Sandboxes @@ -93,26 +95,28 @@ additional paths when the corresponding GPU device is present: 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 to -the stored policy. +it exists in the workload. These paths apply at runtime and are not saved in +the sandbox's policy. ### Protected Paths -On current Linux isolation paths, a mandatory Landlock baseline protects the -private `/.openshell` hierarchy 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. +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. ## Inspect the Selected Policy -Inspect the sandbox's base policy and the gateway's effective representation: +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: ```shell openshell policy get --base openshell policy get --full ``` -`--full` shows gateway composition, not an attestation of the restrictions -already installed in a running kernel process. Use sandbox readiness, revision -status, request checks, and runtime logs to verify activation. +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/overview.mdx b/docs/how-it-works/policies/overview.mdx index 00cadccdcf..4f11267f92 100644 --- a/docs/how-it-works/policies/overview.mdx +++ b/docs/how-it-works/policies/overview.mdx @@ -14,22 +14,23 @@ 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](/get-started/tutorials/first-network-policy). -To create and change policies, refer to -[Manage Sandbox Policies](/sandboxes/manage-policies). +[Write Your First Sandbox Network +Policy](/get-started/tutorials/first-network-policy). To create and change +policies, refer to [Manage Sandbox Policies](/sandboxes/manage-policies). ## What a Policy Controls -A policy file has up to five top-level sections. Each section controls a -different part of the sandbox and takes effect at a specific time: +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 an unsupported kernel or an inaccessible path stops the sandbox from starting. | Sandbox startup checks. | At sandbox startup. | -| `process` | User and group that sandbox processes run as. | Identity change before the workload starts. | 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 or transformation of allowed network traffic. | 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 @@ -76,9 +77,9 @@ Policy](/sandboxes/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 policy. It is not a ceiling -intersected with existing grants. While it is active, sandbox policy changes are -blocked and provider-contributed network rules are suppressed. Deleting the +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](/sandboxes/manage-policies#apply-a-global-policy) for the diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index d87cf95f83..5a9bc70966 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -32,8 +32,8 @@ network_middlewares: { ... } | `network_policies` | map | No | Live | Which binaries can reach which network endpoints. | | `network_middlewares` | map | No | Live | Middleware applied to allowed traffic. | -Startup fields take effect when a sandbox starts. Live fields can change while it -runs. Refer to [How Changes Take +Startup fields take effect when a sandbox starts. Live fields can change while +it runs. Refer to [How Changes Take Effect](/sandboxes/manage-policies#how-changes-take-effect). ## Filesystem Policy @@ -46,12 +46,12 @@ Effect](/sandboxes/manage-policies#how-changes-take-effect). 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, apart from the baseline paths described in -[Default Policy](/reference/default-policy). +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](/reference/default-policy#baseline-filesystem-paths). Each path must be absolute, must not contain `..`, and must not exceed 4096 -characters. `read_write` cannot contain overly broad paths such as `/`. A -policy can list at most 256 paths. +bytes. `read_write` cannot contain `/`. A policy can list at most 256 paths. ```yaml showLineNumbers={false} filesystem_policy: @@ -67,8 +67,8 @@ filesystem_policy: | `compatibility` | string | `best_effort` | `best_effort` or `hard_requirement` | Both values skip individual paths that are missing or that the workload cannot -open. They differ when no path can be applied or the kernel cannot apply the -rules: +open. They differ when none of the listed paths can be applied, or when Landlock +fails to enforce the policy's rules: | Value | Behavior | |---|---| @@ -83,13 +83,15 @@ and skipped. | Field | Type | Default | Description | |---|---|---|---| -| `run_as_user` | string | Driver default | User name or UID for the workload. | -| `run_as_group` | string | Driver default | Group name or GID for the workload. | +| `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. | -A value must be `sandbox` or a numeric ID from `1` through `4294967294`. -OpenShell rejects root. Each field is independent, so you can set one and let -the compute driver choose the other. Docker and Podman default to the image's -`USER`. +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: @@ -109,6 +111,9 @@ for examples, refer to [Network Rules](/sandboxes/network-rules). | `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 Endpoint fields fall into four groups. @@ -117,24 +122,35 @@ Endpoint fields fall into four groups. | Field | Type | Required | Description | |---|---|---|---| -| `host` | string | Conditional | Hostname or IP address. A `*` or `**` wildcard is allowed only in the first DNS label, such as `*.example.com`. | +| `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. Takes precedence over `port`. | +| `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. Wildcard and hostless endpoints can -reach private addresses only through `allowed_ips`. Loopback, link-local, and -unspecified addresses, including cloud metadata addresses, are always blocked, -and `allowed_ips` entries that overlap them are rejected. +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](/sandboxes/network-rules#connection-and-request-checks). | -| `tls` | string | Automatic | `skip` relays traffic without terminating TLS. Use it only on endpoints without `protocol`. | +| `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. | @@ -170,6 +186,8 @@ network_policies: access: full credential_binding: provider: work-gcp + binaries: + - path: /usr/bin/curl ``` #### Protocol Options @@ -177,12 +195,12 @@ network_policies: | 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. Required with `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`, an endpoint without `rules` allows all MCP methods and tools except those that deny rules match, and rules can omit `method`. Refer to [MCP Rules](#mcp-rules). | +| `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 @@ -194,13 +212,13 @@ OpenShell rejects an endpoint that breaks these 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. -- `rules` and `deny_rules` cannot be empty lists, and `rules` must contain at - least one allow rule. - `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 that share a host and port must use the same `tls` and - `allowed_ips` values. +- 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. @@ -218,8 +236,9 @@ endpoints do not. ### Allow and Deny Rules Each entry in `rules` wraps its matcher fields in `allow`. Each entry in -`deny_rules` lists the matcher fields directly. A request that matches any deny -rule is blocked, regardless of the allow rules or access preset. +`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. ```yaml showLineNumbers={false} rules: @@ -251,8 +270,8 @@ rules: method: GET path: /api/v1/download query: - version: - any: ["1.*", "2.*"] + platform: + any: ["linux-*", "darwin-*"] deny_rules: - method: "*" path: "/repos/*/*/rulesets" @@ -272,10 +291,10 @@ OpenShell does not inspect binary frames or messages from the server. 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/** @@ -285,7 +304,7 @@ deny_rules: | Field | Type | Required | Description | |---|---|---|---| -| `operation_type` | string | Yes | `query`, `mutation`, `subscription`, or `*`. | +| `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. | @@ -354,8 +373,10 @@ mcp: versions: ["2025-03-26", "2025-11-25"] ``` -OpenShell reads the revision from each request's `MCP-Protocol-Version` header, -or uses `2025-03-26` when the header is absent. A duplicate, empty, or +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. @@ -380,14 +401,14 @@ deny_rules: | Field | Type | Required | Description | |---|---|---|---| -| `path` | string | Yes | Executable path or glob, such as `/usr/bin/curl` or `/sandbox/.venv/bin/*`. | +| `path` | string | Yes | Executable path or glob, such as `/usr/bin/curl` or `/usr/lib/jvm/*/bin/java`. | A binary matches the executable that opens the connection or any of its parent -processes. Scripts run as their interpreter, so list `/usr/bin/python3` for a -Python script. OpenShell resolves symlinks in exact paths but not in globs. -OpenShell records each executable's checksum when a rule first allows it, and -denies later connections if the file changes. Refer to [Binary -Matching](/sandboxes/network-rules#binary-matching). +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](/sandboxes/network-rules#binary-matching). ## Network Middleware @@ -404,7 +425,7 @@ network rules allow, in ascending `order`. | `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 use the same wildcards as endpoint hosts, up to 32 patterns per +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). @@ -425,10 +446,10 @@ network_middlewares: | Matcher | Case-sensitive | Wildcards | |---|---|---| -| Endpoint `host` and middleware hosts | No | `*` matches one DNS label, and `**` matches one or more labels. | +| Endpoint `host` and middleware hosts | No | `*` matches characters within one DNS label, and `**` matches one or more labels. | | Binary `path` | Yes | `*` matches within one path segment, and `**` matches across segments. | | REST and WebSocket `path` | Yes | `*` matches within one path segment, and `**` matches across segments. `?` matches one character, and bracket classes such as `[0-9]` are supported. `/repos/**` does not match `/repos`. | -| Query values | Yes | `*` matches any characters. | +| Query values and MCP tool names | Yes | `*` matches any characters except `.`, and `**` matches any characters. | ## Full Example From a4565ab0f6f16a326dcc776c15641c66195be5ee Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 02:15:13 +0000 Subject: [PATCH 34/40] docs(policy): correct prover installation and coverage limits Signed-off-by: Johnny Greco --- docs/how-it-works/policies/prover.mdx | 29 ++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/docs/how-it-works/policies/prover.mdx b/docs/how-it-works/policies/prover.mdx index 5399e2e19a..74f80c2e56 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -50,8 +50,12 @@ 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 `openshell-prover` CLI is installed with OpenShell. It reads policy files on -your machine, does not need a gateway, and does not apply or approve policies. +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`: @@ -85,7 +89,7 @@ result: within_boundary coverage: domains=filesystem,network_l4,network_rest,process,landlock ``` -The `coverage` line lists the parts of the policy that the prover checked. Now +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: @@ -117,8 +121,9 @@ openshell sandbox get my-sandbox --policy-only > candidate.yaml openshell-prover check candidate.yaml --boundary boundary.yaml ``` -The effective policy includes rules from attached providers, so the check covers -everything the sandbox can reach. The prover does not add provider rules itself. +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. @@ -206,8 +211,13 @@ The prover returns `unsupported` in these cases: 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 that could match the same host and port, including wildcard hosts, - set different `allowed_ips`. +- 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 @@ -216,5 +226,6 @@ 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`. Other request protocols, such as WebSocket, GraphQL, -MCP, and JSON-RPC, return `unsupported`. +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`. From a9b9bc8b1d03d4c53783096562b9d24015fca72e Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 15:17:28 +0000 Subject: [PATCH 35/40] docs(policy): recommend tls skip for server-first protocols Signed-off-by: Johnny Greco --- docs/sandboxes/network-rules.mdx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/sandboxes/network-rules.mdx b/docs/sandboxes/network-rules.mdx index 9838fe30fb..89816a668b 100644 --- a/docs/sandboxes/network-rules.mdx +++ b/docs/sandboxes/network-rules.mdx @@ -106,7 +106,8 @@ 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. +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. @@ -580,9 +581,10 @@ 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. -For a client that starts TLS as soon as it connects, or that must complete TLS -itself, for example to present a client certificate, set `tls: skip` so that -OpenShell relays the encrypted connection without terminating it. Clients that +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](/reference/policy-schema#inspection-fields). From 4f63007334e4223f40b2b1ad68068a27d7a976e7 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 15:17:28 +0000 Subject: [PATCH 36/40] docs(policy): fix stale baseline path and interpreter examples Signed-off-by: Johnny Greco --- docs/how-it-works/inference.mdx | 8 ++++---- docs/security/best-practices.mdx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/how-it-works/inference.mdx b/docs/how-it-works/inference.mdx index f01c2cf36a..5e8c88eca8 100644 --- a/docs/how-it-works/inference.mdx +++ b/docs/how-it-works/inference.mdx @@ -34,7 +34,8 @@ 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. +distinct `display_name`, and set `binaries` to the executables that may call the +API, as described in [Binary Matching](/sandboxes/network-rules#binary-matching). Keep the credential and endpoint definitions you intend to grant. For a Python workload, the edited fields can look like: @@ -42,9 +43,8 @@ workload, the edited fields can look like: 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/** ``` diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index be254db410..c3b4d0ef5a 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -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. | From c5a458aaf1859bb8e02c08407f5ffa339621c56e Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 15:29:57 +0000 Subject: [PATCH 37/40] docs(policy): move policy pages under how-it-works and fix links Signed-off-by: Johnny Greco --- docs/about/overview.mdx | 4 +- docs/extensibility/isolation-backends.mdx | 6 +-- docs/how-it-works/inference.mdx | 9 ++-- docs/how-it-works/policies/advisor.mdx | 16 +++---- docs/how-it-works/policies/default-policy.mdx | 4 +- .../policies}/manage-policies.mdx | 33 ++++++------- .../policies}/network-rules.mdx | 46 +++++++++--------- docs/how-it-works/policies/overview.mdx | 47 ++++++++++--------- docs/how-it-works/policies/prover.mdx | 4 +- docs/how-it-works/policies/schema.mdx | 18 +++---- docs/how-it-works/providers/profiles.mdx | 2 +- docs/how-it-works/sandboxes/overview.mdx | 4 +- docs/index.yml | 17 +++++-- docs/kubernetes/ingress.mdx | 2 +- docs/security/best-practices.mdx | 12 ++--- docs/tutorials/first-network-policy.mdx | 8 ++-- docs/tutorials/github-sandbox.mdx | 2 +- 17 files changed, 126 insertions(+), 108 deletions(-) rename docs/{sandboxes => how-it-works/policies}/manage-policies.mdx (93%) rename docs/{sandboxes => how-it-works/policies}/network-rules.mdx (93%) 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 5e8c88eca8..72c361e654 100644 --- a/docs/how-it-works/inference.mdx +++ b/docs/how-it-works/inference.mdx @@ -35,9 +35,10 @@ 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 executables that may call the -API, as described in [Binary Matching](/sandboxes/network-rules#binary-matching). -Keep the credential and endpoint definitions you intend to grant. For a Python -workload, the edited fields can look like: +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 @@ -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 68a902fe96..20cfda7d81 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -181,8 +181,8 @@ every sandbox by setting `manual` on the gateway. ### Proposal Risk Check -The policy advisor uses the [policy prover](/reference/policy-prover) to run a -proposal risk check on every proposal. Unlike the boundary check that the +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 @@ -250,7 +250,7 @@ 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](/sandboxes/manage-policies). +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 @@ -305,10 +305,10 @@ if the agent was waiting for the decision. An automatic approval's ## Next Steps -- Use [Manage Sandbox Policies](/sandboxes/manage-policies) to change policies - yourself. -- Use [Network Rules](/sandboxes/network-rules) and the - [Policy Schema Reference](/reference/policy-schema) for rules that agents +- 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](/reference/policy-prover) to check a +- 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 9ef7ab578f..b682c14437 100644 --- a/docs/how-it-works/policies/default-policy.mdx +++ b/docs/how-it-works/policies/default-policy.mdx @@ -20,8 +20,8 @@ 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](/sandboxes/policies#where-the-active-policy-comes-from) for the complete -selection order. +From](/how-it-works/policies/overview#where-the-active-policy-comes-from) for +the complete selection order. ## Default Filesystem Access diff --git a/docs/sandboxes/manage-policies.mdx b/docs/how-it-works/policies/manage-policies.mdx similarity index 93% rename from docs/sandboxes/manage-policies.mdx rename to docs/how-it-works/policies/manage-policies.mdx index 0975d70e66..681f7f0f29 100644 --- a/docs/sandboxes/manage-policies.mdx +++ b/docs/how-it-works/policies/manage-policies.mdx @@ -35,7 +35,7 @@ openshell sandbox create --name my-sandbox ``` Without either, the sandbox uses a policy from its image or the restrictive -[default policy](/reference/default-policy). +[default policy](/how-it-works/policies/default-policy). ## Ship a Policy in an Image @@ -69,7 +69,7 @@ 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](/reference/policy-prover): +check it with the [policy prover](/how-it-works/policies/prover): ```shell openshell sandbox get my-sandbox --policy-only > effective-policy.yaml @@ -103,7 +103,7 @@ 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](/sandboxes/network-rules#binary-matching). +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`. @@ -214,11 +214,11 @@ flowchart TD 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](/reference/gateway-config#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. +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 @@ -229,7 +229,8 @@ 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](/reference/default-policy#baseline-filesystem-paths) lists +Filesystem +Paths](/how-it-works/policies/default-policy#baseline-filesystem-paths) lists those paths. @@ -379,11 +380,11 @@ openshell sandbox start my-sandbox ## Next Steps -- Use [Network Rules](/sandboxes/network-rules) for example rules you can adapt - to common services and protocols. -- Use the [Policy Prover](/reference/policy-prover) to check that a policy +- 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](/sandboxes/policy-advisor) to let an agent propose - the network rules it needs for your review. -- Use the [Policy Schema Reference](/reference/policy-schema) for every field, - default, and constraint. +- 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/sandboxes/network-rules.mdx b/docs/how-it-works/policies/network-rules.mdx similarity index 93% rename from docs/sandboxes/network-rules.mdx rename to docs/how-it-works/policies/network-rules.mdx index 89816a668b..4a96241653 100644 --- a/docs/sandboxes/network-rules.mdx +++ b/docs/how-it-works/policies/network-rules.mdx @@ -98,7 +98,7 @@ an inspected endpoint. The `protocol` field selects what OpenShell inspects: | `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](/reference/policy-schema#json-rpc-rules) | +| `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. | -- | @@ -161,8 +161,9 @@ 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](/providers/profiles) for profile endpoints, and to [Credential -Fields](/reference/policy-schema#credential-fields) for explicit bindings. +Profiles](/how-it-works/providers/profiles) for profile endpoints, and to +[Credential Fields](/how-it-works/policies/schema#credential-fields) for +explicit bindings. ## Examples @@ -174,9 +175,10 @@ 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](/sandboxes/manage-policies#replace-the-complete-policy). After you apply -a rule, [verify the change](/sandboxes/manage-policies#verify-a-change) before -testing traffic. +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 @@ -264,14 +266,14 @@ 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](/sandboxes/manage-policies#add-or-remove-network-access). +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](/get-started/tutorials/github-sandbox). +Agent](/tutorials/github-push-access). ### Allow PyPI Downloads @@ -350,9 +352,9 @@ openshell sandbox exec -n my-sandbox --no-login-shell -- \ 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](/sandboxes/policy-advisor) proposal. -Loopback, link-local, and unspecified addresses, including the cloud metadata -address `169.254.169.254`, are always blocked. +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 @@ -483,7 +485,7 @@ 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](/reference/policy-schema#graphql-rules). +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 @@ -528,10 +530,10 @@ 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](/reference/policy-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. +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 @@ -586,7 +588,7 @@ 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](/reference/policy-schema#inspection-fields). +[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 @@ -603,9 +605,9 @@ record. ## Next Steps -- Use [Manage Sandbox Policies](/sandboxes/manage-policies) to apply, verify, - and roll back policy changes. -- Use the [Policy Schema Reference](/reference/policy-schema) for protocol +- 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](/sandboxes/manage-policies#troubleshoot) when a request is - denied unexpectedly. +- 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 4f11267f92..d5800bf0ac 100644 --- a/docs/how-it-works/policies/overview.mdx +++ b/docs/how-it-works/policies/overview.mdx @@ -14,9 +14,9 @@ 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](/get-started/tutorials/first-network-policy). To create and change -policies, refer to [Manage Sandbox Policies](/sandboxes/manage-policies). +[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 @@ -34,18 +34,21 @@ specific time: 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](/sandboxes/manage-policies#how-changes-take-effect). +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](/sandboxes/network-rules) -explains how OpenShell evaluates rules and gives examples you can adapt. +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](/reference/policy-schema) describes every field in -each section, and [Supervisor Middleware](/extensibility/supervisor-middleware) -explains how middleware processes traffic. +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 @@ -57,7 +60,8 @@ order: 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](/reference/default-policy). +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. @@ -72,7 +76,8 @@ 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](/sandboxes/manage-policies#inspect-the-current-policy) shows both views. +Policy](/how-it-works/policies/manage-policies#inspect-the-current-policy) shows +both views. ### Global Policy @@ -82,18 +87,18 @@ 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](/sandboxes/manage-policies#apply-a-global-policy) for the +Policy](/how-it-works/policies/manage-policies#apply-a-global-policy) for the commands. ## Next Steps -- Use [Network Rules](/sandboxes/network-rules) to learn how OpenShell - evaluates network rules and to adapt examples for common services. -- Use [Manage Sandbox Policies](/sandboxes/manage-policies) to create, update, - verify, and roll back policies. -- Use the [Policy Prover](/reference/policy-prover) to check that a 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](/sandboxes/policy-advisor) to let an agent propose - narrow network rules for your review. -- Use the [Policy Schema Reference](/reference/policy-schema) for every field, - default, and validation rule. +- 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 74f80c2e56..95b48dcf15 100644 --- a/docs/how-it-works/policies/prover.mdx +++ b/docs/how-it-works/policies/prover.mdx @@ -26,7 +26,7 @@ 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](/sandboxes/policy-advisor). +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 @@ -34,7 +34,7 @@ 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](/sandboxes/policy-advisor). +refer to [Policy Advisor](/how-it-works/policies/advisor). ## Run a Boundary Check diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 5a9bc70966..71ee496839 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -34,7 +34,7 @@ network_middlewares: { ... } Startup fields take effect when a sandbox starts. Live fields can change while it runs. Refer to [How Changes Take -Effect](/sandboxes/manage-policies#how-changes-take-effect). +Effect](/how-it-works/policies/manage-policies#how-changes-take-effect). ## Filesystem Policy @@ -48,7 +48,7 @@ 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](/reference/default-policy#baseline-filesystem-paths). +Policy](/how-it-works/policies/default-policy#baseline-filesystem-paths). 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. @@ -103,7 +103,7 @@ process: 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](/sandboxes/network-rules). +for examples, refer to [Network Rules](/how-it-works/policies/network-rules). | Field | Type | Required | Description | |---|---|---|---| @@ -149,7 +149,7 @@ for `host.openshell.internal` can still reach services on the gateway host. | 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](/sandboxes/network-rules#connection-and-request-checks). | +| `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). | @@ -172,9 +172,9 @@ for `host.openshell.internal` can still reach services on the gateway host. 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](/providers/aws-sigv4) and [Static +AWS credentials. Refer to [AWS SigV4](/how-it-works/providers/aws) and [Static Credential Endpoint -Binding](/providers/profiles#understand-static-credential-endpoint-binding). +Binding](/how-it-works/providers/profiles#understand-static-credential-endpoint-binding). ```yaml showLineNumbers={false} network_policies: @@ -408,7 +408,8 @@ 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](/sandboxes/network-rules#binary-matching). +Refer to [Binary +Matching](/how-it-works/policies/network-rules#binary-matching). ## Network Middleware @@ -427,7 +428,8 @@ network rules allow, in ascending `order`. 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). +`tls: skip`. Refer to [Supervisor +Middleware](/extensibility/supervisor-middleware). ```yaml showLineNumbers={false} network_middlewares: 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/security/best-practices.mdx b/docs/security/best-practices.mdx index c3b4d0ef5a..7eb99a9515 100644 --- a/docs/security/best-practices.mdx +++ b/docs/security/best-practices.mdx @@ -296,10 +296,10 @@ The following patterns weaken security without providing meaningful benefit. ## Related Topics -- [Sandbox Policies](/sandboxes/policies) for how policies are evaluated and applied. -- [Manage Sandbox Policies](/sandboxes/manage-policies) for applying and iterating on sandbox policies. -- [Policy Schema](/reference/policy-schema) for the full field-by-field YAML reference. -- [Default Policy](/reference/default-policy) for the built-in default policy breakdown. -- [Gateway Auth](/reference/gateway-auth) for gateway authentication details. -- [Architecture](/about/how-it-works) for the system architecture. +- [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. +- [Architecture](/about/architecture) for the system architecture. - NemoClaw [Security Best Practices](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) for entrypoint-level controls (capability drops, PATH hardening, build toolchain removal), policy presets, provider trust tiers, and posture profiles. diff --git a/docs/tutorials/first-network-policy.mdx b/docs/tutorials/first-network-policy.mdx index 9aa9b3467d..bf966a458d 100644 --- a/docs/tutorials/first-network-policy.mdx +++ b/docs/tutorials/first-network-policy.mdx @@ -128,8 +128,8 @@ To see the complete policy, run `openshell policy get demo --base`. This tutorial uses `curl` and `read-only` access to keep things simple. When building policies for real workloads: - 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](/reference/policy-schema). -- To allow other services, such as PyPI, npm, or your internal APIs, adapt the examples in [Network Rules](/sandboxes/network-rules). +- 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). @@ -205,5 +205,5 @@ bash examples/sandbox-policy-quickstart/demo.sh ## Next Steps -- To understand how OpenShell evaluates network rules, refer to [Sandbox Policies](/sandboxes/policies). -- To walk through a full policy iteration with Claude Code, including diagnosing denials and applying fixes from outside the sandbox, refer to [GitHub Sandbox](/get-started/tutorials/github-sandbox). +- 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 5fcac5f319..53e32dc59d 100644 --- a/docs/tutorials/github-sandbox.mdx +++ b/docs/tutorials/github-sandbox.mdx @@ -223,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. From c19dd00c74f1f3470d7f508d6ef0d1900919bb0e Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 15:29:57 +0000 Subject: [PATCH 38/40] docs(policy): align native TCP guidance in security best practices Signed-off-by: Johnny Greco --- docs/security/best-practices.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/security/best-practices.mdx b/docs/security/best-practices.mdx index 7eb99a9515..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 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` and `tls: skip` endpoints relay the stream without inspecting payloads. Provider-credentialed endpoints reject uninspected modes 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 | 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, or `tls: skip` for proxy clients that must complete TLS with the upstream service. For WebSocket endpoints that must carry placeholder credentials in client text frames, add `websocket_credential_rewrite: true`. | +| 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`) From 82617519f989b7e98c721e7c656337a4f15325e6 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 16:03:34 +0000 Subject: [PATCH 39/40] docs(policy): restore policy.local and policy DNS details from main Signed-off-by: Johnny Greco --- docs/how-it-works/policies/advisor.mdx | 13 ++++++++----- docs/how-it-works/policies/network-rules.mdx | 6 ++++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/docs/how-it-works/policies/advisor.mdx b/docs/how-it-works/policies/advisor.mdx index 20cfda7d81..3190219c64 100644 --- a/docs/how-it-works/policies/advisor.mdx +++ b/docs/how-it-works/policies/advisor.mdx @@ -51,9 +51,12 @@ A proposal goes through these steps: 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. 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. +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: @@ -65,8 +68,8 @@ so the agent can find and use it: 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. - Refer to [Agent API](#agent-api). + 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. diff --git a/docs/how-it-works/policies/network-rules.mdx b/docs/how-it-works/policies/network-rules.mdx index 4a96241653..ccd409d7c8 100644 --- a/docs/how-it-works/policies/network-rules.mdx +++ b/docs/how-it-works/policies/network-rules.mdx @@ -597,8 +597,10 @@ 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. Clients must resolve the hostname -again before reconnecting. A client that caches an address indefinitely can fail +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. From 379f55a5f2fbc193e519993c37cfd3a2518ea0c3 Mon Sep 17 00:00:00 2001 From: Johnny Greco Date: Fri, 25 Sep 2026 16:27:20 +0000 Subject: [PATCH 40/40] docs(policy): state exact glob matching rules Signed-off-by: Johnny Greco --- docs/how-it-works/policies/network-rules.mdx | 18 +++++------ docs/how-it-works/policies/schema.mdx | 32 +++++++++++++++++--- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/docs/how-it-works/policies/network-rules.mdx b/docs/how-it-works/policies/network-rules.mdx index ccd409d7c8..5b3b366458 100644 --- a/docs/how-it-works/policies/network-rules.mdx +++ b/docs/how-it-works/policies/network-rules.mdx @@ -257,15 +257,15 @@ github_repository_api: - path: /usr/local/bin/gh ``` -In request paths, `*` matches within one path segment and `**` 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 +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 diff --git a/docs/how-it-works/policies/schema.mdx b/docs/how-it-works/policies/schema.mdx index 71ee496839..505a0a7838 100644 --- a/docs/how-it-works/policies/schema.mdx +++ b/docs/how-it-works/policies/schema.mdx @@ -446,12 +446,34 @@ network_middlewares: ## Matcher Semantics -| Matcher | Case-sensitive | Wildcards | +Glob patterns follow one set of rules. Each matcher splits values at a +separator: + +| Matcher | Separator | Case-sensitive | |---|---|---| -| Endpoint `host` and middleware hosts | No | `*` matches characters within one DNS label, and `**` matches one or more labels. | -| Binary `path` | Yes | `*` matches within one path segment, and `**` matches across segments. | -| REST and WebSocket `path` | Yes | `*` matches within one path segment, and `**` matches across segments. `?` matches one character, and bracket classes such as `[0-9]` are supported. `/repos/**` does not match `/repos`. | -| Query values and MCP tool names | Yes | `*` matches any characters except `.`, and `**` matches any characters. | +| 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