How to write OpenShell policies

An OpenShell policy is a YAML file with version: 1 and up to five more sections, and OpenShell denies anything the policy does not allow. Filesystem, Landlock and process settings are fixed when the sandbox starts. Network rules and middleware can change while it runs.
Version note: this guide is written against the OpenShell v0.1.2 docs, read on 2026-11-10. The commands and YAML were checked against those docs only and have not been run on a live gateway. A v0.1.3 tag also exists, but four of the six pages cited here differ in it, so this guide stays on v0.1.2 until it is checked against that release.
What each section controls
Each section takes effect at a different time, so the order you work in matters. Put filesystem and process settings in the file you pass when you create the sandbox, because changing them later means a new sandbox. Network rules make up most of a typical policy, and you can change them on a running sandbox without a restart. The policy overview lists where each section is enforced.
| Section | Controls | Enforced by | Takes effect |
|---|---|---|---|
filesystem_policy |
Paths that processes can read, or read and write. | Landlock LSM in the kernel | At sandbox startup |
landlock |
Whether the sandbox still starts if its filesystem rules cannot be applied. | Sandbox runtime | At sandbox startup |
process |
The user and group that processes run as. | Docker and Podman, at creation | 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 |
Extra inspection, transformation or blocking of traffic that rules allow. | Sandbox network proxy | While the sandbox runs |
Where the active policy comes from
Every sandbox runs under a policy, even if you pass none. OpenShell uses the first source that applies: a global policy set by a gateway administrator, then the sandbox’s saved policy, then a policy included in the sandbox image, and finally the restrictive default. An invalid image policy stops the workload from starting. OpenShell does not fall back to the default in that case, so check the image policy first when a new sandbox stays in provisioning.
Write the filesystem and Landlock settings
- Name every path the workload needs. A path must be absolute, contain no
.., and be 4096 bytes or less.read_writecannot contain/, and a policy can list at most 256 paths. Paths you do not list are inaccessible. - Set
include_workdiryourself. Whenfilesystem_policyis omitted, it defaults totrue. When the section is present, the default isfalse. - Choose a Landlock compatibility mode.
best_effortis the default.
filesystem_policy:
include_workdir: true
read_only: [/usr, /lib, /etc]
read_write: [/tmp]
landlock:
compatibility: best_effort
With best_effort, the sandbox runs without the filesystem rules and logs a high-severity finding when none of the listed paths can be applied or Landlock fails to enforce them. With hard_requirement, the sandbox fails to start. Both modes skip a listed path that is missing or that the workload cannot open, so check the sandbox log for the Landlock ruleset built event. It reports how many paths were applied and skipped. Either mode needs a kernel with Landlock ABI v3 or later.
OpenShell also adds baseline paths when the effective policy has at least one network rule. These are /usr, /lib, /etc, /app, /var/log, /proc and /dev/urandom as read-only, and /tmp and /dev/null as read-write. It adds a path only if your policy does not already list it, and it never changes the access of a path you list. If your workload needs /app, list it yourself. The default policy page has the full list.
Write network rules for each binary
A network rule allows every listed binary to reach every listed endpoint. The key of the entry is the rule’s name. This rule from the first-policy tutorial lets curl make read-only REST calls to the GitHub API:
network_policies:
github_api:
endpoints:
- host: api.github.com
port: 443
protocol: rest
enforcement: enforce
access: read-only
binaries:
- path: /usr/bin/curl
Rules multiply out. A rule with two binaries and two endpoints allows all four pairs. If one binary should reach only one host, put it in its own rule. Small rules with clear names are easier to audit, and they are easier to remove later with --remove-rule.
Match the binary that opens the connection
- List the executable that makes the connection, not the command you type.
pipis a Python script, so the process that connects to PyPI is the interpreter. List/usr/bin/python3.12, not/usr/bin/pip. - Use real paths, not symlinks. On Ubuntu 24.04,
/usr/bin/python3is a symlink to/usr/bin/python3.12. Runreadlink -fon the path inside the sandbox to find the real one. - Expect child processes to inherit the rule. A rule also applies to processes that a listed binary starts. An empty
binarieslist matches no binary and allows nothing. - Expect hash checks. OpenShell records a hash of each executable the first time it takes part in a connection. If the file at that path changes, later connections are denied.
Add protocol checks and enforcement
Without a protocol, OpenShell allows any method and path to the endpoint, although it still terminates TLS and rejects malformed HTTP. Set a protocol when the request itself determines the risk, as well as the destination:
protocol |
What OpenShell checks in each request |
|---|---|
rest |
HTTP method, path and query parameters |
websocket |
The upgrade request and each text message the client sends |
graphql |
Operation type, operation name and top-level fields |
mcp |
MCP method and tool name |
json-rpc |
JSON-RPC method name |
tcp |
No request rules. For clients that do not speak HTTP, such as databases |
| Omitted | No request rules |
network_policies:
github_repos:
endpoints:
- host: api.github.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
The enforcement field defaults to audit. An endpoint without enforcement: enforce logs violations and still allows the request, so set enforce explicitly on every rule you mean to block. Use access presets when a method class is enough: read-only allows GET, HEAD and OPTIONS, read-write adds POST, PUT and PATCH, and full allows everything. Use rules when you need specific paths. You cannot combine access and rules on one endpoint, and OpenShell rejects a policy that tries.
Deny rules always win. A matching deny rule blocks the request whatever the allow rules say, and it does not matter where the deny appears in the file. Also, read-only limits the HTTP method. It does not guarantee that an upstream GET has no side effect.
Apply changes to a running sandbox
Create the sandbox with your file, then change network rules with openshell policy update. That command edits only the network_policies section, so you do not need a complete file for a small change:
openshell sandbox create --name my-sandbox --policy ./policy.yaml
openshell policy update my-sandbox \
--rule-name github_readonly \
--binary /usr/bin/curl \
--add-endpoint api.github.com:443:read-only:rest:enforce \
--wait
--add-allow and --add-deny extend an existing rule, and --remove-rule deletes one. --dry-run previews a change, but a preview does not guarantee that the sandbox will accept it. The command fails if the --binary list does not match the rule’s binaries, so a new permission cannot reach a binary you did not name.
Use openshell policy set for what policy update cannot change, such as middleware, GraphQL, MCP and JSON-RPC rules, tls: skip, credential bindings and request signing. It replaces the whole policy, so start from the current base policy:
openshell policy get my-sandbox --base
openshell policy set my-sandbox --policy policy.yaml --wait
The policy get output starts with revision details, so save only the YAML before you edit it. Filesystem changes are the exception to live editing:
| Change | Effect on a running sandbox | What to do |
|---|---|---|
| Network rules | Loaded live. Connections opened under the old rules close. | Apply the change, then retry the request |
| Middleware | Built-in middleware can be added, removed or reconfigured | Apply with policy set |
| Added filesystem paths | Saved, but the running workload keeps its existing permissions | Recreate the sandbox |
Removed filesystem paths, or changed include_workdir, Landlock or process settings |
Rejected after the workload starts | Recreate the sandbox |
To recreate a sandbox, delete it and create it again with the edited file:
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. If the delete reports that cleanup is pending, wait until openshell sandbox get my-sandbox no longer finds the sandbox before you create the new one.
Verify the revision and roll back
A successful exit from --wait does not always mean your change is active. It can also mean the policy was unchanged, or that a newer change replaced yours before it loaded. With --wait, the command exits with status 1 if the sandbox rejects the revision, and with status 124 if the wait times out. Check the result:
openshell policy list my-sandbox
openshell policy get my-sandbox --full
The latest revision should show Loaded, and the effective policy should contain your change. Then send one request that should be allowed and one that should be blocked. A denial should come from OpenShell, as a policy_denied response or a sandbox log entry. Use openshell logs my-sandbox --since 10m --source sandbox to read the denial, and do not filter it with --level warn, which hides policy events.
| Error | Usual cause | What to check |
|---|---|---|
policy_denied |
No rule allows the connection, or a request rule blocks the request | The logged binary, host, port, method and path against the rule |
request_authority_mismatch |
The request’s host or port differs from the connection | The Host header, including any non-default port |
credential_endpoint_mismatch |
The network rule allowed the request, but the provider credential is not bound to this destination | The provider binding. Do not widen the network rule to fix it |
To roll back, print an earlier revision’s base policy, save it, and submit it as a new revision:
openshell policy get my-sandbox --rev 2 --base
openshell policy set my-sandbox --policy previous-base.yaml --wait
A rollback restores the policy only. It does not restore provider profiles, attachments, credentials or a global policy.
Checklist before you apply a policy
- Put filesystem, Landlock and process settings in the file you pass at creation, because changing them later means a new sandbox.
- List the interpreter or real binary path for every rule, not a symlink or the command you type.
- Set
enforcement: enforceon every endpoint you intend to block. The default isaudit. - Check each deny rule against every allow rule that could match the same request.
- List the paths your workload needs, such as
/app, rather than relying on baseline paths. - Read the effective policy with
--full, because attached providers add rules to it. - Confirm the latest revision shows
Loadedbefore you test.
What a sandbox policy does not control
A sandbox policy limits what the processes inside one sandbox can do. It does not decide who may change the policy, and it does not set the Pod around the sandbox. A global policy replaces each sandbox’s own policy while it is active, and the platform administrator role is required to set or delete it. On Kubernetes, the sandbox Pod’s image, privileges and credentials are set by Kubernetes settings, which need their own controls. The OpenShell on Kubernetes guide shows admission policies that check the Sandbox objects OpenShell creates. Kyverno is one tool for that layer.
The policy overview describes two tools for reviewing policy. The Policy Prover checks that a policy grants no more access than a boundary you define, and the Policy Advisor lets an agent propose narrow network rules for your review. For the wider picture, see how to sandbox AI agents on Kubernetes and OpenShell vs Agent Substrate vs Kubernetes Agent Sandbox, or the AI agent governance hub.
Frequently asked questions
Can I change filesystem rules on a running OpenShell sandbox?
No. Filesystem, Landlock and process settings take effect only when a sandbox starts. OpenShell rejects a change that removes a filesystem path, or that changes include_workdir, Landlock or process settings, after the workload starts. To change them, save the base policy, edit it, delete the sandbox and create a new one with --policy. Deleting a sandbox stops its processes and removes its state, so copy out anything you need first.
Why is my OpenShell policy denying a request that looks allowed?
Check the binary before the rule. OpenShell matches the executable that opens the connection, or one of its parent processes, not the command you typed. A Python script runs as its interpreter, so allow /usr/bin/python3.12 rather than pip. Then check the request. A rest endpoint with enforce returns policy_denied when no allow rule matches the method and path, or when a deny rule matches. Read the sandbox log without --level warn.
What is the difference between enforce and audit in an OpenShell policy?
The enforcement field sets what happens when a request breaks an inspected endpoint's rules. enforce blocks the request and returns a policy_denied response. audit allows the request and logs the violation, and it is the default when the field is omitted. Audit mode still rejects malformed requests and requests addressed to another host. Use audit to see what a new rule would block, then switch to enforce once the log shows only the violations you expect.
Does a deny rule override an allow rule in an OpenShell policy?
Yes. Network rules are not an ordered list. A matching deny rule takes precedence over any allow rule, wherever it appears in the file. An allow for GET /repos/** on a host, with a deny for GET /repos/private/** on the same host, port and binary, blocks requests under /repos/private/. Review every rule that could match a request, including rules that providers add to the effective policy.
What does best_effort do when Landlock rules cannot be applied?
With best_effort, the default, the sandbox runs without the filesystem rules and logs a high-severity finding. That happens when none of the listed paths can be applied, or when Landlock fails to enforce the rules. With hard_requirement, the sandbox fails to start instead. Both modes skip a listed path that is missing or that the workload cannot open, so check the Landlock ruleset built event in the sandbox log.