Skip to content

Commit c1b0e60

Browse files
committed
docs(policy): rewrite policy prover guide for clarity
Signed-off-by: Johnny Greco <jogreco@nvidia.com>
1 parent 8a1b7bb commit c1b0e60

2 files changed

Lines changed: 116 additions & 212 deletions

File tree

‎docs/reference/policy-prover.mdx‎

Lines changed: 115 additions & 211 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,51 @@
11
---
22
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
33
# SPDX-License-Identifier: Apache-2.0
4-
title: "Standalone Policy Prover"
4+
title: "Policy Prover"
55
sidebar-title: "Prover"
6-
description: "Install and use the standalone OpenShell policy prover to check a local candidate policy against a managed boundary."
7-
keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, Containment, CI"
6+
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."
7+
keywords: "Generative AI, Cybersecurity, Policy, Prover, Boundary, CI"
88
position: 8
99
---
1010

11-
`openshell-prover` checks whether the authority in a local candidate policy is
12-
contained within a local boundary policy. The command reads files from the host
13-
and does not connect to an OpenShell gateway.
11+
The policy prover checks that a policy grants no more access than a limit you
12+
set. You write the limit as a second policy, called the boundary. The prover
13+
determines whether everything allowed by the policy you are testing, called the
14+
candidate, is also allowed by the boundary. If the candidate allows something
15+
the boundary does not, the prover shows an example.
1416

15-
The candidate must be the fully composed effective policy after the proposed
16-
change, including any provider-contributed authority. The boundary is an
17-
operator-owned ceiling. It does not grant authority by itself.
17+
The prover checks only some parts of a policy: filesystem access, process
18+
identity, Landlock settings, network destinations, and REST methods and paths.
19+
If a policy uses anything else, such as GraphQL or MCP rules, the prover reports
20+
that it cannot check the policy instead of ignoring those rules.
21+
22+
Use the prover to check a policy before you apply it, for example in CI, so
23+
that no change grants more than your organization allows. The prover is a
24+
separate command, `openshell-prover`, that reads policy files on your machine.
25+
It does not connect to a gateway, and it does not apply or approve policies.
1826

