Skip to content

All posts

KubernetesAI

How to write OpenShell policies

Ritesh Patel

Kubernetes: 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

  1. Name every path the workload needs. A path must be absolute, contain no .., and be 4096 bytes or less. read_write cannot contain /, and a policy can list at most 256 paths. Paths you do not list are inaccessible.
  2. Set include_workdir yourself. When filesystem_policy is omitted, it defaults to true. When the section is present, the default is false.
  3. Choose a Landlock compatibility mode. best_effort is 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

  1. List the executable that makes the connection, not the command you type. pip is a Python script, so the process that connects to PyPI is the interpreter. List /usr/bin/python3.12, not /usr/bin/pip.
  2. Use real paths, not symlinks. On Ubuntu 24.04, /usr/bin/python3 is a symlink to /usr/bin/python3.12. Run readlink -f on the path inside the sandbox to find the real one.
  3. Expect child processes to inherit the rule. A rule also applies to processes that a listed binary starts. An empty binaries list matches no binary and allows nothing.
  4. 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

  1. Put filesystem, Landlock and process settings in the file you pass at creation, because changing them later means a new sandbox.
  2. List the interpreter or real binary path for every rule, not a symlink or the command you type.
  3. Set enforcement: enforce on every endpoint you intend to block. The default is audit.
  4. Check each deny rule against every allow rule that could match the same request.
  5. List the paths your workload needs, such as /app, rather than relying on baseline paths.
  6. Read the effective policy with --full, because attached providers add rules to it.
  7. Confirm the latest revision shows Loaded before 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.

Ritesh Patel, Founder & VP Product, Nirmata

Ritesh Patel is a founder of Nirmata and leads its products. He writes about governing AI coding agents, MCP security and Kubernetes policy as code.

LinkedIn

Govern AI agents with AIControlsControl what AI agents do, and prove it.
ShareXLinkedIn

Stay up-to-date with the latest in AI, governance, and infrastructure

Subscribe to our free monthly newsletter to get industry news, DevOps tips, and articles right to your inbox on Kyverno, containerization, Kubernetes deployment & management and more.