> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/NVIDIA/OpenShell/llms.txt
> Use this file to discover all available pages before exploring further.

# Write your first sandbox network policy

> Create a sandbox, observe default-deny networking in action, apply a fine-grained L7 read-only rule, and inspect the audit trail — all in under five minutes.

This tutorial shows how OpenShell's network policy system works in under five minutes. You create a sandbox, watch a request get blocked by the default-deny policy, apply a fine-grained L7 rule, and verify that reads are allowed while writes are blocked — all without restarting anything.

After completing this tutorial, you will understand:

* How default-deny networking blocks all outbound traffic from a sandbox.
* How to apply a network policy that grants read-only access to a specific API.
* How L7 enforcement distinguishes between HTTP methods such as GET and POST on the same endpoint.
* How to inspect deny logs for a complete audit trail.

## Prerequisites

* A working OpenShell installation. Complete the [Quickstart](/quickstart) before proceeding.
* Docker Desktop running on your machine.

<Tip>
  To run every step of this tutorial non-interactively, use the automated demo
  script in the
  [examples/sandbox-policy-quickstart](https://github.com/NVIDIA/OpenShell/blob/main/examples/sandbox-policy-quickstart)
  directory. It runs the full walkthrough in under a minute.

  ```bash theme={null}
  bash examples/sandbox-policy-quickstart/demo.sh
  ```
</Tip>

## Tutorial

<Steps>
  <Step title="Create a sandbox">
    Create a sandbox with no network policies. This gives you a clean environment to observe default-deny behavior.

    ```console theme={null}
    $ openshell sandbox create --name demo --keep --no-auto-providers
    ```

    `--keep` keeps the sandbox running after you exit so you can reconnect later. `--no-auto-providers` skips the provider setup prompt since this tutorial uses `curl` instead of an AI agent.

    You land in an interactive shell inside the sandbox:

    ```text theme={null}
    sandbox@demo:~$
    ```
  </Step>

  <Step title="Try to reach the GitHub API">
    With no network policy in place, every outbound connection is blocked. Test this by making a simple API call from inside the sandbox:

    ```console theme={null}
    $ curl -s https://api.github.com/zen
    ```

    `https://api.github.com/zen` is a lightweight, unauthenticated GitHub REST endpoint that returns a random aphorism. It requires no tokens or parameters, which makes it a convenient smoke-test target for verifying outbound HTTPS connectivity.

    The request fails. The sandbox proxy intercepted the HTTPS CONNECT request to `api.github.com:443` and rejected it because no network policy authorizes `curl` to reach that host.

    ```text theme={null}
    curl: (56) Received HTTP code 403 from proxy after CONNECT
    ```

    Exit the sandbox. The `--keep` flag keeps it running:

    ```console theme={null}
    $ exit
    ```
  </Step>

  <Step title="Check the deny log">
    Every denied connection produces a structured log entry. Query the sandbox logs from your host to confirm the denial and inspect the reason.

    ```console theme={null}
    $ openshell logs demo --since 5m
    ```

    You see a line like:

    ```text theme={null}
    action=deny dst_host=api.github.com dst_port=443 binary=/usr/bin/curl deny_reason="no matching network policy"
    ```

    Every denied connection is logged with the destination, the binary that attempted it, and the reason. Nothing gets out silently.
  </Step>

  <Step title="Create the policy file">
    To allow the sandbox to reach the GitHub API, define a network policy that grants read-only access. Create a file called `github-readonly.yaml` with the following content:

    ```yaml github-readonly.yaml theme={null}
    version: 1

    filesystem_policy:
      include_workdir: true
      read_only: [/usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log]
      read_write: [/sandbox, /tmp, /dev/null]
    landlock:
      compatibility: best_effort
    process:
      run_as_user: sandbox
      run_as_group: sandbox

    network_policies:
      github_api:
        name: github-api-readonly
        endpoints:
          - host: api.github.com
            port: 443
            protocol: rest
            enforcement: enforce
            access: read-only
        binaries:
          - { path: /usr/bin/curl }
    ```

    The `filesystem_policy`, `landlock`, and `process` sections preserve the default sandbox settings. This is required because `policy set` replaces the entire policy.

    The `network_policies` section is the key part: `curl` may 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.

    <Tip>
      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 Reference](/reference/policy-schema).
      * To allow additional endpoints, stack multiple policies in the same file for PyPI, npm, or your internal APIs. Refer to [Policies](/sandboxes/policies) for examples.
    </Tip>
  </Step>

  <Step title="Apply the policy">
    Apply the policy to the running sandbox:

    ```console theme={null}
    $ 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.
  </Step>

  <Step title="Verify GET requests are allowed">
    Reconnect to the sandbox and retry the same request to confirm that read access works.

    ```console theme={null}
    $ openshell sandbox connect demo
    ```

    Retry the request:

    ```console theme={null}
    $ curl -s https://api.github.com/zen
    ```

    ```text theme={null}
    Anything added dilutes everything else.
    ```

    The `read-only` preset allows GET requests through.
  </Step>

  <Step title="Try a write operation">
    The read-only preset allows GET but blocks mutating methods like POST, PUT, and DELETE. Test this by sending a POST request while still inside the sandbox:

    ```console theme={null}
    $ curl -s -X POST https://api.github.com/repos/octocat/hello-world/issues \
        -H "Content-Type: application/json" \
        -d '{"title":"oops"}'
    ```

    ```json theme={null}
    {"error":"policy_denied","policy":"github-api-readonly","detail":"POST /repos/octocat/hello-world/issues not permitted by policy"}
    ```

    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.

    Exit the sandbox:

    ```console theme={null}
    $ exit
    ```
  </Step>

  <Step title="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.

    ```console theme={null}
    $ openshell logs demo --level warn --since 5m
    ```

    ```text theme={null}
    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"
    ```

    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.

    <Tip>
      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.
    </Tip>
  </Step>

  <Step title="Clean up">
    Delete the sandbox to free resources. This stops all processes and purges any injected credentials.

    ```console theme={null}
    $ openshell sandbox delete demo
    ```
  </Step>
</Steps>

## What just happened

| State        | What happens                                                                  |
| ------------ | ----------------------------------------------------------------------------- |
| Default deny | All outbound traffic is blocked — nothing gets out without an explicit policy |
| L7 read-only | GET to `api.github.com` is allowed; POST is blocked at the HTTP method level  |
| Audit trail  | Every request is logged with the method, path, binary, and decision           |

The policy hot-reloads in seconds and gives you verifiable, fine-grained control over what your agent can access — without `--dangerously-skip-permissions`.

## What's next

<CardGroup cols={2}>
  <Card title="GitHub push access" icon="github" href="/tutorials/github-sandbox">
    Walk through a full policy iteration with Claude Code, including diagnosing denials and applying fixes from outside the sandbox.
  </Card>

  <Card title="Policy schema reference" icon="file-code" href="/reference/policy-schema">
    Learn about all available policy fields, access presets, and rule syntax.
  </Card>

  <Card title="Sandbox policies" icon="shield" href="/sandboxes/policies">
    See full policy examples for PyPI, npm, and internal APIs.
  </Card>

  <Card title="Policy audit mode" icon="eye" href="/sandboxes/policies">
    Use enforcement: audit to log violations without blocking, then promote to enforce.
  </Card>
</CardGroup>