1927
The [policy advisor](/sandboxes/policy-advisor#what-the-prover-checks) runs a
20-
separate prover check automatically on each proposal to find whether the
21-
proposal adds new access compared with the current policy. Use this command when
22-
you need to check a complete policy against a fixed boundary, for example in
23-
CI.
28+
different prover check on each proposal. That check compares the proposal with
29+
the sandbox's current policy instead of a boundary you write.
2430

2531
## Install the Prover
2632

27-
The standard OpenShell installer includes `openshell-prover`:
33+
The OpenShell installer includes `openshell-prover`:
2834

2935
```shell
3036
curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh
3137
openshell-prover --version
3238
```
3339

34-
The prover remains independent of the gateway at runtime. If you only need the
35-
standalone binary, use the artifacts listed in the
36-
[Support Matrix](/reference/support-matrix#standalone-policy-prover). These
37-
artifacts and `openshell-prover-checksums-sha256.txt` are attached to
38-
[OpenShell releases](https://github.com/NVIDIA/OpenShell/releases).
39-
40-
The release archive includes the solver linkage required by the executable.
41-
It does not require the main `openshell` command, a gateway configuration, or
42-
a separate Z3 installation.
40+
To install only the prover, download its archive from
41+
[OpenShell releases](https://github.com/NVIDIA/OpenShell/releases). The
42+
[Support Matrix](/reference/support-matrix#standalone-policy-prover) lists the
43+
available platforms. The prover runs on its own, without the `openshell` CLI, a
44+
gateway, or a separate Z3 installation.
4345

44-
## Check a Policy Boundary
46+
## Check a Policy
4547

46-
Create `boundary.yaml` with this boundary policy:
48+
Create `boundary.yaml`, a boundary that allows reading `/usr` and `/etc`:
4749

4850
```yaml
4951
version: 1
@@ -53,7 +55,7 @@ filesystem_policy:
5355
- /etc
5456
```
5557
56-
Create `candidate.yaml` with this contained candidate:
58+
Create `candidate.yaml`, a candidate that allows reading only `/usr`:
5759

5860
```yaml
5961
version: 1
@@ -62,16 +64,22 @@ filesystem_policy:
6264
- /usr
6365
```
6466

65-
Run the check:
67+
Check the candidate against the boundary:
6668

6769
```shell
6870
openshell-prover check candidate.yaml --boundary boundary.yaml
6971
```
7072

71-
A contained candidate prints `result: within_boundary` and exits with status `0`.
72-
To see an exceeding result and its counterexample, replace `candidate.yaml`
73-
with a policy that grants writes to `/tmp`. The boundary grants no write
74-
access:
73+
The candidate allows less than the boundary, so the check passes:
74+
75+
```text
76+
result: within_boundary
77+
coverage: domains=filesystem,network_l4,network_rest,process,landlock
78+
```
79+
80+
The `coverage` line lists the parts of the policy that the prover checked. Now
81+
change the candidate so that it also allows writing to `/tmp`, which the
82+
boundary does not allow:
7583

7684
```yaml
7785
version: 1
@@ -82,196 +90,92 @@ filesystem_policy:
8290
- /tmp
8391
```
8492

85-
```shell
86-
openshell-prover check candidate.yaml --boundary boundary.yaml
93+
Run the same command again. The check fails, and the `counterexample` line
94+
shows access that the candidate allows but the boundary does not:
95+
96+
```text
97+
result: exceeds_boundary
98+
coverage: domains=filesystem,network_l4,network_rest,process,landlock
99+
counterexample: filesystem write /tmp
87100
```
88101

89-
To check the current effective policy of an existing sandbox, export it without
90-
display metadata:
102+
### Check a Sandbox's Policy
103+
104+
To check the policy that a sandbox enforces, save its effective policy and use
105+
it as the candidate:
91106

92107
```shell
93108
openshell sandbox get my-sandbox --policy-only > candidate.yaml
94109
openshell-prover check candidate.yaml --boundary boundary.yaml
95110
```
96111

97-
This export includes provider-contributed rules. For a proposed change that is
98-
not active, supply the complete post-change effective policy. The standalone
99-
prover does not compose a base policy with provider rules.
100-
101-
Use JSON when a script consumes the result:
112+
The effective policy includes rules from attached providers, so the check covers
113+
everything the sandbox can reach. The prover does not add provider rules itself.
114+
To check a change before you apply it, give the prover the complete effective
115+
policy as it would be after the change.
102116

103-
```shell
104-
openshell-prover check candidate.yaml \
105-
--boundary boundary.yaml \
106-
--output json
107-
```
117+
## Read the Result
108118

109-
The JSON object includes the result, exit code, input paths, schema version,
110-
prover version, and modeled domains. `schema_version` versions the JSON contract,
111-
`prover_version` identifies the implementation that produced the result, and
112-
`coverage.domains` is the machine-readable declaration of modeled policy
113-
domains. An exceeding result also includes a typed counterexample. Automation
114-
should use `result` and `reason_code` instead of parsing the human-readable
115-
explanation.
116-
117-
| Field | When populated |
118-
|---|---|
119-
| `counterexample` | An object for `exceeds_boundary`; otherwise `null`. |
120-
| `reason_code` | A stable identifier for an emitted `error`, `unsupported`, or `inconclusive` result; otherwise `null`. |
121-
| `reason` | A human-readable explanation paired with `reason_code`; otherwise `null`. |
122-
123-
The `counterexample.domain` field selects one of these objects:
124-
125-
| Domain | Fields |
126-
|---|---|
127-
| `filesystem` | `access` (`read` or `write`) and `path`. |
128-
| `process` | `field` (`run_as_user` or `run_as_group`), `boundary`, and `candidate`. |
129-
| `landlock` | `boundary` and `candidate` compatibility modes. |
130-
| `network` | `binary`, `ancestor_binary`, `binary_identity_required`, `host`, `destination_ip`, `trusted_gateway`, `port`, `protocol`, `method`, and `path`. |
131-
132-
Network `binary` and `ancestor_binary` values are `null` when binary identity
133-
enforcement is disabled. `method` and `path` are `null` for L4 witnesses.
134-
`trusted_gateway: true` means the witness uses a recognized host-gateway alias
135-
with a runtime-provided trusted gateway binding; `false` uses ordinary
136-
destination validation.
137-
138-
The stable reason codes are `invalid_input`, `unsupported_policy_shape`,
139-
`unresolved_workdir`, `unresolved_binary_path`,
140-
`unresolved_filesystem_path`, `solver_timeout`, `solver_unknown`,
141-
`resource_limit`, `invalid_witness`, and `cancelled`.
142-
143-
The solver has a finite 10-second default budget. Set a different positive
144-
budget with an integer followed by `ms`, `s`, or `m`:
119+
The prover reports one of these results:
145120

146-
```shell
147-
openshell-prover check candidate.yaml \
148-
--boundary boundary.yaml \
149-
--timeout 30s \
150-
--output json
121+
| Result | Exit code | Meaning |
122+
|---|---|---|
123+
| `within_boundary` | `0` | The candidate allows nothing beyond the boundary in the parts of the policy that the prover checks. |
124+
| `exceeds_boundary` | `1` | The candidate allows access that the boundary does not. The counterexample shows one example. |
125+
| `error` | `2` | The prover could not run the check, for example because a file is missing or a policy is invalid. |
126+
| `unsupported` | `3` | A policy uses something the prover cannot check. The reason explains what. |
127+
| `inconclusive` | `3` | The prover could not finish, for example because it ran out of time or the policies are too large. |
128+
129+
Only `within_boundary` means that the check passed. In CI, treat every other
130+
result as a failure. For example, a candidate with a GraphQL rule returns:
131+
132+
```text
133+
result: unsupported
134+
coverage: domains=filesystem,network_l4,network_rest,process,landlock
135+
reason: candidate policy rule 'g' uses protocol 'graphql'; only L4 TCP and REST are modeled
151136
```
152137

153-
## Exit Codes
154-
155-
| Exit | Result | Meaning |
156-
|---|---|---|
157-
| `0` | `within_boundary` | Containment was established for the reported modeled domains. |
158-
| `1` | `exceeds_boundary` | The candidate exceeds the boundary; inspect the counterexample. |
159-
| `2` | `error` | Arguments, input files, policy syntax, or command execution prevented a valid check. |
160-
| `3` | `unsupported` or `inconclusive` | The model cannot soundly cover the policy shape, or the solver did not reach a determination. |
161-
| `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. |
162-
163-
Only exit `0` means the verification succeeded. Treat unsupported and
164-
inconclusive results as failures in CI.
165-
166-
## Interpretation and Limits
167-
168-
The containment check covers filesystem paths, process identity settings,
169-
Landlock compatibility requirements, L4 destination authority, and enforced
170-
REST method and path authority, including explicit REST denies. The result
171-
object reports the policy domains modeled by each check. Policies that use
172-
recognized authority outside that coverage return `unsupported` rather than
173-
silently ignoring it.
174-
175-
Both inputs use the same bounded YAML/JSON parser and authored policy schema as
176-
OpenShell. Unknown fields, duplicate keys, malformed field types, and unsupported
177-
managed `metadata` or `review` annotations return `error` with
178-
`reason_code: invalid_input` and exit `2`. Schema-valid controls outside the
179-
containment model return `unsupported` and exit `3`.
180-
181-
The prover applies aggregate limits across the candidate and boundary before
182-
semantic shape validation: 1,024 network rules, 4,096 endpoints, 4,096 binary
183-
selectors, 65,536 authored port entries, 4,096 `allowed_ips` entries, 16,384
184-
REST rules, 4 KiB per modeled pattern, and 1 MiB of modeled pattern text.
185-
Exceeding any limit returns `inconclusive` with `reason_code: resource_limit`.
186-
A cancellation already requested at preflight takes precedence over that
187-
result; otherwise a resource limit takes precedence over unsupported
188-
policy-shape diagnostics. This ordering keeps validation work bounded for
189-
checked-in CI inputs.
190-
191-
### Process and Landlock settings
192-
193-
Matching supported `run_as_user` and `run_as_group` values do not expand the
194-
configuration. Changing an explicit non-root identity to `root` or `0` returns
195-
`exceeds_boundary` with the field and both values. Other identity changes return
196-
`unsupported`; the command does not resolve accounts from a sandbox image.
197-
These comparisons assume consistent identity resolution and execution settings.
198-
They do not prove permission relationships between arbitrary Linux accounts.
199-
200-
Landlock `hard_requirement` may not become `best_effort`. That change returns
201-
`exceeds_boundary`. Keeping the same mode or strengthening it to
202-
`hard_requirement` passes this part of the check. This compares the requested
203-
requirement, not whether a target kernel successfully installed Landlock
204-
restrictions.
205-
206-
### Destination IP restrictions
207-
208-
The network action includes an IPv4 or IPv6 destination address. CIDR entries
209-
in `allowed_ips` are modeled together with host, port, executable, and REST
210-
restrictions; network counterexamples include `destination_ip`. An empty IP
211-
list follows the runtime's destination rules and is not a universal allowlist.
212-
The check does not resolve DNS on the host running the CLI.
213-
214-
Policies whose potentially overlapping endpoints select different destination
215-
restrictions remain unsupported because the runtime uses the selected
216-
endpoint's address filter. The prover conservatively treats wildcard host
217-
selectors on a shared port as potentially overlapping, including when their
218-
literal suffixes differ. CIDR unions within a supported endpoint are checked by
219-
the solver. Unsupported IP-literal wildcard selectors and ranges rejected by
220-
the runtime also cannot produce a successful proof.
221-
222-
Consumers must inspect `coverage.domains` and require every domain relevant to
223-
their authorization decision. A successful result applies only to those
224-
reported domains and the documented assumptions.
225-
226-
### Remaining limits
227-
228-
Network binary selectors, endpoint host and path selectors, and REST allow and
229-
deny method and path selectors must use ASCII literals in both the candidate and
230-
boundary. A non-ASCII literal in one of these fields returns `unsupported` with
231-
`reason_code: unsupported_policy_shape`, including when it appears only in a
232-
deny. Embedded NUL bytes in these fields are also unsupported. This is a
233-
prover-model limitation, not a general policy validation rule: filesystem paths
234-
and unrelated policy text retain their existing Unicode behavior. ASCII
235-
wildcard selectors still cover non-ASCII runtime values matched by the policy
236-
engine.
237-
238-
Containment means `Allowed(candidate)` is a subset of `Allowed(boundary)` under
239-
the reported model. It does not establish least privilege, automatic approval
240-
eligibility, semantic safety, or equivalence to a running sandbox's kernel
241-
state. In particular:
242-
243-
- The command does not fetch the current sandbox policy or compose provider
244-
rules. The caller must supply the effective candidate.
245-
- The command does not apply, approve, or persist a policy.
246-
- Environment-dependent authority, such as an unresolved image workdir,
247-
returns `unsupported` when the result depends on that missing context.
248-
- Binary comparisons that can change when an image resolves an exact selector
249-
through a symlink return `unsupported`. This covers exact candidate grants
250-
under boundary globs and exact boundary denies replaced by candidate globs.
251-
Use matching exact canonical paths when possible.
252-
- Policies with overlapping L4 and enforced REST endpoints return
253-
`unsupported` because inspection selection depends on the complete set of
254-
matching runtime endpoint configurations.
255-
- Network containment checks both supported runtime configurations: binary
256-
identity enforcement enabled and disabled. When enabled, grants and denies
257-
match the executable or an ancestor identity. Network counterexamples report
258-
the configuration and identities that expose the additional authority.
259-
- REST containment witnesses use canonical request methods and paths.
260-
- If a solver string cannot be decoded and validated faithfully, the command
261-
returns `inconclusive` with `reason_code: invalid_witness` instead of emitting
262-
a counterexample.
263-
- Both files are interpreted in the same sandbox filesystem namespace and
264-
mount model, with stable path resolution when enforcement rules are created.
265-
The CLI does not resolve sandbox paths against the host.
266-
- Filesystem containment supports removing grants and reducing write grants to
267-
read-only grants at matching paths. A boundary grant for `/` also covers other
268-
paths for the same access. Comparisons between different paths otherwise
269-
return `unsupported` with `reason_code: unresolved_filesystem_path`: a lexical
270-
child can resolve outside its parent through a symlink, and unrelated paths
271-
can resolve to the same object. This includes narrowing `/tmp` to `/tmp/cache`.
272-
If the boundary grants no access of the requested kind, adding that access
273-
returns `exceeds_boundary`.
274-
275-
Use the [Policy Schema Reference](/reference/policy-schema) for the full policy
276-
language. A successful prover result covers only the policy domains reported in
277-
its evidence.
138+
For scripts, add `--output json`. Read the `result` and `reason_code` fields
139+
instead of parsing the text output. `reason_code` is a stable identifier, such
140+
as `unsupported_policy_shape` or `solver_timeout`. Also check that
141+
`coverage.domains` includes every part of the policy that you rely on. Run
142+
`openshell-prover check --help` for all options, including `--timeout`, which
143+
changes the default 10-second time limit.
144+
145+
## What a Passing Result Means
146+
147+
A passing result means that the candidate allows nothing beyond the boundary in
148+
the parts of the policy that the prover checks. It does not mean that the policy
149+
is as narrow as it could be, that it is safe for a particular task, or that a
150+
running sandbox enforces it.
151+
152+
The prover reports `unsupported` instead of guessing when the answer depends on
153+
something it cannot see or does not check:
154+
155+
- Network rules other than connection rules and REST rules, such as WebSocket,
156+
GraphQL, MCP, and JSON-RPC rules.
157+
- Filesystem comparisons between different paths, such as narrowing `/tmp` to
158+
`/tmp/cache`. A symlink in the sandbox image could make the paths refer to
159+
different locations. Removing a path, or changing it from read-write to
160+
read-only, is supported.
161+
- Binary rules that compare an exact path with a glob, because a symlink in the
162+
sandbox image could change which executable the path refers to. Use matching
163+
exact paths when you can.
164+
- Settings that depend on the sandbox image, such as the working directory.
165+
- Endpoints for the same host and port that set different `allowed_ips`, or
166+
that mix connection-only and REST rules.
167+
- Process identity changes other than changing a non-root user or group to
168+
root. The prover reports a change to root as exceeding the boundary.
169+
- Host, path, method, or binary values that contain non-ASCII characters.
170+
171+
The prover also handles these cases in specific ways:
172+
173+
- Changing Landlock from `hard_requirement` to `best_effort` exceeds the
174+
boundary. The prover compares the settings, not what a kernel enforces.
175+
- The prover checks network rules both with and without binary identity
176+
enforcement, because a sandbox runtime can turn it off.
177+
- The prover does not resolve hostnames on your machine. It checks
178+
`allowed_ips` ranges together with hosts, ports, binaries, and REST rules.
179+
- Very large policies, for example with more than 1,024 network rules or 4,096
180+
endpoints across both files, return `inconclusive` with the `reason_code`
181+
`resource_limit`.

‎docs/sandboxes/policy-advisor.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,5 +288,5 @@ created from denials), and where the approval mode setting came from.
288288
- Use [Network Rules](/sandboxes/network-rules) and the
289289
[Policy Schema Reference](/reference/policy-schema) for rules that agents
290290
cannot propose.
291-
- Use the [Standalone Policy Prover](/reference/policy-prover) to check a
291+
- Use the [Policy Prover](/reference/policy-prover) to check a
292292
complete policy against a boundary you define.

0 commit comments

Comments
 (0)