### Index --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "NVIDIA OpenShell Developer Guide" description: "OpenShell is the safe, private runtime for autonomous AI agents. Run agents in sandboxed environments that protect your data, credentials, and infrastructure." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Privacy, Inference Routing" position: 1 --- import { BadgeLinks } from "./_components/BadgeLinks"; import { CommandTerminal } from "./_components/CommandTerminal"; NVIDIA OpenShell is the safe, private runtime for autonomous AI agents. It provides sandboxed execution environments that protect your data, credentials, and infrastructure. Agents run with exactly the permissions they need and nothing more, governed by declarative policies that prevent unauthorized file access, data exfiltration, and uncontrolled network activity. ## Get Started Install OpenShell and create your first sandbox in two commands. ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh openshell sandbox create -- claude ``` Refer to the [Quickstart](/get-started/quickstart) for more details. --- ## Explore
Learn about OpenShell and its capabilities. Concept Install OpenShell and create your first sandbox in two commands. Tutorial Hands-on walkthroughs from first sandbox to custom policies. Concept Deploy gateways, create sandboxes, configure policies, providers, and community images for your AI agents. Concept Keep inference traffic private by routing API calls to local or self-hosted backends. Concept Understand sandbox logs, access them with the CLI and TUI, and export OCSF JSON records. How-To Policy schema, environment variables, and default policy details. Reference Every configurable security control, its default, and the risk of changing it. Concept
--- This software automatically retrieves, accesses or interacts with external materials. Those retrieved materials are not distributed with this software and are governed solely by separate terms, conditions and licenses. You are solely responsible for finding, reviewing and complying with all applicable terms, conditions, and licenses, and for verifying the security, integrity and suitability of any retrieved materials for your specific use case. This software is provided "AS IS", without warranty of any kind. The author makes no representations or warranties regarding any retrieved materials, and assumes no liability for any losses, damages, liabilities or legal consequences from your use or inability to use this software or any retrieved materials. Use this software and the retrieved materials at your own risk. --- ### About/Container Gateway --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Running the Gateway as a Container" sidebar-title: "Container Gateway" description: "Run the OpenShell gateway using docker run or docker-compose without the installer." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Docker, Podman, docker-compose, container, immutable OS, bootc, rpm-ostree" position: 4 --- Use this approach when you want to run the OpenShell gateway as a container instead of installing it with the system package manager. This is useful on immutable OS distributions (Fedora CoreOS, bootc-based images, Silverblue) where the standard installer is not appropriate, or anywhere you prefer a container-first workflow. The gateway image is published at `ghcr.io/nvidia/openshell/gateway`. ## Prerequisites for the Docker Driver When the gateway runs as a container and creates Docker-backed sandboxes, the gateway container communicates with the host Docker daemon via the mounted socket. This requires three things beyond a basic `docker run`: 1. **Docker socket access.** The gateway process must be able to read and write the Docker socket. Add the `docker` group (or the GID of `/var/run/docker.sock`) so the socket is accessible without running as root. 2. **gRPC endpoint.** Sandbox containers call back to the gateway over the `OPENSHELL_GRPC_ENDPOINT` address. The Docker driver substitutes `host.openshell.internal` as the host and the gateway's own bind port as the port — only the **scheme** (`http` or `https`) is preserved. Use `http://host.openshell.internal:8080` when TLS is disabled and `https://host.openshell.internal:8080` when mTLS is enabled. The docker driver automatically binds the gateway to the bridge network interface so sandbox containers can reach it — you do not need to expose the port on `0.0.0.0`. 3. **Supervisor binary on the host.** The gateway bind-mounts the `openshell-sandbox` supervisor binary into each sandbox container. Because bind-mount paths are resolved by the host Docker daemon (not inside the gateway container), the binary must exist at a path on the **host** filesystem and be mounted at the **same absolute path** inside the gateway container. That way the path the gateway records internally matches what Docker can find on the host when it creates sandbox containers. ## Quick Start Extract the supervisor binary to the host once, then start the gateway: ```shell mkdir -p ~/openshell/supervisor docker create --name tmp-supervisor ghcr.io/nvidia/openshell/supervisor:latest docker cp tmp-supervisor:/openshell-sandbox ~/openshell/supervisor/openshell-sandbox docker rm tmp-supervisor chmod +x ~/openshell/supervisor/openshell-sandbox ``` Start the gateway: ```shell docker run -d \ --name openshell-gateway \ --restart unless-stopped \ --group-add docker \ -p 127.0.0.1:8080:8080 \ -v openshell-state:/var/openshell \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/openshell/supervisor/openshell-sandbox:~/openshell/supervisor/openshell-sandbox:ro \ -e OPENSHELL_DRIVERS=docker \ -e OPENSHELL_GRPC_ENDPOINT=http://host.openshell.internal:8080 \ -e OPENSHELL_DOCKER_SUPERVISOR_BIN=~/openshell/supervisor/openshell-sandbox \ -e OPENSHELL_DB_URL=sqlite:/var/openshell/openshell.db \ -e OPENSHELL_DISABLE_TLS=true \ ghcr.io/nvidia/openshell/gateway:latest ``` The volume mount uses `~/openshell/supervisor/openshell-sandbox` for both the host and container paths. The shell expands `~` in both halves before passing the argument to Docker, so both sides resolve to the same absolute path (e.g., `/home/user/openshell/supervisor/openshell-sandbox`). This satisfies the same-path requirement so the host Docker daemon can find the binary when creating sandbox containers. Register the gateway with the CLI. If running on the same machine, use `--local`: ```shell openshell gateway add http://127.0.0.1:8080 --local --name local ``` If registering from a different machine on the same network, use the host IP and `--remote`: ```shell openshell gateway add http://HOST_IP:8080 --remote --name remote ``` Confirm the CLI can reach the gateway: ```shell openshell status ``` Disabling TLS removes authentication. This example binds to `127.0.0.1` so only local connections are accepted. To accept remote connections, enable mTLS or restrict access with a firewall rule. ## Full mTLS Setup To run the gateway with mutual TLS, generate the PKI bundle first, then start the gateway with the cert paths configured. Bootstrap the PKI into a local state directory: ```shell mkdir -p ~/.local/state/openshell/tls docker run --rm \ -v "$HOME/.local/state/openshell:/home/openshell/.local/state/openshell" \ -v "$HOME/.config/openshell:/home/openshell/.config/openshell" \ ghcr.io/nvidia/openshell/gateway:latest \ generate-certs \ --output-dir /home/openshell/.local/state/openshell/tls \ --server-san host.openshell.internal ``` This writes the server and client certificates under `~/.local/state/openshell/tls/`, writes sandbox JWT signing keys under `~/.local/state/openshell/tls/jwt/`, and copies the client bundle to `~/.config/openshell/gateways/openshell/mtls/` so the CLI picks it up automatically. Start the gateway with mTLS enabled: ```shell docker run -d \ --name openshell-gateway \ --restart unless-stopped \ --group-add docker \ -p 127.0.0.1:8080:8080 \ -v "$HOME/.local/state/openshell:/home/openshell/.local/state/openshell" \ -v /var/run/docker.sock:/var/run/docker.sock \ -v ~/openshell/supervisor/openshell-sandbox:~/openshell/supervisor/openshell-sandbox:ro \ -e OPENSHELL_DRIVERS=docker \ -e OPENSHELL_GRPC_ENDPOINT=https://127.0.0.1:8080 \ -e OPENSHELL_DOCKER_SUPERVISOR_BIN=~/openshell/supervisor/openshell-sandbox \ -e OPENSHELL_DB_URL=sqlite:/home/openshell/.local/state/openshell/openshell.db \ -e OPENSHELL_LOCAL_TLS_DIR=/home/openshell/.local/state/openshell/tls \ -e OPENSHELL_TLS_CERT=/home/openshell/.local/state/openshell/tls/server/tls.crt \ -e OPENSHELL_TLS_KEY=/home/openshell/.local/state/openshell/tls/server/tls.key \ -e OPENSHELL_TLS_CLIENT_CA=/home/openshell/.local/state/openshell/tls/ca.crt \ -e OPENSHELL_ENABLE_MTLS_AUTH=true \ -e OPENSHELL_DOCKER_TLS_CA=/home/openshell/.local/state/openshell/tls/ca.crt \ -e OPENSHELL_DOCKER_TLS_CERT=/home/openshell/.local/state/openshell/tls/client/tls.crt \ -e OPENSHELL_DOCKER_TLS_KEY=/home/openshell/.local/state/openshell/tls/client/tls.key \ ghcr.io/nvidia/openshell/gateway:latest ``` Register the gateway with mTLS: ```shell openshell gateway add https://127.0.0.1:8080 --local --name local ``` ## Docker Compose The [`deploy/docker/`](https://github.com/NVIDIA/OpenShell/tree/main/deploy/docker) directory in the repository contains a production-ready Compose setup with full inline documentation: | File | Purpose | |---|---| | `docker-compose.yml` | Gateway service, volumes, and environment variables | | `gateway.toml` | TOML configuration mounted into the container | Clone or copy those files, then start the gateway: ```shell docker compose -f deploy/docker/docker-compose.yml up -d ``` Register the gateway with the CLI. If registering from the same machine: ```shell openshell gateway add http://127.0.0.1:8080 --local --name local ``` If registering from a different machine on the same network, replace `HOST_IP` with the machine's LAN address: ```shell openshell gateway add http://HOST_IP:8080 --remote --name remote ``` ## Using Podman Replace `docker` with `podman` in the commands above. Mount the Podman socket instead of the Docker socket and set the driver to `podman`: ```shell podman run -d \ --name openshell-gateway \ -p 127.0.0.1:8080:8080 \ -v openshell-state:/var/openshell \ -v "$XDG_RUNTIME_DIR/podman/podman.sock:/var/run/podman.sock" \ -e OPENSHELL_DRIVERS=podman \ -e OPENSHELL_PODMAN_SOCKET=/var/run/podman.sock \ -e OPENSHELL_DB_URL=sqlite:/var/openshell/openshell.db \ -e OPENSHELL_DISABLE_TLS=true \ ghcr.io/nvidia/openshell/gateway:latest ``` ## Next Steps - To create your first sandbox, refer to the [Quickstart](/get-started/quickstart). - To control what the agent can access, refer to [Policies](/sandboxes/policies). - For environment variable reference, refer to [Sandbox Compute Drivers](/reference/sandbox-compute-drivers). --- ### About/How It Works --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "How OpenShell Works" sidebar-title: "How It Works" description: "Understand the OpenShell architecture, runtime boundaries, gateways, sandboxes, and ecosystem integration points." keywords: "Generative AI, Cybersecurity, AI Agents, Architecture, Gateway, Sandbox, Inference Routing" position: 2 --- OpenShell is built around three stable runtime components: the **CLI**, the **Gateway**, and the **Supervisor**. The CLI, SDK, and TUI provide user-facing access. The gateway is the control plane: it owns API access, state, policy and settings delivery, provider and inference configuration, and relay coordination. The supervisor runs inside every sandbox workload and is the local security boundary. It launches the agent as a restricted child process and enforces policy where process identity, filesystem access, network egress, and runtime credentials are visible. Infrastructure-specific work sits behind integration boundaries. Compute, credentials, control-plane identity, and sandbox identity each have a driver or adapter boundary so OpenShell can integrate with native runtimes, secret stores, identity providers, and workload identity systems without moving those concerns into the core gateway or sandbox model. ```mermaid flowchart TB subgraph UI["User interfaces"] CLI["CLI"] SDK["SDK"] TUI["TUI"] end subgraph CP["Control plane"] GW["Gateway"] DB[("Entity persistence")] DRIVERS["Compute, credentials, and identity drivers"] end subgraph INFRA["Integrated infrastructure"] RUNTIME["Docker, Podman, Kubernetes, or VM"] SECRETSTORE["Secret stores"] IDP["Identity providers"] WORKLOADID["Workload identity"] end subgraph DP["Sandbox data plane"] SUP["Supervisor"] AGENT["Restricted agent process"] PROXY["Policy proxy"] POLICY["OPA policy engine"] ROUTER["Inference router"] end CLI -->|"gRPC / HTTP"| GW SDK -->|"gRPC / HTTP"| GW TUI -->|"gRPC / HTTP"| GW GW --> DB GW --> DRIVERS DRIVERS --> RUNTIME DRIVERS --> SECRETSTORE DRIVERS --> IDP DRIVERS --> WORKLOADID RUNTIME -->|"provisions workload"| SUP SUP -->|"control, config, logs, relay"| GW SUP -->|"spawn and restrict"| AGENT AGENT -->|"ordinary egress"| PROXY PROXY -->|"evaluate"| POLICY PROXY -->|"allowed traffic"| EXT["External services"] PROXY -->|"inference.local"| ROUTER ROUTER -->|"managed inference"| MODEL["Inference backends"] ``` ## Deployment Models OpenShell can run on a single local machine or in a remote Kubernetes cluster. The CLI workflow stays the same: users point the CLI, SDK, or TUI at a gateway, and the gateway provisions sandboxes through its configured compute driver. | Deployment | How it works | Best for | |---|---|---| | Local machine | The gateway runs on the user's workstation or a nearby development host and creates sandboxes with Docker, Podman, or a VM runtime. The supervisor inside each sandbox connects back to that local gateway. | Individual development, local agent experiments, and private workstation workflows. | | Remote Kubernetes cluster | The gateway runs as a cluster service and creates sandbox pods in the configured namespace. Supervisors connect outbound to the gateway endpoint, so clients do not need direct pod access. | Shared teams, centrally managed policy, remote compute, GPUs, and production-like environments. | This deployment split keeps the runtime model consistent. Local deployments use the host's container or VM runtime as the integrated infrastructure. Kubernetes deployments use the cluster scheduler, networking, secrets, identity, and GPU device plugins without changing the gateway and sandbox contract. ## Core Components | Component | Boundary | |---|---| | [Sandboxes](/sandboxes/manage-sandboxes) | Data-plane workloads that run the supervisor, launch restricted agent processes, apply local isolation, push logs, and maintain the gateway session. | | [Gateways](/sandboxes/manage-gateways) | Authenticated control plane that owns API access, durable state, sandbox lifecycle, settings delivery, authorization, and relay coordination. | | [Providers](/sandboxes/manage-providers) | Credential and provider records that map logical agent needs to platform or user-managed secrets without exposing raw credentials to the agent process. | | [Policies](/sandboxes/policies) | Declarative controls for filesystem access, process identity, network egress, L7 rules, credential injection, and runtime policy updates. | | [Inference Routing](/sandboxes/inference-routing) | Managed `https://inference.local` path that routes model traffic to configured backends while keeping provider credentials outside the sandbox. | ## Gateways and Sandboxes The gateway and sandbox split control-plane authority from runtime enforcement. The gateway owns durable platform state: sandboxes, policy revisions, runtime settings, provider records, inference configuration, session records, and authorization decisions. A sandbox owns the local execution boundary: process identity, filesystem access, network egress, credential injection, local logs, and the agent child process. The relationship is supervisor initiated. Each sandbox supervisor connects outbound to a known gateway endpoint, authenticates as a sandbox workload, and keeps a live session open for control traffic and relays. This avoids requiring every compute driver to solve gateway-to-sandbox reachability through pod IPs, bridge networks, port mappings, NAT traversal, or custom tunnels. The gateway delivers desired state. The supervisor applies it locally, keeps last-known-good config when refresh fails, and leaves static isolation controls in place until the sandbox is recreated. Live operations such as config refresh, policy updates, credential delivery, log push, connect, exec, file sync, and relay setup use the same authenticated gateway-supervisor relationship. ## Supervisor Protection Layers The supervisor is the sandbox-local enforcement component. It starts before the agent process, prepares the sandbox runtime, fetches gateway configuration, and then launches the agent under the active policy. | Protection layer | Supervisor responsibility | |---|---| | Process | Drops privileges, applies process identity rules, disables privilege escalation paths, and starts the agent as a restricted child process. | | Filesystem | Applies filesystem policy before the agent starts so undeclared paths are inaccessible and declared paths are read-only or read-write as configured. | | Network | Routes ordinary egress through the policy proxy so destination, port, binary identity, and L7 request rules can be evaluated before traffic leaves the sandbox. | | Credentials | Receives credential material from the gateway and injects it only through configured policy paths or request-time proxy rules. | | Inference | Intercepts `https://inference.local` and forwards model traffic through the configured inference route instead of exposing provider credentials to the agent. | | Observability | Emits local security and lifecycle logs, pushes sandbox logs to the gateway, and keeps relay endpoints available for connect, exec, and file transfer operations. | Static controls such as filesystem and process isolation are established at sandbox start and require sandbox recreation to change. Dynamic controls such as network policy, credential delivery, and inference routing can refresh over the live gateway-supervisor session. ## Ecosystem Integration OpenShell integrates with infrastructure ecosystems instead of replacing them. Runtimes, schedulers, secret stores, identity providers, workload identity systems, image pipelines, storage, and GPU or device exposure remain owned by the platforms that provide them. The gateway owns OpenShell control-plane semantics: sandbox state, lifecycle ordering, policy and settings resolution, credential mapping, authorization, inference configuration, and relay coordination. Drivers translate those semantics into platform-native operations. The supervisor owns OpenShell sandbox semantics. Filesystem policy, process privilege reduction, network proxying, inference interception, credential injection, security logging, and gateway relay behavior stay consistent across Docker, Podman, Kubernetes, VM-backed sandboxes, and future integrations. --- ### About/Installation --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Installation" sidebar-title: "Installation" description: "Install OpenShell, choose a compute driver, and connect to a gateway." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Installation, Setup, Gateway, Docker, Podman, MicroVM, Kubernetes" position: 3 --- ## Install OpenShell Install OpenShell with a single command: ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` The script detects your operating system and installs the OpenShell CLI and gateway with your native package manager. It then starts the local gateway server so you can begin creating sandboxes. You can also download release artifacts directly from the [OpenShell GitHub Releases](https://github.com/NVIDIA/OpenShell/releases) page. Use `openshell status` to confirm the CLI can reach the gateway. ## Supported Compute Drivers OpenShell supports several local compute drivers. Package-managed gateways leave the driver unset by default so the gateway can auto-detect an available driver. Set `compute_drivers` in the gateway TOML when you need to pin a specific driver. | Compute Driver | How It Is Configured | System Requirements | |---|---|---| | Podman | The gateway is configured to create rootless Podman containers through the Podman API socket. | Linux with Podman 5.x, cgroups v2, rootless networking, and an active Podman user socket. | | Docker | The gateway is configured to create containers through Docker Desktop or Docker Engine. | Docker Desktop or Docker Engine 28.0 or later on the gateway host. | | MicroVM | The gateway is configured to create VM-backed sandboxes. | Host virtualization support. MicroVM uses Hypervisor.framework on macOS, KVM on Linux, and QEMU for GPU-backed sandboxes on Linux. | For detailed driver behavior, refer to [Sandbox Compute Drivers](/reference/sandbox-compute-drivers). For gateway and sandbox operations, refer to [Gateways](/sandboxes/manage-gateways) and [Sandboxes](/sandboxes/manage-sandboxes). ## macOS On macOS, the install script uses Homebrew. The Homebrew package installs the `openshell` CLI, the gateway binary, and a Homebrew-managed gateway service. The Homebrew service uses the gateway's built-in `127.0.0.1:17670` listener and generates a local mTLS bundle on install. The installer registers `https://localhost:17670` with the CLI so TLS uses a DNS name covered by the generated certificate. The formula creates a Homebrew prefix config, such as `/opt/homebrew/var/openshell/gateway.toml`, without overriding `bind_address`. Docker Desktop and Podman Machine reuse the primary listener for sandbox callbacks when they can reach it. The gateway reads `~/.config/openshell/gateway.toml` instead when that file exists. Homebrew preserves user-edited prefix and user configs during upgrades; it removes the IPv6 bind only from an unchanged config generated by the affected formula. The CLI reads the client bundle from `~/.config/openshell/gateways/openshell/mtls/`. The installer starts the service for you. Use Homebrew service commands when you need to inspect, restart, or stop the gateway service: ```shell brew services list brew services restart openshell ``` ## Linux On Fedora and RHEL, the install script uses RPM packages. The RPM installs the `openshell` CLI, the `openshell-gateway` daemon, and a systemd user service. On Debian and Ubuntu, the install script uses a Debian package. The Debian package installs the `openshell` CLI, the `openshell-gateway` daemon, VM sandbox support, and a systemd user service. Linux packages require glibc 2.28 or newer. The installer checks libc before downloading packages and exits with an error on older glibc versions, Alpine, musl-based distributions, or unknown libc environments. The Linux user service listens on `https://127.0.0.1:17670`, starts from built-in defaults, and generates a local mTLS bundle before the gateway starts. Create `~/.config/openshell/gateway.toml` only when you need to override those defaults. The CLI reads the client bundle from `~/.config/openshell/gateways/openshell/mtls/`. The installer starts the service for you. Use systemd user commands when you need to inspect, restart, or stop the gateway service: ```shell systemctl --user status openshell-gateway systemctl --user restart openshell-gateway journalctl --user -u openshell-gateway -f ``` To keep the user service running after logout, enable linger: ```shell sudo loginctl enable-linger $USER ``` ## Snap Install the OpenShell snap from the Snap Store: ```shell sudo snap install openshell ``` The snap defines two apps: the `openshell` CLI and the `openshell.gateway` systemd service. The gateway listens on `https://127.0.0.1:17670` and stores its database at `$SNAP_COMMON/gateway.db` (typically `/var/snap/openshell/common/gateway.db`). Create `$SNAP_COMMON/gateway.toml` when you need to override gateway settings. The snap CLI stores per-user config, data, and state under `$SNAP_USER_COMMON`, typically `~/snap/openshell/common`. Gateway registrations live under `$SNAP_USER_COMMON/.config/openshell/gateways/` instead of `~/.config/openshell/gateways/`. ### Snap store installs When installing from the Snap Store, snapd automatically connects the `home`, `network`, and `network-bind` plugs. The `docker` plug still requires manual connection: ```shell sudo snap connect openshell:docker docker:docker-daemon ``` The snap declares `default-provider: docker` on the Docker plug so snapd will offer to install the Docker snap, but the connection itself must be made manually. ### Locally built snap packages When installing a locally built `.snap` file, no plugs are connected by default: ```shell sudo snap install ./openshell_*.snap --dangerous sudo snap connect openshell:home sudo snap connect openshell:network sudo snap connect openshell:network-bind sudo snap connect openshell:docker docker:docker-daemon sudo snap connect openshell:log-observe sudo snap connect openshell:system-observe ``` The `log-observe` and `system-observe` plugs are needed for the gateway service to read logs and inspect system processes. The `docker` plug requires the `docker:docker-daemon` slot from the Docker snap and does not work with system-installed Docker. ### Gateway service The gateway runs as a snap daemon with `refresh-mode: endure`, meaning snapd will not restart it during snap refreshes. This prevents the gateway from killing active sandbox sessions mid-refresh. Restart the service manually after a snap refresh when you need the updated binary: ```shell sudo systemctl restart snap.openshell.gateway ``` ## Kubernetes Kubernetes deployments use the OpenShell Helm chart. For step-by-step installation, refer to [Kubernetes Setup](/kubernetes/setup). For chart values and packaging details, refer to the [Helm chart README](https://github.com/NVIDIA/OpenShell/blob/main/deploy/helm/openshell/README.md). ## Next Steps - To create your first sandbox, refer to the [Quickstart](/get-started/quickstart). - To run the gateway as a container without the installer, refer to [Running the Gateway as a Container](/about/container-gateway). - To register, select, and inspect gateways, refer to [Gateways](/sandboxes/manage-gateways). - To supply API keys or tokens, refer to [Manage Providers](/sandboxes/manage-providers). - To control what the agent can access, refer to [Policies](/sandboxes/policies). --- ### About/Overview --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Overview of NVIDIA OpenShell" sidebar-title: "Overview" description: "OpenShell is the safe, private runtime for autonomous AI agents. Run agents in sandboxed environments that protect your data, credentials, and infrastructure." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Security, Privacy, Inference Routing" position: 1 --- NVIDIA OpenShell is an open-source runtime for executing autonomous AI agents in sandboxed environments with kernel-level isolation. It combines sandbox runtime controls and a declarative YAML policy so teams can run agents without giving them unrestricted access to local files, credentials, and external networks. ## Why OpenShell Exists AI agents are most useful when they can read files, install packages, call APIs, and use credentials. That same access can create material risk. OpenShell is designed for this tradeoff: preserve agent capability while enforcing explicit controls over what the agent can access. ## Common Risks and Controls The table below summarizes common failure modes and how OpenShell mitigates them. | Threat | Without controls | With OpenShell | |---|---|---| | Data exfiltration | Agent uploads source code or internal files to unauthorized endpoints. | Network policies allow only approved destinations; other outbound traffic is denied. | | Credential theft | Agent reads local secrets such as SSH keys or cloud credentials. | Filesystem restrictions (Landlock) confine access to declared paths only. | | Unauthorized API usage | Agent sends prompts or data to unapproved model providers. | Privacy routing and network policies control where inference traffic can go. | | Privilege escalation | Agent attempts `sudo`, setuid paths, or dangerous syscall behavior. | Unprivileged process identity and seccomp restrictions block escalation paths. | ## Protection Layers at a Glance OpenShell applies defense in depth across the following policy domains. | Layer | What it protects | When it applies | |---|---|---| | Filesystem | Prevents reads/writes outside allowed paths. | Locked at sandbox creation. | | Network | Blocks unauthorized outbound connections. | Hot-reloadable at runtime. | | Process | Blocks privilege escalation and dangerous syscalls. | Locked at sandbox creation. | | Inference | Reroutes model API calls to controlled backends. | Hot-reloadable at runtime. | For details, refer to [Customize Sandbox Policies](/sandboxes/policies) and [Default Policy](/reference/default-policy). ## Common Use Cases OpenShell supports a range of agent deployment patterns. | Use Case | Description | |-----------------------------|----------------------------------------------------------------------------------------------------------| | Secure coding agents | Run Claude Code, OpenCode, Codex, or GitHub Copilot CLI with constrained file and network access. | | Private enterprise development | Route inference to self-hosted or private backends while keeping sensitive context under your control. | | Compliance and audit | Treat policy YAML as version-controlled security controls that can be reviewed and audited. | | Reusable environments | Use community sandbox images or bring your own containerized runtime. | ## Next Steps Explore these topics to go deeper: - To understand the runtime architecture, refer to [How OpenShell Works](/about/how-it-works). - To install the CLI and create your first sandbox, refer to the [Quickstart](/get-started/quickstart). - To learn how OpenShell enforces policy controls across protection layers, refer to [Customize Sandbox Policies](/sandboxes/policies). --- ### About/Release Notes --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "NVIDIA OpenShell Release Notes" sidebar-title: "Release Notes" description: "Track the latest changes and improvements to NVIDIA OpenShell." keywords: "Generative AI, Cybersecurity, Release Notes, Changelog, AI Agents" position: 6 --- NVIDIA OpenShell follows a frequent release cadence. Use the following GitHub resources directly. | Resource | Description | |---|---| | [Releases](https://github.com/NVIDIA/OpenShell/releases) | Versioned release notes and downloadable assets. | | [Release comparison](https://github.com/NVIDIA/OpenShell/compare) | Diff between any two tags or branches. | | [Merged pull requests](https://github.com/NVIDIA/OpenShell/pulls?q=is%3Apr+is%3Amerged) | Individual changes with review discussion. | | [Commit history](https://github.com/NVIDIA/OpenShell/commits/main) | Full commit log on `main`. | --- ### About/Supported Agents --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Supported Agents" description: "AI agent frameworks and runtimes compatible with OpenShell sandboxes." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Claude, Codex, Cursor" position: 5 --- The following table summarizes the agents that run in OpenShell sandboxes. Most agent sandbox images are maintained in the [OpenShell Community](https://github.com/NVIDIA/OpenShell-Community) repository. Agents in the base image are auto-configured when passed as the trailing command to `openshell sandbox create`. | Agent | Source | Default Policy | Notes | |---|---|---|---| | [Claude Code](https://docs.anthropic.com/en/docs/claude-code) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Works out of the box. Requires `ANTHROPIC_API_KEY` for direct Anthropic access, or use `inference.local` with a configured provider (e.g. Vertex AI). | | [OpenCode](https://opencode.ai/) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Partial coverage | Pre-installed. Use `ANTHROPIC_BASE_URL="https://inference.local/v1"` with a configured provider. Add `opencode.ai` endpoint and OpenCode binary paths to the policy for full functionality. | | [Codex](https://developers.openai.com/codex) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | No coverage | Pre-installed. Requires a custom policy with OpenAI endpoints and Codex binary paths. Requires `OPENAI_API_KEY`. | | [GitHub Copilot CLI](https://docs.github.com/en/copilot/github-copilot-in-the-cli) | [`base`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/base) | Full coverage | Pre-installed. Works out of the box. Requires `GITHUB_TOKEN` or `COPILOT_GITHUB_TOKEN`. | | [OpenClaw](https://openclaw.ai/) | [NemoClaw](https://github.com/NVIDIA/NemoClaw) | Blueprint-managed | Run OpenClaw more securely inside NVIDIA OpenShell with managed inference using NemoClaw. | | [Hermes Agent](https://github.com/NousResearch/hermes-agent) | [NemoClaw](https://github.com/NVIDIA/NemoClaw) | Blueprint-managed | Run Hermes Agent more securely inside NVIDIA OpenShell with managed inference using NemoClaw. | | [Ollama](https://ollama.com/) | [`ollama`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/ollama) | Bundled | Run cloud and local models. Includes Claude Code, Codex, and OpenCode. Launch with `openshell sandbox create --from ollama`. | | [Pi](https://pi.dev/) | [`pi`](https://github.com/NVIDIA/OpenShell-Community/tree/main/sandboxes/pi) | Bundled | Comes with Pi pre-installed. Launch with `openshell sandbox create --from pi`. | For base image details and `--from` usage, refer to [Sandboxes](/sandboxes/manage-sandboxes#base-sandbox-container). For a complete support matrix, refer to the [Support Matrix](/reference/support-matrix) page. --- ### Extensibility/Gateway Interceptors --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Gateway Interceptors" sidebar-title: "Gateway Interceptors" description: "Extend OpenShell gateway operations with deployment-specific governance and business logic." keywords: "Generative AI, Cybersecurity, AI Agents, Gateway Interceptors, Extensibility, Governance" --- Gateway interceptors let operators add deployment-specific governance to OpenShell control-plane operations without modifying the gateway. An external gRPC service can modify or validate selected API writes before the gateway handles them, then observe successful responses after commit. See the [governance interceptor example](https://github.com/NVIDIA/OpenShell/tree/main/examples/governance-interceptor) for a complete service that vends provider profiles, applies a signed policy to new sandboxes, and rejects attempts to weaken that policy. ## Choose Gateway Interceptors Use a gateway interceptor when an external service needs to govern gateway API operations. For example, an interceptor can: - Apply an approved policy to every new sandbox. - Reject provider or policy changes that violate organizational rules. - Enforce tenant quotas or naming conventions. - Observe committed operations for an audit or inventory service. - Vend an authoritative or composed provider profile catalog. Gateway interceptors do not replace gateway persistence, authentication, authorization, policy safety checks, or driver validation. The gateway remains the system of record and validates an operation after interceptor modification. ## How Interception Works The gateway runs interceptors after authentication and before dispatching a request to its handler: `authenticate → decode and omit secrets → modify_operation → validate → gateway handler → post_commit` | Phase | Input | Capabilities | | ------------------ | ------------------------------------------- | ------------------------------------------------ | | `modify_operation` | Proposed request | Allow, deny, or return RFC 6902 JSON patches. | | `validate` | Modified request and optional current state | Allow or deny. | | `post_commit` | Successful gateway response | Observe the response and attach log annotations. | Only explicitly allowlisted unary mutation RPCs are interceptable. The gateway converts an operation to its protobuf JSON representation before evaluation. It applies patches atomically, validates the result against the RPC's protobuf schema, and converts the operation back to protobuf before the handler receives it. The gateway runs built-in operation and driver validation after `modify_operation`. A patched operation cannot bypass gateway-owned invariants. `post_commit` is observational. It cannot deny or modify an operation that the gateway has already committed. ## Implement an Interceptor Service An interceptor implements the `openshell.gateway_interceptor.v1.GatewayInterceptor` gRPC service defined in [`proto/gateway_interceptor.proto`](https://github.com/NVIDIA/OpenShell/blob/main/proto/gateway_interceptor.proto): - `Describe` declares the service's bindings and capabilities. - `Evaluate` handles one selected operation phase. - `SnapshotProviderProfiles` optionally returns a provider profile catalog. Each `InterceptorEvaluation` identifies the configured interceptor, manifest binding, public OpenShell service and method, authenticated principal, and active phase. The phase determines whether the payload contains a proposed operation, optional current state, or committed response. The interceptor returns an `InterceptorResult` with an allow or deny decision, an optional denial status and reason, JSON patches during `modify_operation`, and non-secret log annotations. ## Declare and Authorize Bindings The interceptor declares bindings in its `InterceptorManifest` returned by `Describe`. Each binding selects one public OpenShell RPC and one or more phases. The gateway validates these declarations at startup against its compiled protobuf descriptors and explicit interceptable RPC allowlist. The operator chooses how the manifest and gateway configuration combine with `binding_policy`: | Policy | Behavior | | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `dynamic` | Enables valid manifest bindings. Gateway configuration may narrow or disable them. This is the compatibility default and emits a startup warning. | | `allowlist` | Enables only operator-configured RPCs and phases. Extra manifest bindings are ignored and logged. | | `exact` | Requires the configured RPCs and phases to match the manifest exactly. | Use `allowlist` or `exact` when interceptor authority is part of a security boundary. These modes select bindings by public RPC rather than manifest binding ID, so renaming a binding does not change its authority. ## Register an Interceptor Service Start the interceptor before the gateway, then register it in gateway TOML: ```toml [[openshell.gateway.interceptors]] name = "policy-governance" grpc_endpoint = "https://governance.example:18081" tls_ca_cert_path = "/etc/openshell/governance-ca.pem" audience = "urn:example:governance" order = 10 failure_policy = "fail_closed" binding_policy = "allowlist" timeout = "500ms" max_response_bytes = 1048576 max_patches = 32 [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/CreateSandbox" phases = ["modify_operation", "validate"] [[openshell.gateway.interceptors.bindings]] rpc = "openshell.v1.OpenShell/UpdateConfig" phases = ["validate"] ``` The gateway supports `http://`, `https://`, and `unix://` interceptor endpoints. When gateway JWT signing is configured, authenticated network interceptors use `https://`; Unix sockets remain available for local integrations. HTTPS uses platform trust roots unless `tls_ca_cert_path` supplies a private CA, and normal hostname verification remains enabled. The gateway calls `Describe` and builds an immutable execution plan during startup. An unavailable service, invalid manifest, missing credential, or unauthorized configured binding prevents the gateway from starting. The gateway attaches a short-lived EdDSA bearer token to `Describe`, `Evaluate`, and provider-profile snapshot calls. The token uses the configured `audience` (defaulting to `urn:openshell:extension:interceptor:`) and `caller_kind: gateway`. Return your expected audience in the `expected_audience` field of your `Describe` manifest. After authenticated `Describe` succeeds, the gateway compares the advertised value with its operator-configured audience and refuses to start when they differ. This is a post-authentication consistency assertion, not audience discovery: a strict verifier may reject an incorrect audience before returning the manifest, in which case startup reports an authentication failure. Leave the field empty to skip the consistency check. Provision the trusted gateway URL, expected gateway ID, and public key or JWKS through the deployment. This operator-provisioned key material is the authoritative cold-start trust anchor. The expected issuer is exactly `openshell-gateway:`; fetching JWKS does not establish that identity by itself. After initial trust is established, `GET /.well-known/openid-configuration` and its `jwks_uri` provide steady-state key refresh and operational convenience. The document is OIDC-shaped rather than OIDC-compliant because `issuer` is the gateway identity rather than the serving URL; compare `iss` against the configured value and fetch updates only over authenticated TLS at the trusted gateway URL. Pin `alg` to `EdDSA`, require `typ` to be exactly `openshell-ext+jwt`, and validate `kid`, signature, expected issuer, exact audience, positive expiry, and caller kind. Set `allow_insecure_transport = true` on an interceptor to keep a plaintext `http://` endpoint working with no credential attached. The gateway logs a warning naming the interceptor at every startup, and the service cannot distinguish the gateway from any other client that can reach it. Registration is static. Restart the gateway after adding, removing, or changing an interceptor. See [Gateway Configuration](/reference/gateway-config#gateway-interceptors) for the complete field reference. ## Select RPCs and Phases Start with the invariant the interceptor must preserve, then identify every gateway RPC that can establish or weaken it. For example, an interceptor that owns sandbox policy can use `modify_operation` on `CreateSandbox` to apply an approved initial policy and `validate` on `UpdateConfig` to reject unauthorized changes. The gateway maintains the canonical [interceptable route allowlist](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-gateway-interceptors/src/routes.rs). Read the interceptor manifest and gateway startup diagnostics when selecting bindings. Unknown, streaming, read-only, and non-allowlisted RPCs cannot be intercepted. Choose a phase by intent: - Use `modify_operation` to apply defaults or controlled changes. - Use `validate` to enforce a rule without changing the request. - Use `post_commit` to notify or audit an external system after success. ## Mutate Operations Safely Operations and committed responses use protobuf JSON field names and shapes. Only `modify_operation` accepts RFC 6902 JSON patches. The gateway applies all patches returned by one binding as an atomic candidate. It then encodes that candidate as the RPC's protobuf request type and decodes it back to canonical protobuf JSON. If any patch or the resulting operation is invalid, the gateway discards the complete candidate and applies the binding's failure policy. Later bindings see only schema-valid operations that the handler can receive. Fields marked secret in the protobuf schema are recursively omitted from interceptor requests and committed responses. An interceptor cannot patch an omitted field, use one as a patch source, or replace a containing object. Gateway handlers retain the complete operation and continue to receive secret fields that were omitted from the interceptor view. ## Configure Failure Behavior Failure policy controls what happens when the gateway cannot obtain or apply a valid result. Failures include timeouts, transport errors, invalid responses, response-size violations, invalid phase behavior, and patch-limit violations. | Policy | Behavior | | ------------- | -------------------------------------------------------------------------------------------------- | | `fail_closed` | Rejects the API operation before handler dispatch. | | `fail_open` | Skips the failed result, continues with the previous valid operation, and emits warning telemetry. | A valid deny result during `modify_operation` or `validate` always rejects the operation. It is not an interceptor failure and does not follow the failure policy. Bindings that include `post_commit` must resolve to `fail_open`. The gateway rejects fail-closed post-commit configuration at startup because an observer cannot revoke an already committed response. A post-commit observation or evaluation failure is logged and counted without replacing the successful gateway response. Use `fail_open` only when bypassing an unavailable or invalid interceptor preserves the intended governance boundary. ## Vend Provider Profiles An interceptor can advertise `provider_profiles = true` in its manifest and implement `SnapshotProviderProfiles`. The RPC returns a `ProviderProfileSnapshot`. Add that interceptor to `provider_profile_sources` to include its profiles in the gateway's effective catalog. Select only the interceptor to make its catalog authoritative: ```toml [openshell.gateway] provider_profile_sources = [ { type = "interceptor", name = "provider-governance" }, ] ``` Include `{ type = "builtin" }` or `{ type = "user" }` entries to compose interceptor profiles with the built-in or user-managed sources. Duplicate normalized profile IDs fail instead of overriding one source with another. The gateway validates snapshot structure and provider profile semantics. It treats the configured interceptor as a trusted source and does not verify interceptor-defined signature, hash, or key annotations. ## Operate and Observe Interceptors Plan interceptor deployment around these boundaries: - Start every configured service before the gateway. - Restart the gateway after changing registrations or bindings. - Keep fail-closed services available whenever the gateway accepts writes. - Treat the endpoint and its operator configuration as part of the gateway trust boundary. - Do not include secrets in denial reasons or log annotations. The gateway emits structured evaluation logs containing the interceptor name, binding ID, RPC, phase, decision, patch count, and interceptor-provided log annotations. Metrics record evaluation decisions, latency, patch application, fail-open and fail-closed outcomes, and post-commit observation failures. ## Current Limitations - Only explicitly allowlisted unary write RPCs are interceptable. New gateway RPCs are non-interceptable until added to the allowlist. - `current_state` is available only in the `validate` contract. The gateway does not yet populate it with method-specific state. - Registration changes require a gateway restart. - mTLS client authentication, service health checks, runtime registration, and overlapping signing-key rotation are not available. - Extension tokens and sandbox-to-gateway tokens are signed by the same key, separated by audience and `typ`. The extension credential path cannot yet be rotated or revoked independently of sandbox admission. - Interceptors cannot receive or mutate protobuf fields marked secret. --- ### Extensibility/Supervisor Middleware --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Supervisor Middleware" sidebar-title: "Supervisor Middleware" description: "Configure and operate built-in and operator-run middleware for sandbox HTTP requests and WebSocket messages." keywords: "Generative AI, Cybersecurity, AI Agents, Supervisor Middleware, Extensibility, Request Filtering" --- Supervisor middleware adds ordered processing stages to allowed HTTP and WebSocket egress. Middleware runs after network and L7 policy admit traffic and before OpenShell injects provider credentials. A stage can allow or deny an HTTP request or client WebSocket text message, replace its payload, add approved HTTP headers, and report audit-safe findings. Middleware selection is independent of the network policy rule that admitted the request. OpenShell matches middleware by destination host, so the same middleware applies consistently across broad, specific, user-authored, and provider-derived network policies. ## Request Flow For each inspected HTTP request, the supervisor: 1. Evaluates network and L7 policy. 2. Selects middleware whose host selectors match the admitted destination. 3. Buffers the request body using the largest body limit in the selected chain. 4. Runs matching middleware by ascending `order`. Policy validation rejects duplicate order values. 5. Re-checks body-aware protocol policy (GraphQL, JSON-RPC, MCP) after each stage that replaces the body. Every middleware receives a payload the policy admits, and a transformation cannot smuggle a denied or unparseable operation to a later stage or the upstream. 6. Applies allowed transformations, injects provider credentials, and forwards the request. For an RFC 6455 upgrade over `ws://` or `wss://`, the supervisor first finds every host-matched attachment, then selects only implementations that advertise `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`. It opens one ordered, phase-specific `EvaluateWebSocketSession` stream per selected stage. OpenShell sends `WebSocketSessionEvent` values, while the service returns `WebSocketSessionEventResult` values only for preflight and message events; session start and end are notifications. Future upstream-to-client inspection uses the same RPC with `PRE_RETURN`; an implementation that advertises both phases receives two independent streams for the WebSocket session. An attachment without the selected binding can still inspect the HTTP upgrade request when it advertises the HTTP binding, but it is not a failed WebSocket stage. OpenShell allows post-upgrade traffic and emits an informational `binding_not_selected` coverage event for that attachment. 1. A preflight before the upgrade is sent upstream. The stage chooses `INSPECT`, voluntary `SKIP`, or authoritative `DENY` and may return a bounded diagnostic reason, stable reason code, findings, and metadata. OpenShell runs selected preflights concurrently; any `DENY` rejects the upgrade regardless of `on_error`. 2. A session-start event after the upstream accepts the upgrade, including the negotiated subprotocol. 3. Complete client-to-upstream text messages in sequence order. OpenShell reassembles fragmented messages and decompresses negotiated `permessage-deflate` messages before evaluation. 4. A best-effort session-end event when the stage stream remains writable. OpenShell attempts at most one terminal event for each opened stream, including streams opened during a preflight that rejects the upgrade before session start. The protobuf represents each logical message with a `text` or `binary` payload variant. Text uses the protobuf `string` type, so invalid UTF-8 cannot enter the middleware contract. Results use an optional matching replacement variant: absence preserves the input, while presence represents a replacement even when its content is empty. OpenShell rejects attempts to change the message type. Allowed replacements are re-framed, re-compressed when required, and forwarded. Binary messages, control frames, and upstream-to-client traffic remain uninspected. Binary messages pass through under both `on_error` modes. For each active selected stage, OpenShell emits an informational `unsupported_message_type` coverage event and advances the session-global sequence; the next text message can therefore reach the stage with a valid sequence gap. The network supervisor reserves process-wide assembly capacity before buffering every parsed WebSocket text message, even when no middleware is selected. At most 32 assemblies run while 64 additional callers wait without buffering payload bytes. When both bounds are full, OpenShell closes the WebSocket with code `1013` before reading the new message payload. A text message may contain at most 4,096 fragments, must make input progress within 30 seconds, and must finish assembly within 2 minutes. Forwarding the completed text frame must finish within another 2 minutes. The assembly budget lasts for the supervisor process lifetime, so policy reloads do not reset its capacity. Active middleware sessions additionally reserve shared middleware capacity before buffering WebSocket text, and HTTP middleware reserves the same capacity before buffering request bodies; at most 32 evaluations run and 64 additional unbuffered callers wait for capacity. When both middleware bounds are full, OpenShell sheds an HTTP request with `503 Service Unavailable` before reading its body. Persistent middleware streams use a separate process-wide budget of 32 sessions. WebSocket session admission does not wait: if the budget is full, OpenShell applies each selected config's `on_error` behavior before opening a stream. Because each transformed body is re-checked before the next stage runs, a middleware hook always receives a request that satisfies the sandbox policy. A stage whose output the policy rejects stops the chain; under `enforcement: audit` the rejection is logged and the request proceeds. If post-transformation policy evaluation itself fails, OpenShell denies the request and emits a high-severity detection finding. This failure is separate from middleware `on_error` because the middleware completed successfully; the sandbox policy could not validate its output. Middleware receives the request before credential injection. Operator-run services cannot inspect OpenShell-managed credentials. Middleware-visible request headers are delivered in wire order and repeated header names are preserved as separate entries. OpenShell filters credential, routing, framing, and hop-by-hop headers before invoking middleware. It rejects malformed request headers and unsupported transfer-coding sequences before middleware or policy dispatch. Headers named by a request's `Connection` field are omitted from middleware input and removed before forwarding, except for the validated WebSocket upgrade pair. ## Choose a Middleware Type | Type | Registration | Payload limit | Deployment | | --- | --- | --- | --- | | Built-in | None | Defined by OpenShell | Runs inside the supervisor | | Operator-run service | Required in gateway TOML | Set by the operator, up to the service capability | Runs as a separate service reachable by the gateway and supervisors | `openshell/regex` is an example built-in middleware. It replaces only simple, self-contained token patterns in UTF-8 HTTP bodies and client WebSocket text messages; the initial pattern recognizes `sk-` tokens. It does not infer values from keyword assignments such as JSON `password` fields. This best-effort text transformation is not parser-aware and does not guarantee that it will detect or fully remove sensitive values. Its `config` accepts one field, `mode: redact`, which is also the default when the field is omitted. Unknown config fields and non-string values are rejected at policy validation. Custom expressions are not configurable yet. Operator-run services expose bindings for supported operation and phase pairs. A binding is identified by its operation and phase. V1 supports `HttpRequest/pre_credentials` and `WebSocketMessage/pre_credentials`; a service may expose either or both. Policies attach the complete middleware by its operator-owned gateway registration name. ## Register a Middleware Service Start an operator-run service before starting the gateway, then add a registration to the local gateway TOML: ```toml [[openshell.supervisor.middleware]] name = "local-content-guard" grpc_endpoint = "https://content-guard.example:50051" tls_ca_cert_path = "/etc/openshell/content-guard-ca.pem" audience = "urn:example:content-guard" max_payload_bytes = 262144 timeout = "500ms" ``` | Field | Description | | --- | --- | | `name` | Operator-owned registration name used by policy attachments and diagnostics. Names must be unique, and `openshell/` is reserved for built-ins. | | `grpc_endpoint` | Service address reachable from both the gateway and sandbox supervisors. Authenticated extensions use TLS `https://`. | | `tls_ca_cert_path` | Optional PEM trust roots for a private HTTPS service. Custom roots replace platform roots and retain hostname verification. | | `audience` | Exact audience expected by the service. Defaults to `urn:openshell:extension:middleware:`. | | `allow_insecure_transport` | Opt this registration out of extension authentication, permitting a plaintext `http://` endpoint with no bearer credential. Defaults to `false`. Development and trusted-network deployments only. | | `max_payload_bytes` | Shared operator limit applied to inspectable logical payloads across every binding exposed by the service, up to the 4 MiB platform maximum. It caps HTTP bodies and complete WebSocket text messages. | | `timeout` | Optional service-wide RPC timeout using an integer with an `ms` or `s` suffix. Defaults to `500ms`; valid values range from `10ms` through `30s`. | Each binding returned by `Describe` may advertise a shorter `timeout` using the same syntax and bounds. The operator-configured service timeout is a ceiling: OpenShell uses the smaller of the binding and service values. An omitted binding timeout inherits the service setting, and an omitted service setting uses the 500 ms platform default. OpenShell rejects an invalid timeout before accepting the manifest. The operator-configured service timeout applies to `Describe` and `ValidateConfig`. The effective binding timeout applies only to `EvaluateHttpRequest`, WebSocket preflight, and each WebSocket message. WebSocket streams have no connection-wide deadline. The gateway connects to every registered service and verifies its capabilities before accepting traffic. Gateway startup fails when a service is unavailable, reports an invalid capability, or exposes more than one binding for the same operation and phase. The manifest `name` is diagnostic metadata and does not need to match the operator registration name. Operator-run registration names cannot claim the reserved `openshell/` namespace. Registration is static. Restart the gateway after adding, removing, or changing a service. See [Gateway Configuration](/reference/gateway-config#supervisor-middleware-services) for the complete gateway TOML context. ### Authenticate OpenShell Callers When gateway JWT signing is configured, OpenShell attaches a short-lived EdDSA bearer token to every remote middleware RPC. Gateway calls use `caller_kind: gateway`; sandbox supervisor calls use `caller_kind: supervisor` and include the sandbox ID. Supervisors request credentials by registration name through `RefreshSandboxToken`. The gateway derives the audience from operator-owned configuration and authorizes each name against the sandbox's effective policy. Return your expected audience in the `expected_audience` field of your `Describe` manifest. After authenticated `Describe` succeeds, OpenShell compares the advertised value with its operator-configured audience and refuses to start when they differ. This is a post-authentication consistency assertion, not audience discovery: a strict verifier may reject an incorrect audience before returning the manifest, in which case startup reports an authentication failure. Leave the field empty to skip the consistency check. Provision the trusted gateway URL, expected gateway ID, and public key or JWKS through the deployment. This operator-provisioned key material is the authoritative cold-start trust anchor. The expected issuer is exactly `openshell-gateway:`; fetching JWKS does not establish that identity by itself. After initial trust is established, `GET /.well-known/openid-configuration` and its `jwks_uri` provide steady-state key refresh and operational convenience. The document is OIDC-shaped rather than OIDC-compliant: `issuer` is the gateway identity, not the URL serving the document, so compare `iss` against the configured value and fetch updates only over authenticated TLS at the trusted gateway URL. Cache keys by `kid`. Validate, at minimum: - `typ` is exactly `openshell-ext+jwt`. Extension tokens and sandbox-to-gateway bootstrap tokens share a signing key and differ only in audience; this header is a second, independent discriminator. - `alg` is pinned to `EdDSA`. Never select the algorithm from the token. - Signature, expected issuer, exact audience, and positive expiry. - `caller_kind`, and the sandbox identity when your service scopes behavior per sandbox. A sandbox-to-gateway JWT is not an extension credential even though both token types use the same signing key. Each token carries a unique `jti` that identifies that token instance for correlation and future explicit revocation. OpenShell reuses a token across calls until rotation and does not track `jti`, so rejecting a repeated `jti` would reject legitimate requests. Per-request replay resistance requires a request nonce or signature, channel binding, or another proof-of-possession mechanism. ### Run Without Extension Authentication Set `allow_insecure_transport = true` on a registration to keep a plaintext `http://` endpoint working. OpenShell then attaches no credential to that service, supervisors do not request one, and the gateway refuses to mint one if asked. The gateway logs a warning naming the registration at every startup. The service cannot distinguish OpenShell from any other client that can reach it. Use this only where the network already provides that guarantee, and prefer `https://` everywhere else. ## Apply Middleware with Policy Add middleware configs to the top-level `network_middlewares` map. Each key is the policy-local config name: ```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"] ``` Each config has a stable policy-local identity from its map key, an optional human-readable `name` that defaults to that key, a built-in or operator-owned registration name in `middleware`, an integer `order`, implementation-owned `config`, failure behavior, and host selectors. The optional name does not replace the map key for attachment or future keyed updates. A policy accepts at most 10 middleware configs. `include` selects destination hosts. `exclude` takes precedence and removes hosts from that selection. Each config accepts at most 32 combined include and exclude patterns. Matching is case-insensitive and uses the same exact-host and DNS glob behavior as network policy endpoints: `*` matches exactly one DNS label, `**` matches one or more labels, and intra-label patterns like `*-api.example.com` work. Brace alternates such as `{prod,staging}` are rejected at validation; list each host pattern separately. Matching configs run once each by ascending `order`; lower values run first. Order values must be unique across the complete policy, even when endpoint selectors do not overlap. The default order is `0`, so policies with multiple configs normally set explicit values. Different map keys may attach the same middleware and run as separate stages. Map keys are structurally unique. Runtime selection defensively rejects chains with more than 10 stages. See [Policy Schema](/reference/policy-schema#network-middleware) for the complete field reference. ## Configure Failure Behavior `on_error` controls what happens after an operation binding is selected and middleware is unavailable, rejects its configuration, returns an invalid result, or exceeds the selected binding's payload limit. It does not turn an unadvertised operation or an unsupported WebSocket message class into a middleware failure. | Value | Behavior | | --- | --- | | `fail_closed` | Denies the HTTP request or closes the WebSocket when the stage fails. This is the default. | | `fail_open` | Skips the failed HTTP stage. For a broken WebSocket stage stream, disables that stage for the rest of the connection and continues the remaining chain. | Use `fail_open` only when bypassing the middleware preserves the intended security policy. OpenShell emits a detection finding when a failed stage is bypassed and a separate state-change finding when a WebSocket stage is disabled for the session. Capability coverage is separate from failure handling. A host-matched HTTP-only attachment does not join the WebSocket chain, regardless of `on_error`. Binary messages are outside the V1 text-message binding and pass through even when a selected stage is `fail_closed`. OpenShell records both states as informational coverage events so operators do not mistake pass-through traffic for inspected traffic. If a deployment requires all WebSocket message classes to be inspected, V1 cannot express that requirement. An explicit deny decision always stops the chain and denies the request or WebSocket upgrade, regardless of `on_error`. A WebSocket preflight `DENY` is a successful policy decision, not a middleware failure; OpenShell rejects the upgrade before upstream contact and ends each still-writable stream opened by a successful preflight decision with `MIDDLEWARE_DENIAL`. The HTTP response uses `error: middleware_denied`, identifies the policy-local middleware config, and omits policy-advisor remediation because the network and L7 allow rules already matched. OpenShell never copies the free-form middleware `reason` into the response or security logs. HTTP results, WebSocket preflight decisions, and WebSocket message results can instead return an optional stable `reason_code`: 1–64 bytes, starting with a lowercase ASCII letter and containing only lowercase ASCII letters, digits, and underscores. Invalid codes make the result a middleware failure governed by `on_error`. Preflight findings and metadata use the same bounds and audit-safe handling as message results. ```json { "error": "middleware_denied", "detail": "Request rejected by configured middleware", "policy": "api-policy", "middleware": "prototype-content-guard", "reason_code": "content_match" } ``` A failed `fail_closed` stage uses `error: middleware_failed` and a platform-owned `detail`. It also omits `rule_missing`, `next_steps`, and `agent_guidance`: the failure did not result from a missing network or L7 policy rule, and changing policy cannot repair it. Runtime diagnostic text is available only through sanitized operator telemetry. Middleware decisions are enforced regardless of the endpoint's `enforcement` mode. `enforcement: audit` applies to an endpoint's network and L7 policy rules and does not bypass middleware: a middleware deny, or a failed `fail_closed` stage, blocks the request even on an audit endpoint. A middleware service that needs to observe traffic without blocking should return an allow decision with findings, which OpenShell emits as detection findings. ## Set Payload Limits Every middleware binding declares the largest logical payload or replacement it supports through `max_payload_bytes`. For `HTTP_REQUEST`, that payload is one request body. For `WEBSOCKET_MESSAGE`, it is one complete message rather than the whole session. - Built-in middleware uses its OpenShell-defined limit. - Each operator-run registration sets one `max_payload_bytes` ceiling no higher than any binding's advertised `max_payload_bytes` capability. - A selected chain buffers using its largest stage limit, so every stage that can process the body receives it. - The same per-stage limit applies to request bodies and replacement bodies. The gateway rejects a registration whose operator limit exceeds the service capability or the 4 MiB platform maximum instead of silently clamping it. OpenShell also bounds the non-payload protobuf components: 64 KiB for service config, 4 KiB for request context, 32 KiB for the target, and 128 request header lines totaling at most 64 KiB encoded. Results allow a 4 KiB discarded free-form reason, a 64-byte validated reason code, 64 header mutations totaling at most 64 KiB encoded, 32 findings of at most 4 KiB encoded each, and 64 metadata entries totaling at most 32 KiB. Middleware gRPC servers should configure request and response message limits to at least 4 MiB plus 293 KiB so every platform-valid envelope fits. At request time, exceeding a selected stage's limit is a middleware failure for that stage alone and follows that config's `on_error` behavior; other stages in the chain still run against their own limits. OpenShell can apply `fail_open` to an oversized `Content-Length` before consuming body bytes. A chunked body can cross the limit only after bytes have been consumed, so OpenShell denies that request because it cannot safely resume the original stream. For a WebSocket binding, `max_payload_bytes` covers complete client text messages and replacements. Exceeding a selected stage's effective text-message limit follows that stage's `on_error`. The 4 MiB parsed-text platform cap and other protocol-safety limits are independent of middleware failure policy. Binary messages are not delivered to middleware, so the operator ceiling does not become a binary relay limit; individual raw binary frames retain the 16 MiB relay-safety bound. Oversized parsed text closes the connection with code `1009`; invalid UTF-8 uses `1007`; protocol errors use `1002`; middleware or policy denials use `1008`; and policy reload uses `1012`. ## Mutate Request Headers A middleware result can return ordered header mutations before OpenShell injects credentials. A `write` mutation adds a value when the case-insensitive header name is absent and selects one behavior when it is already present: - `append` adds another field value. - `overwrite` removes every existing value before adding the new value. - `skip` leaves existing values unchanged. A `remove` mutation removes every value for a case-insensitive header name. OpenShell applies each successful stage's mutations before invoking the next middleware, so later stages observe the accumulated header state. Header writes must use the `x-openshell-middleware-` prefix. Removes may target other middleware-visible request headers. Protected credential, routing, framing, and hop-by-hop headers are always rejected. Header values must not contain control characters. OpenShell validates and applies each stage's mutations atomically. An invalid operation discards every mutation from that stage and follows its `on_error` behavior. Built-in failures can name the offending header. Operator-run failures use a platform-owned error code so request-derived header text cannot reach logs or denied responses. ## Operate Middleware Services Plan startup and updates around these boundaries: - Start registered services before the gateway. The gateway validates every registration during startup. - Keep service endpoints reachable from both the gateway and sandbox supervisors. The supervisors call operator-run services directly on the request path. - Restart the gateway after changing registrations. - Keep required services available before creating or updating policies. The gateway validates implementation-owned config before persisting a policy. - Treat `fail_open` as an explicit availability-over-enforcement decision. When the effective sandbox configuration changes, a running supervisor validates the new service registry before installing it. If the reload fails, the supervisor keeps its last-known-good registry and emits a configuration failure event. ## Observe Middleware Middleware activity is emitted through OpenShell's OCSF logging: - Each invocation records its policy-local config name, attached middleware name, decision, transformation state, and failure state. - A denied invocation records a platform-owned reason derived from the policy-local config name and optional validated reason code. OpenShell does not record service-provided free-form reason text. - A bypass under `fail_open` emits a detection finding. - A required stage that fails closed emits a high-severity detection finding. - A host-matched attachment without a WebSocket binding emits an informational `binding_not_selected` coverage event. - A binary message encountered by an active WebSocket stage emits an informational `unsupported_message_type` coverage event with message type, sequence, and byte count. It is not reported as an invocation or failure. - Built-in findings include their type, label, and aggregate count. Operator-run findings use the operator-owned registration name and a platform label plus the aggregate count; OpenShell does not log service-provided finding text or diagnostic metadata. A stage can return at most 32 findings. Exceeding the per-stage cap is an invalid response handled through `on_error`. A maximum 10-stage chain retains and emits up to 320 findings without silently dropping findings from later stages. - Registry reload success and failure are emitted as configuration state changes. See [Logging](/observability/logging) for log access and [OCSF JSON Export](/observability/ocsf-json-export) for structured export. ## Current Limitations - Middleware applies only through operation bindings advertised by each implementation. For protocols that have no supported middleware operation at all, such as HTTP/2 prior knowledge or non-HTTP TCP, the existing uninspectable-traffic gate denies a host match containing `fail_closed` and relays an all-`fail_open` match with a detection finding. - The typed operation and phase pairs are `HTTP_REQUEST/PRE_CREDENTIALS` and `WEBSOCKET_MESSAGE/PRE_CREDENTIALS`. - A host match does not imply every advertised operation: an HTTP-only attachment can inspect the upgrade GET, then post-upgrade traffic passes with `binding_not_selected` coverage. - The V1 WebSocket binding inspects complete client text messages only. Binary messages pass with `unsupported_message_type` coverage for active stages; control frames and upstream-to-client messages remain outside the middleware operation. - Selection uses destination host include and exclude patterns. - A fail-closed middleware cannot cover `tls: skip` endpoints because OpenShell cannot inspect that traffic. An all-`fail_open` match may cover the endpoint; OpenShell bypasses the middleware and emits a detection finding. - Operator-run services use TLS `https://` when gateway JWT signing is enabled, unless the registration sets `allow_insecure_transport`. Certificates must chain to the configured custom CA or platform roots, and the endpoint hostname must match. - Extension tokens and sandbox-to-gateway tokens are signed by the same key. They are separated by audience and by `typ`, but the extension credential path cannot yet be rotated or revoked independently of sandbox admission. - OpenShell does not track or revoke `jti`; bearer tokens can be replayed until expiry. Per-request replay resistance requires proof of possession or request binding. - mTLS client authentication, health checks, runtime registration, and overlapping signing-key rotation are not available. --- ### Get Started/Tutorials/Docker Compose --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Run the Gateway with Docker Compose" sidebar-title: "Docker Compose Setup" slug: "get-started/tutorials/docker-compose" description: "Run the OpenShell gateway as a Docker Compose service and create agent sandboxes." keywords: "Generative AI, Docker Compose, Gateway, Sandbox, OpenClaw, Docker, Installation" --- This tutorial shows how to run the OpenShell gateway as a Docker Compose service on a Linux host or on a machine running Docker Desktop (Windows or macOS). After completing this tutorial you have: - An OpenShell gateway running as a Compose service. - The `openshell` CLI registered against that gateway. - An AI provider configured with your API key. - A running agent sandbox. ## Prerequisites - Docker Desktop (Windows or macOS) or Docker Engine with the Compose plugin (Linux). - The `openshell` CLI installed on your workstation. See [Install the CLI](#install-the-cli) below. - Port 8080 available on the host. ## Compose files The Compose configuration lives at [`deploy/docker/`](https://github.com/NVIDIA/OpenShell/tree/main/deploy/docker) in the repository. | File | Purpose | |---|---| | `docker-compose.yml` | Gateway service, volumes, and environment variables | | `gateway.toml` | TOML reference for release builds with config-file support | ## Port note The Docker compute driver injects `host.openshell.internal:` into every sandbox container as its callback address. The gateway listens on port 8080 inside the container, so **port 8080 must be published at the same number on the Docker host**. Publishing it as a different host port (for example `18080:8080`) causes sandbox containers to call back to the wrong port and remain stuck in the `Provisioning` phase. If port 8080 is taken, change `OPENSHELL_SERVER_PORT` and update the port mapping to `:8080`, then set `OPENSHELL_PORT=` in an `.env` file. ## Data directory The gateway extracts the `openshell-sandbox` supervisor binary from `ghcr.io/nvidia/openshell/supervisor:latest` on first start and caches it at: ```text /var/lib/openshell/openshell/docker-supervisor//openshell-sandbox ``` This path is used as a bind-mount source when Docker creates sandbox containers. Docker resolves bind-mount sources against the **host filesystem**, not the container filesystem, so the data directory must be bind-mounted at the **same absolute path** in both the host and the container. The Compose file uses `/var/lib/openshell` for this purpose and sets `create_host_path: true` so Docker creates it on first run. ## Start the gateway ```shell cd deploy/docker docker compose up -d ``` Verify the gateway is healthy: ```shell curl -sf http://localhost:8080/healthz ``` ## Install the CLI **Binary (recommended — macOS / Linux / WSL):** ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` **From PyPI (any platform with [uv](https://docs.astral.sh/uv/)):** ```shell uv tool install -U openshell ``` On Windows without WSL, install the CLI inside a WSL 2 distribution (for example AlmaLinux or Ubuntu) and run all `openshell` commands from that distribution. ## Register the gateway Run this once after the gateway starts: ```shell openshell gateway add http://localhost:8080 --name openshell-docker ``` Verify the connection: ```shell openshell status ``` The output should show `Status: Connected`. ## Configure an AI provider Set your API key as an environment variable and create a provider: ```shell ANTHROPIC_API_KEY=sk-ant-... \ openshell provider create --name anthropic --type anthropic --from-existing ``` ```shell OPENAI_API_KEY=sk-... \ openshell provider create --name openai --type openai --from-existing ``` Confirm the provider was stored: ```shell openshell provider list ``` ## Pre-pull sandbox images (optional) Sandbox images are pulled automatically on first use, but the initial pull can take several minutes for large images. Pre-pull to avoid long waits at sandbox creation time: ```shell # Base image — includes Claude Code, OpenCode, Codex, and Copilot docker pull ghcr.io/nvidia/openshell-community/sandboxes/base:latest ``` ## Create a sandbox OpenClaw runs inside OpenShell through [NemoClaw](https://github.com/NVIDIA/NemoClaw), which manages the sandbox image, inference routing, and security policies. Follow the [NemoClaw Quickstart](https://docs.nvidia.com/nemoclaw/latest/get-started/quickstart/) to set up an OpenClaw sandbox with managed inference. ```shell openshell sandbox create -- claude ``` ```shell openshell sandbox create -- opencode ``` Wait for the phase to change from `Provisioning` to `Ready`: ```shell openshell sandbox list ``` Then connect: ```shell openshell sandbox connect ``` ## Manage the gateway | Command | Purpose | |---|---| | `docker compose up -d` | Start or restart the gateway | | `docker compose down` | Stop the gateway and remove the container | | `docker compose logs -f` | Tail gateway logs | | `docker compose pull` | Pull a new gateway image version | ## Linux notes On Linux, `host.docker.internal` and `host.openshell.internal` are not automatically resolvable from containers. Add the following under the `gateway` service in `docker-compose.yml`: ```yaml extra_hosts: - "host.docker.internal:host-gateway" - "host.openshell.internal:host-gateway" ``` ## Next steps - [First Network Policy](/get-started/tutorials/first-network-policy) — apply L7 policies to your sandbox. - [GitHub Push Access](/get-started/tutorials/github-sandbox) — grant a sandbox scoped GitHub access. --- ### Get Started/Tutorials/First Network Policy --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Write Your First Sandbox Network Policy" sidebar-title: "First Network Policy" slug: "get-started/tutorials/first-network-policy" description: "Learn how OpenShell network policies work by creating a sandbox, observing default-deny in action, and applying a fine-grained L7 read-only rule." keywords: "Generative AI, Cybersecurity, Tutorial, Policy, Network Policy, Sandbox, Security" --- 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 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](/get-started/quickstart) before proceeding. - Docker Desktop running on your machine. To run every step of this tutorial, you can also use the automated demo script at the [examples/sandbox-policy-quickstart](https://github.com/NVIDIA/OpenShell/blob/main/examples/sandbox-policy-quickstart) directory in the NVIDIA OpenShell repository. It runs the full walkthrough in under a minute but without any user interaction. ```shell bash examples/sandbox-policy-quickstart/demo.sh ``` ## Create a Sandbox Start by creating a sandbox with no network policies. This gives you a clean environment to observe default-deny behavior. ```shell openshell sandbox create --name demo --no-auto-providers ``` `--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 sandbox@demo:~$ ``` ## 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: ```shell curl -s https://api.github.com/zen ``` `https://api.github.com/zen` is a lightweight, unauthenticated GitHub REST endpoint that returns a random aphorism on each call. It requires no tokens or parameters, which makes it a convenient smoke-test target for verifying outbound HTTPS connectivity. The request fails. By default, all outbound network traffic is denied. 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 curl: (56) Received HTTP code 403 from proxy after CONNECT ``` Exit the sandbox. Sandboxes are kept running by default, so you can reconnect later. Use `--no-keep` at creation time if you want the sandbox deleted after exit: ```shell exit ``` ## 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. ```shell openshell logs demo --since 5m ``` 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" ``` 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: ```yaml version: 1 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 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` 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. 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. 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/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. ## Verify If GET Requests Are Allowed The policy is now active. Reconnect to the sandbox and retry the same request to confirm that read access works. ```shell openshell sandbox connect demo ``` Retry the same request: ```shell curl -s https://api.github.com/zen ``` ```text Anything added dilutes everything else. ``` It works. The `read-only` preset allows GET requests through. ## Try a Write The read-only preset allows GET but blocks mutating methods like POST, PUT, and DELETE. Test this by sending a POST request to the GitHub API while still inside the sandbox: ```shell curl -s -X POST https://api.github.com/repos/octocat/hello-world/issues \ -H "Content-Type: application/json" \ -d '{"title":"oops"}' ``` ```json {"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: ```shell exit ``` ## 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. ```shell openshell logs demo --level warn --since 5m ``` ```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" ``` 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. 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. ## Clean Up Delete the sandbox to free resources. This stops all processes and purges any injected credentials. ```shell openshell sandbox delete demo ``` To run this entire walkthrough non-interactively, use the automated demo script: ```shell 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](/get-started/tutorials/github-sandbox). --- ### Get Started/Tutorials/Github Sandbox --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Grant GitHub Push Access to a Sandboxed Agent" sidebar-title: "GitHub Push Access" slug: "get-started/tutorials/github-sandbox" description: "Learn the iterative policy workflow by launching a sandbox, diagnosing a GitHub access denial, and applying a custom policy to fix it." keywords: "Generative AI, Cybersecurity, Tutorial, GitHub, Sandbox, Policy, Claude Code" --- This tutorial walks through an iterative sandbox policy workflow. You launch a sandbox, ask Claude Code to push code to GitHub, and observe the default network policy denying the request. You then diagnose the denial from your machine and from inside the sandbox, apply a policy update, and verify that the policy update to the sandbox takes effect. After completing this tutorial, you have: - A running sandbox with Claude Code that can push to a GitHub repository. - A custom network policy that grants GitHub access for a specific repository. - Experience with the policy iteration workflow: fail, diagnose, update, verify. This tutorial shows example prompts and responses from Claude Code. The exact wording you see might vary between sessions. Use the examples as a guide for the type of interaction, not as expected output. ## Prerequisites This tutorial requires the following: - A working OpenShell installation. Complete the [Quickstart](/get-started/quickstart) before proceeding. - A GitHub personal access token (PAT) with `repo` scope. Generate one from the [GitHub personal access token settings page](https://github.com/settings/tokens) by selecting **Generate new token (classic)** and enabling the `repo` scope. - An [Anthropic account](https://console.anthropic.com/) with access to Claude Code. OpenShell provides the sandbox runtime, not the agent. You must authenticate with your own account. - A GitHub repository you own to use as the push target. A scratch repository is sufficient. You can [create one](https://github.com/new) with a README if needed. This tutorial uses two terminals to demonstrate the iterative policy workflow: - **Terminal 1**: The sandbox terminal. You create the sandbox in this terminal by running `openshell sandbox create` and interact with Claude Code inside it. - **Terminal 2**: A terminal outside the sandbox on your machine. You use this terminal for viewing the sandbox logs with `openshell term` and applying an updated policy with `openshell policy set`. Each section below indicates which terminal to use. ## Set Up a Sandbox with Your GitHub Token Depending on whether you start a new sandbox or use an existing sandbox, choose the appropriate tab and follow the instructions. In terminal 2, create a new sandbox with Claude Code. The [default policy](/reference/default-policy) is applied automatically, which allows read-only access to GitHub. Create a [credential provider](/sandboxes/manage-providers) that injects your GitHub token into the sandbox automatically. The provider reads `GITHUB_TOKEN` from your host environment and sets it as an environment variable inside the sandbox: ```shell GITHUB_TOKEN= openshell provider create --name my-github --type github --from-existing openshell sandbox create --provider my-github -- claude ``` `openshell sandbox create` keeps the sandbox running after Claude Code exits, so you can apply policy updates later without recreating the environment. Add `--no-keep` if you want the sandbox deleted automatically instead. Claude Code starts inside the sandbox. It prints an authentication link. Open it in your browser, sign in to your Anthropic account, and return to the terminal. When prompted, trust the `/sandbox` workspace to allow Claude Code to read and write files. In terminal 1, connect to a sandbox that is already running and set your GitHub token as an environment variable: ```shell openshell sandbox connect export GITHUB_TOKEN= ``` To find the name of running sandboxes, run `openshell sandbox list` in terminal 2. ## Push Code to GitHub In terminal 1, ask Claude Code to write a simple script and push it to your repository. Replace `` with your GitHub organization or username and `` with your repository name. ```md title="Prompt" wordWrap showLineNumbers={false} Write a `hello_world.py` script and push it to `https://github.com//`. ``` Claude recognizes that it needs GitHub credentials. It asks how you want to authenticate. Provide your GitHub personal access token by pasting it into the conversation. Claude configures authentication and attempts the push. The push fails. Claude reports an error, but the failure is not an authentication problem. The default sandbox policy permits read-only access to GitHub and blocks write operations, so the proxy denies the push before the request reaches the GitHub server. ## Diagnose the Denial In this section, you diagnose the denial from your machine and from inside the sandbox. ### View the Logs from Your Machine In terminal 2, launch the OpenShell terminal: ```shell openshell term ``` The dashboard shows sandbox status and a live stream of policy decisions. Look for entries with `l7_decision=deny`. Select a deny entry to see the full detail: ```text l7_action: PUT l7_target: /repos///contents/hello_world.py l7_decision: deny dst_host: api.github.com dst_port: 443 l7_protocol: rest policy: github_rest_api l7_deny_reason: PUT /repos///contents/hello_world.py not permitted by policy ``` The log shows that the sandbox proxy intercepted an outbound `PUT` request to `api.github.com` and denied it. The `github_rest_api` policy allows read operations (GET) but blocks write operations (PUT, POST, DELETE) to the GitHub API. A similar denial appears for `github.com` if Claude attempted a git push over HTTPS. ### Ask Claude Code to Check the Sandbox Logs In terminal 1, ask Claude Code to check the sandbox logs for denied requests: ```md title="Prompt" wordWrap showLineNumbers={false} Check the sandbox logs for any denied network requests. What is blocking the push? ``` Claude reads the deny entries and identifies the root cause. It explains that the failure is a sandbox network policy restriction, not a token permissions issue. For example, the following is a possible response: The sandbox runs a proxy that enforces policies on outbound traffic. The `github_rest_api` policy allows GET requests (used to read the file) but blocks PUT/write requests to GitHub. This is a sandbox-level restriction, not a token issue. No matter what token you provide, pushes through the API are blocked until you update the policy. Both perspectives confirm the same thing: the proxy is doing its job. The default policy is designed to be restrictive. To allow GitHub pushes, you need to update the network policy. Copy the deny reason from Claude's response. You paste it into an agent running on your machine in the next step. ## Update the Policy from Your Machine In terminal 2, paste the deny reason from the previous step into your coding agent on your machine, such as Claude Code or Cursor, and ask it to recommend a policy update. The deny reason gives the agent the context it needs to generate the correct policy rules. After pasting the following prompt sample, properly provide the GitHub organization and repository names of the repository you are pushing to. ```md title="Prompt" wordWrap showLineNumbers={false} Based on the following deny reasons, recommend a sandbox policy update that allows GitHub pushes to `https://github.com//`, and save to `/tmp/sandbox-policy-update.yaml`: The `filesystem_policy` and `landlock` sections are static. They are read once at sandbox creation and cannot be changed by a hot reload. They are included here for completeness so the file is self-contained. Process identity is omitted so the active compute driver can select it, and only the `network_policies` section takes effect when you apply this to a running sandbox. ``` The following steps outline the expected process done by the agent: 1. Inspects the deny reasons. 2. Writes an updated policy that adds `github_git` and `github_api` blocks that grant write access to your repository. 3. Saves the policy to `/tmp/sandbox-policy-update.yaml`. ## Review the Generated Policy Refer to the following policy example to compare with the generated policy before applying it. Confirm that the policy grants only the access you expect. In this case, `git push` operations and GitHub REST API access scoped to a single repository. The following YAML shows a complete policy that extends the [default policy](/reference/default-policy) with GitHub access for a single repository. Replace `` with your GitHub organization or username and `` with your repository name. The `filesystem_policy` and `landlock` sections are static. OpenShell reads them at sandbox creation, and a hot reload cannot change them. They are included here for completeness so the file is self-contained. Process identity is omitted so the active compute driver can select it, and only the `network_policies` section takes effect when you apply this to a running sandbox. ``` /* Detailed source-code truncated for AI context efficiency. */ ``` The following table summarizes the two GitHub-specific blocks: | Block | Endpoint | Behavior | |---|---|---| | `github_git` | `github.com:443` | Git Smart HTTP protocol. The proxy auto-detects and terminates TLS to inspect requests. Permits `info/refs` (clone/fetch), `git-upload-pack` (fetch data), and `git-receive-pack` (push) for the specified repository. Denies all operations on unlisted repositories. | | `github_api` | `api.github.com:443` | REST API. The proxy auto-detects and terminates TLS to inspect requests. Permits all HTTP methods for the specified repository and GraphQL queries. Denies API access to unlisted repositories. | The remaining blocks (`claude_code`, `nvidia_inference`, `pypi`, `vscode`) are identical to the [default policy](/reference/default-policy). The default policy's `github_ssh_over_https` and `github_rest_api` blocks are replaced by the `github_git` and `github_api` blocks above, which grant write access to the specified repository. Sandbox behavior outside of GitHub operations is unchanged. For details on policy block structure, refer to [Policies](/sandboxes/policies). ## Apply the Policy After you have reviewed the generated policy, apply it to the running sandbox: ```shell openshell policy set --policy /tmp/sandbox-policy-update.yaml --wait ``` Network policies are hot-reloadable. The `--wait` flag blocks until the policy engine confirms the new revision loaded, and the update takes effect immediately without restarting the sandbox or reconnecting Claude Code. ## Retry the Push In terminal 1, ask Claude Code to retry the push: ```md title="Prompt" wordWrap showLineNumbers={false} The sandbox policy has been updated. Try pushing to the repository again. ``` The push completes successfully. The `openshell term` dashboard now shows `l7_decision=allow` entries for `api.github.com` and `github.com` where it previously showed denials. ## Clean Up When you are finished, delete the sandbox to free gateway compute resources: ```shell openshell sandbox delete ``` ## Next Steps The following resources cover related topics in greater depth: - To add per-repository access levels (read-write vs read-only) or restrict to specific API methods, refer to the [Policy Schema Reference](/reference/policy-schema). - To learn the full policy iteration workflow (pull, edit, push, verify), refer to [Policies](/sandboxes/policies). - To inject credentials automatically instead of pasting tokens, refer to [Manage Providers](/sandboxes/manage-providers) --- ### Get Started/Tutorials/Index --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Tutorials" slug: "get-started/tutorials" description: "Step-by-step walkthroughs for OpenShell, from first sandbox to production-ready policies." keywords: "Generative AI, Cybersecurity, Tutorial, Sandbox, Policy" position: 1 --- Hands-on walkthroughs that teach OpenShell concepts by building real configurations. Each tutorial builds on the previous one, starting with core sandbox mechanics and progressing to production workflows. Create a sandbox, observe default-deny networking, apply a read-only L7 policy, and inspect audit logs. No AI agent required. Launch Claude Code in a sandbox, diagnose a policy denial, and iterate on a custom GitHub policy from outside the sandbox. Configure a Providers v2 Microsoft Graph provider with gateway-managed OAuth2 refresh-token rotation. Route inference through Ollama using cloud-hosted or local models, and verify it from a sandbox. Route inference to a local LM Studio server using the OpenAI-compatible or Anthropic-compatible APIs. Run the OpenShell gateway as a Docker Compose service and create agent sandboxes including OpenClaw. --- ### Get Started/Tutorials/Inference Ollama --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Run Local Inference with Ollama" sidebar-title: "Inference with Ollama" slug: "get-started/tutorials/inference-ollama" description: "Run local and cloud models inside an OpenShell sandbox using the Ollama community sandbox, or route sandbox requests to a host-level Ollama server." keywords: "Generative AI, Cybersecurity, Tutorial, Inference Routing, Ollama, Local Inference, Sandbox" --- This tutorial covers two ways of running Ollama with OpenShell: 1. Ollama sandbox. This is the recommended way to run Ollama. A self-contained sandbox with Ollama, Claude Code, and Codex pre-installed. One command starts it. 2. Host-level Ollama. This is an alternative way to run Ollama. Run Ollama on the gateway host and route sandbox inference to it. Use this option when you want a single Ollama instance shared across multiple sandboxes. After completing this tutorial, you know how to: - Launch the Ollama community sandbox for a batteries-included experience. - Use `ollama launch` to start coding agents inside a sandbox. - Expose a host-level Ollama server to sandboxes through `inference.local`. ## Prerequisites - A working OpenShell installation. Complete the [Quickstart](/get-started/quickstart) before proceeding. ## Option A: Ollama Community Sandbox (Recommended) The Ollama community sandbox bundles Ollama, Claude Code, OpenCode, and Codex into a single image. Ollama starts automatically when the sandbox launches. ### Create the Sandbox ```shell openshell sandbox create --from ollama ``` This pulls the community sandbox image, applies the bundled policy, and drops you into a shell with Ollama running. ### Chat with a Model Chat with a local model ```shell ollama run qwen3.5 ``` Or a cloud model ```shell ollama run kimi-k2.5:cloud ``` Or use `ollama launch` to start a coding agent with Ollama as the model backend: ```shell ollama launch claude ollama launch codex ollama launch opencode ``` For CI/CD and automated workflows, `ollama launch` supports a headless mode: ```shell ollama launch claude --yes --model qwen3.5 ``` ### Model Recommendations | Use case | Model | Notes | |---|---|---| | Smoke test | `qwen3.5:0.8b` | Fast, lightweight, good for verifying setup | | Coding and reasoning | `qwen3.5` | Strong tool calling support for agentic workflows | | Complex tasks | `nemotron-3-super` | 122B parameter model, needs 48GB+ VRAM | | No local GPU | `qwen3.5:cloud` | Runs on Ollama's cloud infrastructure, no `ollama pull` required | Cloud models use the `:cloud` tag suffix and do not require local hardware. ```shell openshell sandbox create --from ollama ``` ### Tool Calling Agentic workflows (Claude Code, Codex, OpenCode) rely on tool calling. The following models have reliable tool calling support: Qwen 3.5, Nemotron-3-Super, GLM-5, and Kimi-K2.5. Check the [Ollama model library](https://ollama.com/library) for the latest models. ### Updating Ollama To update Ollama inside a running sandbox: ```shell update-ollama ``` Or auto-update on every sandbox start: ```shell openshell sandbox create --from ollama -e OLLAMA_UPDATE=1 ``` ## Option B: Host-Level Ollama Use this approach when you want a single Ollama instance on the gateway host, shared across multiple sandboxes through `inference.local`. This approach uses Ollama because it is easy to install and run locally, but you can substitute other inference engines such as vLLM, SGLang, TRT-LLM, and NVIDIA NIM by changing the startup command, base URL, and model name. ### Install and Start Ollama Install [Ollama](https://ollama.com/) on the gateway host: ```shell curl -fsSL https://ollama.com/install.sh | sh ``` Start Ollama on all interfaces so it is reachable from sandboxes: ```shell OLLAMA_HOST=0.0.0.0:11434 ollama serve ``` If you see `Error: listen tcp 0.0.0.0:11434: bind: address already in use`, Ollama is already running as a system service. Stop it first: ```shell systemctl stop ollama OLLAMA_HOST=0.0.0.0:11434 ollama serve ``` ### Pull a Model In a second terminal, pull a model: ```shell ollama run qwen3.5:0.8b ``` Type `/bye` to exit the interactive session. The model stays loaded. ### Create a Provider Create an OpenAI-compatible provider pointing at the host Ollama: ```shell openshell provider create \ --name ollama \ --type openai \ --credential OPENAI_API_KEY=empty \ --config OPENAI_BASE_URL=http://host.openshell.internal:11434/v1 ``` OpenShell injects `host.openshell.internal` so sandboxes and the gateway can reach the host machine. You can also use the host's LAN IP. ### Set Inference Routing ```shell openshell inference set --provider ollama --model qwen3.5:0.8b ``` Confirm: ```shell openshell inference get ``` ### Verify from a Sandbox ```shell openshell sandbox create -- \ curl https://inference.local/v1/chat/completions \ --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' ``` The response should be JSON from the model. ## Troubleshooting Common issues and fixes: - **Ollama not reachable from sandbox:** Ollama must be bound to `0.0.0.0`, not `127.0.0.1`. This applies to host-level Ollama only; the community sandbox handles this automatically. - **`OPENAI_BASE_URL` wrong:** Use `http://host.openshell.internal:11434/v1`, not `localhost` or `127.0.0.1`. - **Model not found:** Run `ollama ps` to confirm the model is loaded. Run `ollama pull ` if needed. - **HTTPS instead of HTTP:** Code inside sandboxes must call `https://inference.local`, not `http://`. - **AMD GPU driver issues:** Ollama v0.18+ requires ROCm 7 drivers for AMD GPUs. Update your drivers if you see GPU detection failures. Useful commands: ```shell openshell status openshell inference get openshell provider get ollama ``` ## Next Steps - To learn more about managed inference, refer to [Inference Routing](/sandboxes/inference-routing). - To configure a different self-hosted backend, refer to [Inference Routing](/sandboxes/inference-routing#configure-inference-routing). - To learn how sandbox containers are selected, refer to [Sandboxes](/sandboxes/manage-sandboxes#custom-containers). --- ### Get Started/Tutorials/Local Inference Lmstudio --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Route Local Inference Requests to LM Studio" sidebar-title: "Local Inference with LM Studio" slug: "get-started/tutorials/local-inference-lmstudio" description: "Configure inference.local to route sandbox requests to a local LM Studio server running on the gateway host." keywords: "Generative AI, Cybersecurity, Tutorial, Inference Routing, LM Studio, Local Inference, Sandbox" --- This tutorial describes how to configure OpenShell to route inference requests to a local LM Studio server. The LM Studio server provides easy setup with both OpenAI and Anthropic compatible endpoints. This tutorial covers: - Expose a local inference server to OpenShell sandboxes. - Verify end-to-end inference from inside a sandbox. ## Prerequisites First, complete OpenShell installation and follow the [Quickstart](/get-started/quickstart). [Install the LM Studio app](https://lmstudio.ai/download). Make sure that your LM Studio is running in the same environment as your gateway. If you prefer to work without having to keep the LM Studio app open, download llmster (headless LM Studio) with the following command: ```shell curl -fsSL https://lmstudio.ai/install.sh | bash ``` ```shell irm https://lmstudio.ai/install.ps1 | iex ``` And start llmster: ```shell lms daemon up ``` ## Start LM Studio Local Server Start the LM Studio local server from the Developer tab, and verify the OpenAI-compatible endpoint is enabled. LM Studio listens to `127.0.0.1:1234` by default. For use with OpenShell, configure LM Studio to listen on all interfaces (`0.0.0.0`). If you use the GUI, go to the Developer Tab, select Server Settings, then enable Serve on Local Network. If you use llmster in headless mode, run `lms server start --bind 0.0.0.0`. ## Test with a small model In the LM Studio app, head to the Model Search tab to download a small model like Qwen3.5 2B. In the terminal, use the following command to download and load the model: ```shell lms get qwen/qwen3.5-2b lms load qwen/qwen3.5-2b ``` ## Add LM Studio as a provider Choose the provider type that matches the client protocol you want to route through `inference.local`. Add LM Studio as an OpenAI-compatible provider through `host.openshell.internal`: ```shell openshell provider create \ --name lmstudio \ --type openai \ --credential OPENAI_API_KEY=lmstudio \ --config OPENAI_BASE_URL=http://host.openshell.internal:1234/v1 ``` Use this provider for clients that send OpenAI-compatible requests such as `POST /v1/chat/completions` or `POST /v1/responses`. Add a provider that points to LM Studio's Anthropic-compatible `POST /v1/messages` endpoint: ```shell openshell provider create \ --name lmstudio-anthropic \ --type anthropic \ --credential ANTHROPIC_API_KEY=lmstudio \ --config ANTHROPIC_BASE_URL=http://host.openshell.internal:1234 ``` Use this provider for Anthropic-compatible `POST /v1/messages` requests. ## Configure LM Studio as the local inference provider Set the managed inference route for the active gateway:
```shell openshell inference set --provider lmstudio --model qwen/qwen3.5-2b ``` If the command succeeds, OpenShell has verified that the upstream is reachable and accepts the expected OpenAI-compatible request shape. ```shell openshell inference set --provider lmstudio-anthropic --model qwen/qwen3.5-2b ``` If the command succeeds, OpenShell has verified that the upstream is reachable and accepts the expected Anthropic-compatible request shape.
The active `inference.local` route is gateway-scoped, so only one provider and model pair is active at a time. Re-run `openshell inference set` whenever you want to switch between OpenAI-compatible and Anthropic-compatible clients. Confirm the saved config: ```shell openshell inference get ``` You should see either `Provider: lmstudio` or `Provider: lmstudio-anthropic`, along with `Model: qwen/qwen3.5-2b`. ## Verify from Inside a Sandbox Run a simple request through `https://inference.local`: ```shell showLineNumbers={true} openshell sandbox create -- \ curl https://inference.local/v1/chat/completions \ --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' openshell sandbox create -- \ curl https://inference.local/v1/responses \ --json '{ "instructions": "You are a helpful assistant.", "input": "hello", "max_output_tokens": 10 }' ``` ```shell openshell sandbox create -- \ curl https://inference.local/v1/messages \ --json '{"messages":[{"role":"user","content":"hello"}],"max_tokens":10}' ```
## Troubleshooting If setup fails, check these first: - LM Studio local server is running and reachable from the gateway host - `OPENAI_BASE_URL` uses `http://host.openshell.internal:1234/v1` when you use an `openai` provider - `ANTHROPIC_BASE_URL` uses `http://host.openshell.internal:1234` when you use an `anthropic` provider - The gateway and LM Studio run on the same machine or a reachable network path - The configured model name matches the model exposed by LM Studio Useful commands: ```shell openshell status openshell inference get openshell provider get lmstudio openshell provider get lmstudio-anthropic ``` ## Next Steps - To learn more about using the LM Studio CLI, refer to [LM Studio docs](https://lmstudio.ai/docs/cli) - To learn more about managed inference, refer to [Inference Routing](/sandboxes/inference-routing). - To configure a different self-hosted backend, refer to [Inference Routing](/sandboxes/inference-routing#configure-inference-routing). --- ### Get Started/Tutorials/Microsoft Graph Provider Refresh --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Refresh Microsoft Graph Credentials with Providers v2" sidebar-title: "Microsoft Graph Provider Refresh" slug: "get-started/tutorials/microsoft-graph-provider-refresh" description: "Configure a Providers v2 Microsoft Graph profile with gateway-managed OAuth2 refresh-token rotation." keywords: "Generative AI, Cybersecurity, Tutorial, Providers, Microsoft Graph, OAuth2, Credential Refresh, Sandbox" --- Use Providers v2 to keep Microsoft Graph access tokens short lived while sandboxes receive a stable `MS_GRAPH_ACCESS_TOKEN` placeholder. OpenShell stores the non-injectable refresh material at the gateway, refreshes the Microsoft Graph access token before it expires, updates the provider record, and injects the current credential into newly launched sandbox processes. After completing this tutorial, you have: - A custom Microsoft Graph mail provider profile. - A provider instance configured with `oauth2-refresh-token`. - A sandbox that can use `curl` to read Microsoft Graph mail through provider-owned policy. This tutorial starts after your OAuth client has already completed the initial Microsoft sign-in flow. It does not publish a token bootstrap script. Use the Microsoft identity platform documentation for the [device authorization grant flow](https://learn.microsoft.com/en-ie/entra/identity-platform/v2-oauth2-device-code) or [authorization code flow](https://learn.microsoft.com/en-us/entra/identity-platform/v2-oauth2-auth-code-flow), and use any standards-compliant client that returns an access token, refresh token, and expiry. ## Prerequisites - A working OpenShell installation with an active gateway. Complete the [Quickstart](/get-started/quickstart) before proceeding. - A Microsoft Entra app registration that can acquire delegated Microsoft Graph mail access. - Delegated Microsoft Graph mail permission for the signed-in user. `Mail.Read` allows reading the signed-in user's mailbox; see the [Microsoft Graph permissions reference](https://learn.microsoft.com/en-us/graph/permissions-reference). OAuth material from your initial Microsoft sign-in flow: | Variable | Value | |---|---| | `MS_TENANT_ID` | Microsoft Entra tenant ID, domain, or `common`. | | `MS_CLIENT_ID` | Microsoft Entra application client ID. | | `MS_GRAPH_ACCESS_TOKEN` | Current delegated Microsoft Graph access token. | | `MS_GRAPH_REFRESH_TOKEN` | Delegated OAuth refresh token. | | `MS_GRAPH_ACCESS_TOKEN_EXPIRES_AT` | Absolute expiry for the current access token. | `MS_GRAPH_ACCESS_TOKEN_EXPIRES_AT` can be an RFC3339 timestamp such as `2026-01-01T00:00:00Z` or a Unix epoch millisecond timestamp. Do not commit access tokens, refresh tokens, or local `.env` files. The commands below pass token material to the gateway; they are not examples of values to store in source control. ## Enable Providers v2 Enable provider profile policy composition on the active gateway: ```shell openshell settings set --global --key providers_v2_enabled --value true --yes ``` ## Create a Microsoft Graph Provider Profile Create `microsoft-graph-mail.yaml` with this profile: ```yaml showLineNumbers={false} # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 id: microsoft-graph-mail display_name: Microsoft Graph Mail description: Delegated Microsoft Graph mail read access category: messaging credentials: - name: graph_access_token description: Microsoft Graph delegated access token env_vars: [MS_GRAPH_ACCESS_TOKEN] required: true auth_style: bearer header_name: authorization refresh: strategy: oauth2_refresh_token token_url: https://login.microsoftonline.com/common/oauth2/v2.0/token scopes: [https://graph.microsoft.com/.default] refresh_before_seconds: 600 max_lifetime_seconds: 3600 material: - name: tenant_id description: Microsoft Entra tenant ID required: true - name: client_id description: Microsoft Entra application client ID required: true - name: refresh_token description: Delegated OAuth refresh token required: true secret: true endpoints: - host: graph.microsoft.com port: 443 protocol: rest access: read-only enforcement: enforce binaries: - /usr/bin/curl - /usr/local/bin/curl ``` Lint and import the profile: ```shell openshell provider profile lint -f microsoft-graph-mail.yaml openshell provider profile import -f microsoft-graph-mail.yaml ``` The profile defines the refresh strategy and Graph network policy. The `tenant_id` refresh material selects the Microsoft token endpoint during gateway-managed refresh. ## Create the Provider Create the provider with the current Microsoft Graph access token: ```shell openshell provider create \ --name microsoft-mail \ --type microsoft-graph-mail \ --credential MS_GRAPH_ACCESS_TOKEN="$MS_GRAPH_ACCESS_TOKEN" ``` The current CLI requires an initial credential at provider creation time. Refresh material is configured separately and is not injected into the sandbox. ## Configure Refresh Configure gateway-managed OAuth2 refresh-token rotation: ```shell openshell provider refresh configure microsoft-mail \ --credential-key MS_GRAPH_ACCESS_TOKEN \ --strategy oauth2-refresh-token \ --material tenant_id="$MS_TENANT_ID" \ --material client_id="$MS_CLIENT_ID" \ --material refresh_token="$MS_GRAPH_REFRESH_TOKEN" \ --secret-material-key refresh_token \ --credential-expires-at "$MS_GRAPH_ACCESS_TOKEN_EXPIRES_AT" ``` `--secret-material-key refresh_token` names the material key to mark as sensitive. It is not the refresh-token value. If Microsoft returns a rotated refresh token during refresh, OpenShell stores the new `refresh_token` material and marks it secret automatically. Force the first refresh immediately: ```shell openshell provider refresh rotate microsoft-mail \ --credential-key MS_GRAPH_ACCESS_TOKEN ``` Check refresh status: ```shell openshell provider refresh status microsoft-mail \ --credential-key MS_GRAPH_ACCESS_TOKEN ``` The status output shows refresh state, expiry, next refresh, and last refresh timing. It does not print access-token values or refresh material. ## Launch a Sandbox Launch a sandbox with the Microsoft Graph provider attached: ```shell openshell sandbox create \ --name microsoft-graph-mail \ --provider microsoft-mail \ --no-auto-providers \ -- /bin/sh ``` Provider policy allows `curl` to reach `graph.microsoft.com:443`. The sandbox process receives `MS_GRAPH_ACCESS_TOKEN` as an OpenShell placeholder, and the proxy resolves that placeholder to the current gateway-managed access token when `curl` sends it in the authorization header. ## Verify Microsoft Graph Access Inside the sandbox, list a small page of mailbox messages: ```shell curl -sS \ -H "Authorization: Bearer $MS_GRAPH_ACCESS_TOKEN" \ 'https://graph.microsoft.com/v1.0/me/messages?$select=sender,subject&$top=5' ``` The request uses the [Microsoft Graph list messages API](https://learn.microsoft.com/en-us/graph/api/user-list-messages?view=graph-rest-1.0). If the token has delegated mail read permission, Microsoft Graph returns message metadata for the signed-in user's mailbox. ## Update Running Sandboxes Provider refresh updates the provider record at the gateway. Running sandboxes poll for provider environment revisions, but already-running processes keep the environment they started with. If you attach this provider to an existing sandbox or update provider credentials after a process has already started, launch a new process inside the sandbox before expecting `MS_GRAPH_ACCESS_TOKEN` to appear in that process environment. --- ### Get Started/Quickstart --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Quickstart" description: "Install the OpenShell CLI, connect to a gateway, and create your first sandboxed AI agent." keywords: "Generative AI, Cybersecurity, AI Agents, Sandboxing, Installation, Quickstart, Gateway, Docker, Kubernetes, Podman" position: 1 --- This page gets you from a reachable OpenShell gateway to a running, policy-enforced sandbox. ## Prerequisites Before you begin, make sure you have: - A reachable OpenShell gateway. - At least one compute driver configured for the gateway: Kubernetes, Docker, Podman, or MicroVM. - The OpenShell CLI installed on your workstation. For a complete list of requirements, refer to [Support Matrix](/reference/support-matrix). If you have not chosen a compute driver yet, refer to [Installation](/about/installation). ## Install the OpenShell CLI Run the install script: ```shell curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | sh ``` The install script uses Homebrew, RPM, or a Debian package based on your machine. It starts the local gateway server after installation. If you prefer [uv](https://docs.astral.sh/uv/): ```shell uv tool install -U openshell ``` After installing the CLI, run `openshell --help` in your terminal to view the full CLI reference. You can also clone the [NVIDIA OpenShell GitHub repository](https://github.com/NVIDIA/OpenShell) and use the `/openshell-cli` skill to load the CLI reference into your agent. ## Create Your First OpenShell Sandbox Create a sandbox and launch an agent inside it. Choose the tab that matches your agent: Run the following command to create a sandbox with Claude Code: ```shell openshell sandbox create -- claude ``` The CLI prompts you to create a provider from local credentials. Type `yes` to continue. If `ANTHROPIC_API_KEY` is set in your environment, the CLI picks it up automatically. If not, you can configure it from inside the sandbox after it launches. `ANTHROPIC_API_KEY` is an API key from [console.anthropic.com](https://console.anthropic.com), not a subscription token. Subscription users must generate a separate API key. Run the following command to create a sandbox with OpenCode: ```shell openshell sandbox create -- opencode ``` The CLI prompts you to create a provider from local credentials. Type `yes` to continue. If `OPENAI_API_KEY` or `OPENROUTER_API_KEY` is set in your environment, the CLI picks it up automatically. If not, you can configure it from inside the sandbox after it launches. Run the following command to create a sandbox with Codex: ```shell openshell sandbox create -- codex ``` The CLI prompts you to create a provider from local credentials. Type `yes` to continue. If `OPENAI_API_KEY` is set in your environment, the CLI picks it up automatically. If not, you can configure it from inside the sandbox after it launches. Use the `--from` flag to create a sandbox from the base container: ```shell openshell sandbox create --from base ``` --- ### Kubernetes/Access Control --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Access Control" sidebar-title: "Access Control" description: "Configure OIDC user authentication or reverse-proxy auth termination for a Kubernetes-deployed OpenShell gateway." keywords: "Generative AI, Cybersecurity, Kubernetes, Authentication, mTLS, OIDC, Keycloak, Entra ID, Okta, Gateway Auth" position: 5 --- The OpenShell gateway supports two access-control models for human callers on Kubernetes: | Model | When to use | |---|---| | OIDC (recommended) | Production deployments. Integrates with an existing identity provider, supports role-based access control, and gives each user their own identity without distributing certificates. | | Reverse-proxy auth termination | An access proxy (Cloudflare Access, ngrok, corporate SSO) authenticates callers in front of the gateway. The gateway trusts the proxy and skips its own client-cert check. | The Helm chart always generates mTLS certificates at install time. The gateway uses them for transport-layer security regardless of which access-control model you choose. The client bundle in the `openshell-client-tls` secret is used internally by sandbox supervisors, not for granting access to individual users. For how the CLI resolves gateways and stores credentials, refer to [Gateway Authentication](/reference/gateway-auth). ## Sandbox Supervisor Identity Kubernetes sandbox supervisors authenticate back to the gateway as sandbox workloads. By default, the gateway mints its own sandbox JWTs and Kubernetes sandboxes bootstrap them with a projected ServiceAccount token. Dynamic provider token grants can use SPIFFE without changing supervisor-to-gateway authentication. Set `server.providerTokenGrants.spiffe.enabled=true` to mount the SPIFFE CSI Workload API socket into sandbox pods while keeping the projected ServiceAccount token bootstrap and gateway-minted sandbox JWT path. Provider token grants require a SPIFFE implementation such as SPIRE and a `ClusterSPIFFEID` that assigns per-sandbox IDs from the pod's `openshell.io/sandbox-id` annotation. Provider profiles with `token_grant` metadata cause the sandbox supervisor to request JWT-SVIDs and exchange them for upstream OAuth2 access tokens. ## OIDC User Authentication Set `server.oidc.issuer` to enable OIDC. The gateway validates the `Authorization: Bearer ` header on every request against the issuer's JWKS endpoint. ```shell helm upgrade openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set server.oidc.issuer=https://your-idp.example.com/realms/openshell \ --set server.oidc.audience=openshell-cli ``` The `audience` value must match the client ID configured in your identity provider for the OpenShell resource server. ### OIDC values reference | Value | Default | Purpose | |---|---|---| | `server.oidc.issuer` | `""` | OIDC issuer URL. Empty disables OIDC. | | `server.oidc.audience` | `openshell-cli` | Expected `aud` claim in the JWT. | | `server.oidc.jwksTtl` | `3600` | JWKS key cache TTL in seconds. | | `server.oidc.rolesClaim` | `""` | Dot-separated path to the roles array in JWT claims. | | `server.oidc.adminRole` | `""` | Role name that grants admin access. | | `server.oidc.userRole` | `""` | Role name that grants standard user access. | | `server.oidc.scopesClaim` | `""` | Dot-separated path to the scopes array in JWT claims. | ### Auth-only mode vs. RBAC mode Leave both `adminRole` and `userRole` empty to use auth-only mode: any request with a valid JWT from the configured issuer is accepted, but no role distinction is enforced. Set both values to enable RBAC mode, where the gateway checks the role claim and enforces access based on the assigned role: ```shell helm upgrade openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set server.oidc.issuer=https://your-idp.example.com/realms/openshell \ --set server.oidc.audience=openshell-cli \ --set server.oidc.rolesClaim=realm_access.roles \ --set server.oidc.adminRole=openshell-admin \ --set server.oidc.userRole=openshell-user ``` Both `adminRole` and `userRole` must be set, or both must be empty. Setting only one is not supported. ### Provider-specific rolesClaim paths | Provider | rolesClaim value | |---|---| | Keycloak | `realm_access.roles` | | Microsoft Entra ID | `roles` | | Okta | `groups` | ## Reverse-Proxy Auth Termination When an access proxy, such as Cloudflare Access, ngrok, or a corporate SSO gateway, handles authentication in front of the OpenShell gateway, you can explicitly allow unauthenticated user calls at the gateway: ```shell helm upgrade openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set server.auth.allowUnauthenticatedUsers=true ``` The gateway still serves TLS and sandbox supervisors still authenticate with gateway-minted sandbox JWTs. User-facing CLI/API calls without OIDC or mTLS credentials are accepted as an unauthenticated local developer principal. The proxy is responsible for authenticating callers and forwarding only authorized traffic. To also disable TLS entirely (when the proxy terminates TLS before the request reaches the gateway): ```shell --set server.disableTls=true \ --set server.auth.allowUnauthenticatedUsers=true ``` Only enable unauthenticated users when the gateway is not reachable from outside a trusted local development environment or the proxy path is fully trusted. Never expose a plaintext, auth-disabled gateway to a public network. Register the gateway with the CLI using the proxy's public URL. The browser-based login flow runs automatically on first use: ```shell openshell gateway add https://gateway.example.com --name production ``` --- ### Kubernetes/Ingress --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Ingress" sidebar-title: "Ingress" description: "Expose the OpenShell gateway externally using the Kubernetes Gateway API and a GRPCRoute." keywords: "Generative AI, Cybersecurity, Kubernetes, Gateway API, Envoy Gateway, GRPCRoute, Ingress, External Access" position: 4 --- By default, the OpenShell gateway is only reachable inside the cluster. To let CLI clients connect without a `kubectl port-forward`, expose the gateway through an ingress. OpenShell uses the [Kubernetes Gateway API](https://gateway-api.sigs.k8s.io) for ingress. The chart creates a `GRPCRoute` that routes inbound gRPC traffic to the gateway pod. You need a Gateway API implementation installed on your cluster to fulfill the `GRPCRoute`. This page uses [Envoy Gateway](https://gateway.envoyproxy.io), which the chart is tested with. ## Install Envoy Gateway Envoy Gateway installs the Gateway API CRDs and controller: ```shell helm install eg \ oci://docker.io/envoyproxy/gateway-helm \ --version v1.8.1 \ --namespace envoy-gateway-system \ --create-namespace \ --wait ``` ## Create the GatewayClass Create the `eg` GatewayClass that the OpenShell chart references: ```shell kubectl apply -f - <<'EOF' apiVersion: gateway.networking.k8s.io/v1 kind: GatewayClass metadata: name: eg spec: controllerName: gateway.envoyproxy.io/gatewayclass-controller EOF ``` Verify the GatewayClass is accepted: ```shell kubectl get gatewayclass eg ``` The `ACCEPTED` column should show `True`. ## Install OpenShell with Gateway API enabled Enable the GRPCRoute and let the chart create a Gateway resource in the `openshell` namespace: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set grpcRoute.enabled=true \ --set grpcRoute.gateway.create=true \ --set grpcRoute.gateway.className=eg ``` ## Get the external address After the Gateway is provisioned, Envoy Gateway creates a LoadBalancer service in the `openshell` namespace. Wait for it to get an external address: ```shell kubectl -n openshell get svc -l gateway.envoyproxy.io/owning-gateway-name=openshell ``` After the `EXTERNAL-IP` is assigned, register the gateway with the CLI: ```shell openshell gateway add http:// --name production openshell status ``` This setup is plaintext end-to-end and is intended for development. For external access, terminate TLS at the gateway as shown below. ## HTTPS (TLS termination) Envoy Gateway can terminate TLS at the listener and forward plaintext to the OpenShell gateway pod: ```text client → HTTPS → Envoy Gateway (terminate TLS) → plaintext → openshell gateway pod ``` Envoy Gateway only terminates TLS here — it does not perform OIDC. Do not enable an Envoy Gateway OIDC `SecurityPolicy` in front of the gateway: that flow relies on browser redirects and cookies and cannot work with the OpenShell CLI or headless agents. Instead, the OpenShell gateway validates an OIDC bearer token that the client sends in the gRPC `authorization` metadata, which Envoy forwards untouched. Because Envoy terminates TLS, the OpenShell gateway never sees a client certificate, so client mTLS cannot provide identity on this path. Use OIDC bearer tokens for client identity instead. For headless agents and CI, the CLI obtains the token via the OAuth2 client-credentials grant (no browser): set `OPENSHELL_OIDC_CLIENT_SECRET` to the OAuth client secret before running `openshell gateway add`. The client id comes from `--oidc-client-id` (default `openshell-cli`); pass it explicitly if your IdP client uses a different id. Interactive human users get the browser-based Authorization Code + PKCE flow by default. ### Provide a TLS certificate Create a `kubernetes.io/tls` Secret in the `openshell` namespace with the certificate for your external hostname: ```shell kubectl -n openshell create secret tls openshell-ingress-tls \ --cert=tls.crt --key=tls.key ``` The Secret may also be issued by cert-manager, or you can reference the chart's existing `openshell-server-tls` Secret if its SANs include the external hostname. ### Install with HTTPS termination Enable an HTTPS listener, point it at the Secret, disable gateway-pod TLS so Envoy forwards plaintext, and configure an OIDC issuer for client identity: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set grpcRoute.enabled=true \ --set grpcRoute.gateway.create=true \ --set grpcRoute.gateway.className=eg \ --set grpcRoute.gateway.listener.protocol=HTTPS \ --set grpcRoute.gateway.listener.port=443 \ --set 'grpcRoute.gateway.listener.tls.certificateRefs[0].name=openshell-ingress-tls' \ --set server.disableTls=true \ --set server.oidc.issuer=https:// \ --set 'grpcRoute.hostnames[0]=' ``` Keep the certificate Secret in the release namespace. Referencing a Secret in another namespace requires a `ReferenceGrant`. ### Register over HTTPS ```shell openshell gateway add https:// --name production --oidc-issuer https:// openshell status ``` See [Authentication](/kubernetes/setup) for OIDC issuer, audience, and roles configuration. ## SSH Relay Sandbox SSH uses the gateway endpoint registered with the CLI. No separate Helm SSH host or port values are required. ## Next Steps Return to [Setup](/kubernetes/setup) to complete the installation. --- ### Kubernetes/Managing Certificates --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Managing Certificates" sidebar-title: "Managing Certificates" description: "Configure the OpenShell Helm chart to use cert-manager for mTLS certificate issuance and automatic renewal." keywords: "Generative AI, Cybersecurity, Kubernetes, cert-manager, PKI, TLS, mTLS, Certificates" position: 3 --- The OpenShell gateway uses mTLS certificates for transport between the gateway and sandbox supervisors. These certificates are not Kubernetes user authentication; configure OIDC or a trusted access proxy for user access. The Helm chart supports two ways to provision and manage the certificate bundle: | Mode | When to use | |---|---| | Built-in `pkiInitJob` (default) | The default path. A pre-install Kubernetes Job generates a self-signed CA and certificates during installation. No additional dependencies. | | cert-manager | Production deployments that need automatic certificate rotation managed by a running controller. | The rest of this page covers switching to cert-manager. The built-in mode requires no configuration. When `certManager.enabled=true`, cert-manager owns TLS certificate generation. The chart still runs a JWT-only initialization hook because cert-manager does not create the sandbox JWT signing Secret required by the gateway. This cert-manager precedence applies even if `pkiInitJob.enabled` remains true. ## Install cert-manager Install cert-manager from the OCI registry with CRD support enabled: ```shell helm upgrade --install cert-manager oci://quay.io/jetstack/charts/cert-manager \ --version v1.20.3 \ --namespace cert-manager \ --create-namespace \ --set crds.enabled=true \ --wait ``` Verify the cert-manager pods are running: ```shell kubectl -n cert-manager get pods ``` ## Install OpenShell with cert-manager PKI Pass the cert-manager values override when installing or upgrading the chart: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set certManager.enabled=true ``` The chart creates a self-signed CA, issues server and client certificates from it, and cert-manager handles renewal before expiry. The chart also runs a pre-install hook in JWT-only mode to create the gateway's sandbox JWT signing Secret. That Secret is separate from the cert-manager TLS certificate Secrets and is mounted at `/etc/openshell-jwt`. ## Using a real Issuer for the server certificate By default, cert-manager issues both the server and client certificates from a self-signed CA the chart creates — this rotates automatically, but the server certificate is still not publicly trusted. `certManager.serverIssuerRef` overrides the `issuerRef` on the server `Certificate` resource to point at a real `Issuer` or `ClusterIssuer` instead, for example an ACME issuer: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set certManager.enabled=true \ --set certManager.serverIssuerRef.name=letsencrypt-prod \ --set certManager.serverIssuerRef.kind=ClusterIssuer \ --set certManager.serverDnsNames[0]=openshell.example.com ``` ### Dual certificate architecture When `serverIssuerRef` is set, the chart creates **two** server certificates: 1. **Internal certificate** (`openshell-server-tls`): signed by the chart CA with internal SANs (`*.svc.cluster.local`, `localhost`, etc.). 2. **External certificate** (`openshell-server-external-tls`): signed by the configured issuer (e.g. ACME) with only the hostnames from `certManager.serverDnsNames`. The gateway uses **SNI** to select which certificate to present: supervisors connect via internal service names and receive the internal certificate (verified against the chart CA they already trust), while CLI users connecting through a Route or ingress use the external hostname and receive the ACME certificate. This keeps supervisor trust pinned to only the operator's chart CA — no WebPKI root trust is needed. Public CAs such as Let's Encrypt reject certificate requests that include internal-only names per CA/Browser Forum baseline requirements. The chart validates this at install time and fails with an actionable error if `certManager.serverDnsNames` contains internal-only entries while `serverIssuerRef` is set. You do **not** need to set `server.grpcEndpoint` to the external hostname. Supervisors connect via the internal service name automatically. Setting `server.grpcEndpoint` to an external hostname would cause supervisors to receive the ACME certificate (via SNI) which they cannot verify against the chart CA. The default `clientCaFromServerTlsSecret=true` is correct even when `serverIssuerRef` is set: the internal server certificate is always signed by the chart CA (the same CA that signs the client certificate), so its `ca.crt` is the right trust anchor for mTLS verification. ## Next Steps Return to [Setup](/kubernetes/setup) to complete the installation. For exposing the gateway externally on OpenShift with a real certificate, see [OpenShift](/kubernetes/openshift). --- ### Kubernetes/Openshift --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "OpenShift" sidebar-title: "OpenShift" description: "Install the OpenShell Helm chart on OpenShift, including the SCC binding and chart overrides required by OpenShift's Security Context Constraints." keywords: "Generative AI, Cybersecurity, Kubernetes, OpenShift, SCC, Security Context Constraints, Helm, Gateway, Installation" position: 6 --- The OpenShift install path is experimental. It currently requires running sandbox pods under the `privileged` SCC and installing the gateway with TLS disabled. Use only for evaluation on a private network. OpenShift's [Security Context Constraints](https://docs.openshift.com/container-platform/latest/authentication/managing-security-context-constraints.html) reject the chart's default pod security settings. Installing on OpenShift requires precreating the namespace, granting the `privileged` SCC to the sandbox service account, and overriding a few chart values so the cluster admission controller can assign UIDs and FS groups itself. OpenShell installs sandbox nftables rules as individual commands. On OpenShift nodes where optional conntrack or packet log expressions are unavailable, those optional rules can fail without rolling back the required proxy bypass reject rules. ## Prerequisites - OpenShift 4.x cluster with `oc` configured - Helm 3.x - [Agent Sandbox](/kubernetes/setup#install-agent-sandbox) controller and CRDs installed ## Install ## Create the namespace Pre-create the namespace so the SCC binding can be applied before the chart installs: ```shell oc create ns openshell ``` ## Grant the privileged SCC to sandbox pods Sandbox pods run under the `openshell-sandbox` service account in the `openshell` namespace and require the `privileged` SCC: ```shell oc adm policy add-scc-to-user privileged -z openshell-sandbox -n openshell ``` ## Install the chart with OpenShift overrides ```shell helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set server.disableTls=true \ --set podSecurityContext.fsGroup=null \ --set securityContext.runAsUser=null ``` | Override | Reason | |---|---| | `server.disableTls=true` | Runs the gateway over plaintext HTTP for simpler evaluation. | | `podSecurityContext.fsGroup=null` / `securityContext.runAsUser=null` | Clear the chart's hardcoded UID and fsGroup so OpenShift's SCC admission can assign them. | ## Wait for the gateway to be ready ```shell oc -n openshell rollout status statefulset/openshell ``` If you set `workload.kind=deployment`, use `oc -n openshell rollout status deployment/openshell` instead. ## Connect to the gateway The gateway is now running over plaintext HTTP. Connect with `oc port-forward`: ```shell oc -n openshell port-forward svc/openshell 8080:8080 ``` Register the gateway with the CLI: ```shell openshell gateway add http://127.0.0.1:8080 --local --name openshift openshell status ``` ## Production: expose externally with a real certificate The steps above run the gateway over plaintext HTTP for quick evaluation. For a real deployment, cert-manager can issue the gateway's server certificate from a real Issuer or ClusterIssuer (for example, an ACME issuer), and an OpenShift Route with TLS passthrough exposes it externally while the gateway keeps terminating its own TLS and mTLS. Install cert-manager and configure a working `ClusterIssuer` first — see [Managing Certificates](/kubernetes/managing-certificates) for the `certManager.serverIssuerRef` details. Configure an OIDC provider as described in [Access Control](/kubernetes/access-control) — remote gateways authenticate CLI users via OIDC, not mTLS, so the gateway must know the OIDC issuer URL. Install the chart with: ```shell helm install openshell oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set podSecurityContext.fsGroup=null \ --set securityContext.runAsUser=null \ --set server.disableTls=false \ --set certManager.enabled=true \ --set certManager.serverIssuerRef.name= \ --set certManager.serverIssuerRef.kind=ClusterIssuer \ --set certManager.serverDnsNames[0]= \ --set openshiftRoute.enabled=true \ --set openshiftRoute.host= \ --set server.oidc.issuer= \ --set server.oidc.audience= ``` | Override | Reason | |---|---| | `certManager.serverIssuerRef` | Creates a second server certificate from your Issuer or ClusterIssuer for external clients. The gateway uses SNI to present this cert for the external hostname while continuing to present the internal (chart CA) cert to supervisors. The internal certificate's `ca.crt` is the chart CA that also signed the client cert, so the default `clientCaFromServerTlsSecret=true` is correct. | | `openshiftRoute.enabled` / `openshiftRoute.host` | Creates an OpenShift Route with TLS passthrough — the router forwards the encrypted connection by SNI without decrypting, so the gateway uses the SNI hostname to select the external certificate. | | `server.oidc.issuer` / `server.oidc.audience` | Configures server-side OIDC validation. Without these, the gateway expects mTLS client certificates and rejects OIDC-only CLI connections. See [Access Control](/kubernetes/access-control). | Register the gateway with the CLI over OIDC. Remote gateways authenticate CLI users via OIDC, not mTLS — see [Access Control](/kubernetes/access-control): ```shell openshell gateway add https:// \ --name openshift \ --oidc-issuer openshell gateway login openshift ``` ## Next Steps - For more on certificate provisioning modes, refer to [Managing Certificates](/kubernetes/managing-certificates). - To expose the gateway externally through the Kubernetes Gateway API instead of a Route, refer to [Ingress](/kubernetes/ingress). - To configure OIDC authentication, refer to [Access Control](/kubernetes/access-control). --- ### Kubernetes/Setup --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Set Up OpenShell on Kubernetes" sidebar-title: "Setup" description: "Deploy the OpenShell gateway to a Kubernetes cluster using the official Helm chart from GHCR." keywords: "Generative AI, Cybersecurity, Kubernetes, Helm, Gateway, Deployment, OCI, GHCR, Installation" position: 1 --- The OpenShell Helm chart is experimental and under active development. Templates, values, and defaults can change between releases. Do not use it in production. Use the Kubernetes deployment when the gateway should run on a shared cluster, in a cloud environment, or as part of team infrastructure. The Helm chart handles PKI bootstrap, RBAC, sandbox namespace setup, and the gateway workload. It uses a StatefulSet by default for the SQLite database, and can render a Deployment when `server.externalDbSecret` points at an external database. ## Prerequisites Make sure the following are in place before you install. | Prerequisite | Required | Notes | |---|---|---| | Kubernetes 1.29+ with RBAC enabled | Yes | No additional notes. | | Helm 3.x | Yes | No additional notes. | | Agent Sandbox controller and CRDs | Yes | Install before the OpenShell chart. Refer to [Install Agent Sandbox](#install-agent-sandbox). | | cert-manager | No | Refer to [Managing Certificates](/kubernetes/managing-certificates). Use cert-manager only if you prefer it over the built-in PKI job. | | Kubernetes Gateway API | No | Refer to [Ingress](/kubernetes/ingress). Use it only for external access without port-forwarding. | ## Install Agent Sandbox OpenShell uses the [Agent Sandbox](https://agent-sandbox.sigs.k8s.io) Kubernetes SIG project to provision sandbox pods. Install the Agent Sandbox controller and its CRDs on your cluster before installing the OpenShell Helm chart. Apply the latest release manifest: ```shell kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/latest/download/manifest.yaml ``` This creates the `agent-sandbox-system` namespace, installs the `sandboxes.agents.x-k8s.io` CRD, and starts the controller. **Air-gapped clusters:** mirror the manifest above and the `registry.k8s.io/agent-sandbox/agent-sandbox-controller` image referenced inside it to your internal registry, then point the manifest's image reference at your mirror before applying. You will also need to mirror the OpenShell gateway and sandbox images — see the chart's `image.repository` value for the gateway and `server.sandboxImage` / `server.supervisorImage` for the sandbox runtime. Confirm the controller pod is running before proceeding: ```shell kubectl -n agent-sandbox-system get pods ``` The controller pod should reach `Running` status within a few seconds. For cluster-specific setup instructions, including KinD and GKE walkthroughs, refer to the [Agent Sandbox getting started guide](https://agent-sandbox.sigs.k8s.io/docs/getting_started/). ### Upgrade Agent Sandbox OpenShell detects the served Agent Sandbox `Sandbox` API when the Kubernetes gateway first needs it and caches that choice for the gateway process. If you upgrade Agent Sandbox in place, restart the OpenShell gateway after the Agent Sandbox controller and CRD rollout completes so the gateway can detect the served API versions again. Existing sandboxes keep running during the upgrade, and the restarted gateway can continue managing them. ## Install OpenShell ## Create the namespace ```shell kubectl create namespace openshell ``` ## Install the chart Install from the OCI registry on GHCR. Replace `` with the chart version you want to install. ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell ``` To use the latest development build instead of a stable release: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version 0.0.0-dev \ --namespace openshell ``` The chart automatically generates PKI secrets on first install using pre-install Helm hooks. No manual secret creation is required. ## Wait for the gateway to be ready ```shell kubectl -n openshell rollout status statefulset/openshell ``` If you set `workload.kind=deployment`, wait on the Deployment instead: ```shell kubectl -n openshell rollout status deployment/openshell ``` ## Connect to the gateway For local evaluation, use a port-forward: ```shell kubectl -n openshell port-forward svc/openshell 8080:8080 ``` The port-forward is for local evaluation only. For shared environments, expose the gateway through your ingress controller or access proxy. Refer to [Ingress](/kubernetes/ingress) for an external access option. ## Install the TLS client bundle The chart generates an mTLS bundle for transport security. Kubernetes deployments do not use that bundle as user authentication; configure OIDC or a trusted access proxy as described in [Access Control](/kubernetes/access-control). For local port-forwarded access, copy the generated bundle so the CLI can verify the gateway certificate: ```shell mkdir -p ~/.config/openshell/gateways/k8s/mtls kubectl -n openshell get secret openshell-client-tls \ -o jsonpath='{.data.ca\.crt}' | base64 -d > ~/.config/openshell/gateways/k8s/mtls/ca.crt kubectl -n openshell get secret openshell-client-tls \ -o jsonpath='{.data.tls\.crt}' | base64 -d > ~/.config/openshell/gateways/k8s/mtls/tls.crt kubectl -n openshell get secret openshell-client-tls \ -o jsonpath='{.data.tls\.key}' | base64 -d > ~/.config/openshell/gateways/k8s/mtls/tls.key ``` The server certificate SANs include `localhost` and `127.0.0.1`, so hostname verification passes over the port-forward without extra flags. ## Register with the CLI In another terminal, register the gateway with the user authentication mode you configured and verify it is reachable. For example, with OIDC: ```shell openshell gateway add https://127.0.0.1:8080 --local --name k8s \ --oidc-issuer https://your-idp.example.com/realms/openshell \ --oidc-client-id openshell-cli openshell status ``` ## Configure Chart Values The most commonly changed values are: | Value | Purpose | |---|---| | `image.repository` / `image.tag` | Gateway container image. Defaults to `ghcr.io/nvidia/openshell/gateway:latest`. | | `replicaCount` | Number of gateway replicas. Leave at `1` unless you are explicitly testing multi-replica behavior. | | `workload.kind` | Gateway workload controller. Use `statefulset` for SQLite or `deployment` with `server.externalDbSecret`. | | `workload.allowMultiReplicaStatefulSet` | Allow `replicaCount > 1` with `workload.kind=statefulset`. Prefer Deployment for external database-backed multi-replica gateways. | | `server.sandboxNamespace` | Namespace where sandbox pods are created. Defaults to the Helm release namespace when left empty. | | `server.externalDbSecret` | Secret containing a PostgreSQL connection URI in the `uri` key. Use when the database is managed outside the chart. | | `server.telemetryEnabled` | Enable anonymous OpenShell telemetry from the gateway and its sandbox supervisors. Set to `false` to opt out. | | `server.sandboxImage` | Default sandbox image used when a sandbox does not specify one. | | `server.sandboxImagePullSecrets` | Image pull secrets attached to sandbox pods. Referenced Secrets must exist in the sandbox namespace. | | `server.grpcEndpoint` | Endpoint that sandbox supervisors use to call back to the gateway. Must be reachable from inside the cluster. | | `server.appArmorProfile` | AppArmor profile requested for sandbox agent containers. Defaults to `Unconfined`. | | `server.disableTls` | Run the gateway over plaintext HTTP. Use only behind a trusted transport. | | `server.auth.allowUnauthenticatedUsers` | Accept user-facing calls without OIDC or mTLS credentials. Use only for trusted local development or a fully trusted access proxy. | | `server.enableLoopbackServiceHttp` | Enable local plaintext HTTP for loopback sandbox service URLs. Defaults to `true`. | | `pkiInitJob.serverDnsNames` / `certManager.serverDnsNames` | Additional gateway server DNS SANs. Wildcard SANs also enable sandbox service URLs under that domain. | | `supervisor.sideloadMethod` | How the supervisor binary is delivered into sandbox pods. Leave empty to auto-detect based on cluster version: clusters running Kubernetes 1.35 or later use `image-volume` (ImageVolume GA in 1.36); older clusters use `init-container`. Set explicitly to `image-volume` on Kubernetes 1.33 or 1.34 with the ImageVolume feature gate enabled, or to `init-container` to force the legacy path on any version. | | `supervisor.topology` | Sandbox pod topology. Refer to [Topology](/kubernetes/topology). | | `supervisor.sidecar.proxyUid` | Non-root UID used when sidecar process/binary-aware network policy is disabled. The default binary-aware sidecar runs as UID 0 instead. The configured UID must not match the sandbox UID. | | `upstreamProxy` | Operator-owned corporate HTTP forward proxy for policy-approved TLS egress. Refer to [Configure a Corporate Upstream Proxy](#configure-a-corporate-upstream-proxy). | Use a values file for repeatable deployments: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --values my-values.yaml ``` The chart defaults `server.appArmorProfile` to `Unconfined` because runtime/default AppArmor profiles can block the supervisor's network namespace mount setup on AppArmor-enabled nodes. Set `server.appArmorProfile` to an empty string to omit the field, `RuntimeDefault` to force the runtime default, or `Localhost/` when you load and manage a localhost profile on each node. To use private sandbox images, create a `kubernetes.io/dockerconfigjson` Secret in the sandbox namespace and reference its name: ```shell kubectl -n openshell create secret docker-registry regcred \ --docker-server=registry.example.com \ --docker-username="$REGISTRY_USER" \ --docker-password="$REGISTRY_TOKEN" ``` ```yaml server: sandboxImage: registry.example.com/team/openshell-sandbox:latest sandboxImagePullSecrets: - name: regcred ``` ## Configure a Corporate Upstream Proxy Configure a corporate forward proxy when sandbox TLS egress cannot dial the Internet directly. OpenShell evaluates policy and SSRF checks before it opens an HTTP CONNECT tunnel through the proxy. The proxy URL is operator-owned configuration. Sandbox environment variables cannot select, replace, or bypass it. Create the credential Secret in the sandbox namespace when the proxy requires Basic authentication. The Secret value uses the `user:pass` form. ```shell kubectl -n openshell create secret generic corporate-proxy-auth \ --from-literal=credentials="$PROXY_USER:$PROXY_PASSWORD" ``` Add the proxy settings to your Helm values file. Replace the DNS suffixes and CIDRs in `noProxy` with values for your cluster. `noProxy` bypasses only the corporate proxy. OpenShell policy evaluation still applies. ```yaml upstreamProxy: url: http://proxy.corp.example:8080 noProxy: .svc,.svc.cluster.local,10.96.0.0/12,10.244.0.0/16 authSecret: name: corporate-proxy-auth key: credentials authAllowInsecure: true supervisor: topology: sidecar ``` Use `authAllowInsecure: true` only when you accept that Basic authentication is cleartext on the connection to an `http://` proxy. The initial release supports `http://` proxy endpoints and TLS CONNECT egress. It does not support HTTPS-to-proxy, custom corporate CA bundles, or forwarding plain HTTP egress through the proxy. Proxy credentials require `sidecar` topology. It mounts the credential only into the dedicated network supervisor container. OpenShell rejects credential Secrets with `combined` topology because Kubernetes `fsGroup` volume permission handling can make a shared credential mount readable by the sandbox group. ## RBAC The chart creates the following RBAC resources in the release namespace: | Resource | Scope | Name | |---|---|---| | ServiceAccount | Namespace | `openshell` | | ServiceAccount | Namespace | `openshell-sandbox` (for sandbox pods) | | Role + RoleBinding | Namespace | `openshell-sandbox` | | ClusterRole + ClusterRoleBinding | Cluster | `openshell-node-reader` | The namespaced Role covers sandbox lifecycle and identity: | API Group | Resource | Verbs | |---|---|---| | `agents.x-k8s.io` | `sandboxes`, `sandboxes/status` | create, delete, get, list, patch, update, watch | | `""` | `events` | get, list, watch | | `""` | `pods` | get | The ClusterRole grants node inspection and token validation: | API Group | Resource | Verbs | |---|---|---| | `authentication.k8s.io` | `tokenreviews` | create | | `""` | `nodes` | get, list, watch | To use an existing ServiceAccount instead of creating one, set `serviceAccount.create=false` and supply its name: ```shell helm upgrade --install openshell \ oci://ghcr.io/nvidia/openshell/helm-chart \ --version \ --namespace openshell \ --set serviceAccount.create=false \ --set serviceAccount.name=my-existing-sa ``` The ServiceAccount must already have the Role and ClusterRole bindings described above. ## Probes The gateway exposes `/healthz` for process liveness and `/readyz` for dependency-aware readiness on the health port. The Helm chart wires both into Kubernetes probes: - `startupProbe` and `livenessProbe` use `/healthz`. - `readinessProbe` uses `/readyz`, which reflects the latest result of an in-process background database check. ## Next Steps - To choose between combined and sidecar sandbox pods, refer to [Topology](/kubernetes/topology). - To enable automatic certificate rotation with cert-manager, refer to [Managing Certificates](/kubernetes/managing-certificates). - To expose the gateway externally without port-forwarding, refer to [Ingress](/kubernetes/ingress). - To configure OIDC or reverse-proxy authentication, refer to [Access Control](/kubernetes/access-control). - To create your first sandbox, refer to [Manage Sandboxes](/sandboxes/manage-sandboxes). --- ### Kubernetes/Topology --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Kubernetes Sandbox Topology" sidebar-title: "Topology" description: "Choose between combined and sidecar supervisor topology for Kubernetes sandbox pods." keywords: "Generative AI, Cybersecurity, Kubernetes, Sandboxing, Sidecar, Network Policy, RuntimeClass" position: 2 --- Kubernetes sandbox pods can run the OpenShell supervisor in `combined` or `sidecar` topology. Choose the topology based on which controls you need inside the pod and how much privilege your cluster allows on the agent container. ## Choose a Topology The default `combined` topology preserves the full OpenShell enforcement model. Use `sidecar` only when you accept network-focused enforcement in exchange for a lower-privilege agent container. | Topology | Use when | Main tradeoff | |---|---|---| | `combined` | You need OpenShell network, filesystem, and process controls in the sandbox workload. | The agent container carries the Linux capabilities the supervisor needs. | | `sidecar` | You need the agent container to run as non-root without added Linux capabilities, and network policy is the primary control. | Privilege-dropping and supervisor mount isolation do not run in the agent container. | ## Privilege Model The long-running container permissions differ by topology: | Topology | Pod or container | UID/GID | Privilege escalation | Capabilities | Result | |---|---|---|---|---|---| | `combined` | Agent container, which also runs the supervisor | Not forced by topology | Not explicitly disabled by the driver | Adds `SYS_ADMIN`, `NET_ADMIN`, `SYS_PTRACE`, and `SYSLOG`; adds `SETUID`, `SETGID`, and `DAC_READ_SEARCH` when user namespaces are enabled | Full supervisor controls run in the agent container. | | `sidecar` | Agent container, process-only supervisor (`network-only`) | `sandbox_uid:sandbox_gid` | `false` | Drops `ALL` | Agent and workload run without added Linux capabilities. | | `sidecar` | Network supervisor sidecar, binary-aware mode (default) | `0:sandbox_gid` | `false` | Drops `ALL`; adds `SYS_PTRACE` and `DAC_READ_SEARCH` | Root sidecar inspects cross-UID workload `/proc` entries. The nftables fence exempts UID 0, so do not inject other root containers into these pods. | | `sidecar` | Network supervisor sidecar, endpoint/L7-only mode | `proxyUid:sandbox_gid` | `false` | Drops `ALL` | Non-root sidecar enforces endpoint and L7 policy without matching `policy.binaries`. | Short-lived setup containers still have the permissions needed to prepare the pod: | Topology | Setup container | UID/GID | Privilege escalation | Capabilities | Purpose | |---|---|---|---|---|---| | `combined` | Supervisor install init container | `0` | Not set | Not set | Copies the supervisor binary into the agent container volume. | | `sidecar` | Network init container | `0` | `false` | Drops `ALL`; adds `NET_ADMIN`, `NET_RAW`, `CHOWN`, and `FOWNER` | Installs pod-local nftables rules and prepares shared sidecar state. | ## Combined Topology Combined topology is the original Kubernetes mode and remains the default. The agent container starts the OpenShell supervisor, and the supervisor launches the workload after applying sandbox setup. ```mermaid flowchart TB Sandbox["agents.x-k8s.io Sandbox"] subgraph Pod["Sandbox pod"] subgraph Agent["agent container"] Supervisor["OpenShell supervisor
network + process + filesystem"] Workload["Agent workload"] end end Gateway["OpenShell Gateway"] External["External services"] Sandbox --> Pod Supervisor --> Workload Supervisor -->|"gateway callback / SSH relay"| Gateway Supervisor -->|"policy-enforced egress"| External ``` Combined topology keeps these controls in one supervisor path: - Network endpoint and L7 policy enforcement. - Filesystem policy enforcement. - Process and binary identity checks. - Privilege drop into the sandbox user. - Gateway relay, SSH sessions, exec, and file sync. Because the supervisor performs network namespace setup and process/filesystem controls from the agent container, Kubernetes grants that container elevated Linux capabilities. Use this mode when you need the complete OpenShell sandbox contract and your cluster policy permits those capabilities. ## Sidecar Topology Sidecar topology splits the supervisor into a network sidecar and a low-privilege process supervisor in the agent container. ```mermaid flowchart TB Sandbox["agents.x-k8s.io Sandbox"] subgraph Pod["Sandbox pod"] Init["network init container
root setup capabilities"] State["shared state + TLS volumes"] NetNS["pod network namespace"] subgraph Agent["agent container"] ProcessSupervisor["process supervisor
network-only"] Workload["Agent workload"] end NetworkSidecar["network supervisor sidecar
UID 0 by default"] SshEndpoint["abstract SSH relay socket
peer-PID authenticated"] end Gateway["OpenShell Gateway"] External["External services"] Sandbox --> Pod Init -->|"installs nftables rules"| NetNS ProcessSupervisor --> Workload Workload -->|"egress redirected on loopback"| NetworkSidecar NetworkSidecar -->|"gateway session + relays"| Gateway NetworkSidecar -->|"policy-enforced egress"| External NetworkSidecar -->|"control socket + proxy TLS"| State ProcessSupervisor -->|"bootstrap + updates"| State ProcessSupervisor --> SshEndpoint NetworkSidecar -->|"SSH relay"| SshEndpoint NetworkSidecar --- State ``` The pod contains these OpenShell-managed pieces: | Component | Runs as | Purpose | |---|---|---| | Network init container | Root with setup capabilities | Installs pod-level nftables rules and prepares shared sidecar state. | | Network sidecar | UID 0 by default; `supervisor.sidecar.proxyUid` when binary-aware policy is disabled | Runs the proxy, enforces network policy, owns gateway authentication and the gateway session, and serves local policy/provider state over the sidecar control socket. | | Agent container | Resolved sandbox UID/GID | Runs the process supervisor and launches the user workload. | In this topology, the agent container defaults to `runAsNonRoot: true`, `allowPrivilegeEscalation: false`, and `capabilities.drop: ["ALL"]`. The default binary-aware network sidecar runs as UID 0, drops default Linux capabilities, and adds `SYS_PTRACE` plus `DAC_READ_SEARCH` for cross-UID workload process identity resolution. Setting `supervisor.sidecar.processBinaryAwareNetworkPolicy=false` runs the sidecar as the configured non-root `proxyUid`, omits both capabilities, and downgrades network policy to endpoint/L7 enforcement without binary matching. The root init container keeps the setup capabilities needed to configure pod networking. Sidecar mode preserves gateway session behavior, including SSH connectivity, because the network sidecar owns the gateway session and bridges relay requests to a Linux abstract SSH socket owned by the process supervisor. The relay verifies the socket peer PID against the authenticated control connection, so the workload cannot replace the relay endpoint. The agent container does not get a gateway endpoint, gateway TLS material, or the sandbox bootstrap token in the default sidecar path. Sidecar mode runs the process supervisor in `network-only` mode. OpenShell still enforces network endpoint and L7 policy through the sidecar, and the process supervisor applies Landlock filesystem policy and child seccomp filters where the kernel/runtime supports them. The process supervisor does not perform root-to-sandbox privilege dropping because Kubernetes starts the container as the sandbox UID/GID, and it does not perform supervisor identity mount isolation because gateway credentials are not mounted into the agent container. Sidecar pods use `shareProcessNamespace: true` so the network sidecar can resolve workload process and binary identity through `/proc/`. ## Credential Exposure Sidecar topology keeps gateway credentials in the network sidecar. The agent container does not mount the projected ServiceAccount token used for sandbox token bootstrap, does not mount the sandbox client TLS secret, and does not get gateway callback environment variables. The network sidecar serves the policy and workload-facing provider environment over a Unix control socket in the shared sidecar state volume. Before launching the workload, the process supervisor establishes the only accepted connection. The sidecar validates its UID, GID, and PID with peer credentials, unlinks the listener, derives the SSH target from trusted configuration, and rejects later clients. The connection receives bootstrap state and provider-environment updates after settings polls. If it closes, the network sidecar exits so Kubernetes recreates the one-client bootstrap listener, and the process supervisor exits so Kubernetes terminates the workload and restarts the agent container. This symmetric failure behavior prevents a surviving workload from claiming the new control listener after an isolated sidecar restart. Future child processes can see refreshed provider env without giving the agent container gateway authentication material. This does not mutate the environment of the already-running workload entrypoint. Use `combined` topology when you need the full single-supervisor enforcement path; use additional runtime isolation when you need a stronger container boundary around sidecar workloads. ## RuntimeClass Isolation Sidecar topology has been validated with Kata Containers. It does not currently support gVisor because sidecar mode requires pod-local nftables setup, which gVisor does not provide to the init container. A supported sandboxed runtime strengthens the container boundary while OpenShell focuses on network policy enforcement from the sidecar. Runtime classes do not re-enable the OpenShell privilege-drop or supervisor mount-isolation controls that sidecar mode relaxes. Use them as an additional workload boundary, not as a replacement for the combined topology's full supervisor controls. You can set a default runtime class in the Kubernetes driver configuration or override it per sandbox with driver config: ```shell openshell sandbox create \ --driver-config-json '{"kubernetes":{"pod":{"runtime_class_name":"kata-containers"}}}' \ -- claude ``` ## Enable Sidecar Mode For direct gateway TOML configuration, set the Kubernetes driver fields: ```toml [openshell.drivers.kubernetes] topology = "sidecar" [openshell.drivers.kubernetes.sidecar] proxy_uid = 1337 ``` `proxy_uid` configures only the relaxed endpoint/L7-only sidecar. It must be a non-root UID and must not match the sandbox UID. The default binary-aware mode runs the sidecar as UID 0 instead. The network init container exempts the effective sidecar UID from proxy redirection so the sidecar can reach the gateway. When the Helm chart renders `gateway.toml`, set the equivalent chart values: ```yaml supervisor: topology: sidecar sidecar: proxyUid: 1337 processBinaryAwareNetworkPolicy: true ``` Leave `topology` unset, or set it to `combined`, to keep the original single-container supervisor path. For Helm installs, leave `supervisor.topology` unset or set it to `combined`. Set `supervisor.sidecar.processBinaryAwareNetworkPolicy=false` only when you accept downgrading sidecar network policy to endpoint/L7 enforcement without matching `policy.binaries`. This changes the sidecar from UID 0 to `proxyUid` and removes its `SYS_PTRACE` and `DAC_READ_SEARCH` capabilities, which are used for cross-UID `/proc` inspection. ## Next Steps - To install OpenShell on Kubernetes, refer to [Setup](/kubernetes/setup). - To configure gateway authentication, refer to [Access Control](/kubernetes/access-control). - To review the driver fields, refer to [Gateway Configuration File](/reference/gateway-config). --- ### Observability/Accessing Logs --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Accessing Logs" description: "How to view sandbox logs through the CLI, TUI, and directly on the sandbox filesystem." keywords: "Generative AI, Cybersecurity, Logging, CLI, TUI, Observability" --- OpenShell provides three ways to access sandbox logs: the CLI, the TUI, and direct filesystem access inside the sandbox. ## CLI Use `openshell logs` to stream logs from a running sandbox: ```shell openshell logs smoke-l4 --source sandbox ``` The CLI receives logs from the gateway over gRPC. Each line includes a timestamp, source, level, and message: ```text [1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] [1775014132.190] [sandbox] [OCSF ] [ocsf] HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api] [1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox ``` OCSF structured events show `OCSF` as the level. Standard tracing events show `INFO`, `WARN`, or `ERROR`. Gateway-originated policy mutations also appear in this stream. When the gateway merges `openshell policy update` operations or approves or removes draft policy chunks, it emits `gateway` `OCSF` `CONFIG:*` lines for the affected sandbox so you can see the exact logical change that produced a new policy revision. ## TUI The TUI dashboard displays sandbox logs in real time. Logs appear in the log panel with the same format as the CLI. ## Gateway Log Storage The sandbox pushes logs to the gateway over gRPC in real time. The gateway stores a bounded buffer of recent log lines per sandbox. This buffer is not persisted to disk and is lost when the gateway restarts. For durable log storage, use the log files inside the sandbox or enable [OCSF JSON export](/observability/ocsf-json-export) and ship the JSONL files to an external log aggregator. ## Direct Filesystem Access Use `openshell sandbox connect` to open a shell inside the sandbox and read the log files directly: ```text openshell sandbox connect my-sandbox sandbox@my-sandbox:~$ cat /var/log/openshell.2026-04-01.log ``` You can also run a one-off command without an interactive shell: ```shell openshell sandbox connect my-sandbox -- cat /var/log/openshell.2026-04-01.log ``` The log files inside the sandbox contain the complete record, including events that the gRPC push channel can drop under load. The push channel is bounded and drops events rather than blocking. ## Filtering by Event Type The shorthand format is designed for `grep`. Some useful patterns: ```shell # All denied connections grep "DENIED\|BLOCKED" /var/log/openshell.*.log # All network events grep "OCSF NET:" /var/log/openshell.*.log # All L7 enforcement decisions grep "OCSF HTTP:" /var/log/openshell.*.log # Security findings only grep "OCSF FINDING:" /var/log/openshell.*.log # Policy changes grep "OCSF CONFIG:" /var/log/openshell.*.log # All OCSF events, excluding standard tracing grep "^.* OCSF " /var/log/openshell.*.log # Events at medium severity or above grep "\[MED\]\|\[HIGH\]\|\[CRIT\]\|\[FATAL\]" /var/log/openshell.*.log ``` ## Next Steps - Learn how the [log formats](/observability/logging) work and how to read the shorthand. - [Enable OCSF JSON export](/observability/ocsf-json-export) for machine-readable structured output. --- ### Observability/Logging --- # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 title: "Sandbox Logging" sidebar-title: "Logging" description: "How OpenShell logs sandbox activity using standard tracing and OCSF structured events." keywords: "Generative AI, Cybersecurity, Logging, OCSF, Observability" --- Every OpenShell sandbox produces a log that records network connections, process lifecycle events, filesystem policy decisions, and configuration changes. The log uses two formats depending on the type of event. ## Log Formats ### Standard tracing Internal operational events use Rust's `tracing` framework with a conventional format: ```text 2026-04-01T03:28:39.160Z INFO openshell_sandbox: Fetching sandbox policy via gRPC 2026-04-01T03:28:39.175Z INFO openshell_sandbox: Creating OPA engine from proto policy data ``` These events cover startup plumbing, gRPC communication, and internal state transitions that are useful for debugging but do not represent security-relevant decisions. ### OCSF structured events Network, process, filesystem, and configuration events use the [Open Cybersecurity Schema Framework (OCSF)](https://ocsf.io) format. OCSF is an open standard for normalizing security telemetry across tools and platforms. OpenShell maps sandbox events to OCSF v1.7.0 event classes. In the log file, OCSF events appear in a shorthand format with an `OCSF` level label, designed for quick human and agent scanning: ```text 2026-04-01T04:04:13.058Z INFO openshell_sandbox: Starting sandbox 2026-04-01T04:04:13.065Z OCSF CONFIG:DISCOVERY [INFO] Server returned no policy; attempting local discovery 2026-04-01T04:04:13.074Z INFO openshell_sandbox: Creating OPA engine from proto policy data 2026-04-01T04:04:13.078Z OCSF CONFIG:VALIDATED [INFO] Validated 'sandbox' user exists in image 2026-04-01T04:04:32.118Z OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] 2026-04-01T04:04:32.190Z OCSF HTTP:GET [INFO] ALLOWED GET http://api.github.com/zen [policy:github_api engine:opa] 2026-04-01T04:04:32.690Z OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy] ``` The `OCSF` label at column 25 distinguishes structured events from standard `INFO` tracing at the same position. Both formats appear in the same file. When viewed through the CLI or TUI, which receive logs through gRPC, the same distinction applies: ```text [1775014132.118] [sandbox] [OCSF ] [ocsf] NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] [1775014132.690] [sandbox] [OCSF ] [ocsf] NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy] [1775014113.058] [sandbox] [INFO ] [openshell_sandbox] Starting sandbox ``` ## OCSF Event Classes OpenShell maps sandbox events to these OCSF classes: | Shorthand prefix | OCSF class | Class UID | What it covers | |---|---|---|---| | `NET:` | Network Activity | 4001 | TCP proxy CONNECT tunnels, bypass detection, DNS failures | | `HTTP:` | HTTP Activity | 4002 | HTTP FORWARD requests, L7 enforcement decisions | | `SSH:` | SSH Activity | 4007 | SSH handshakes, authentication, channel operations | | `PROC:` | Process Activity | 1007 | Process start, exit, timeout, signal failures | | `FINDING:` | Detection Finding | 2004 | Security findings (nonce replay, proxy bypass, unsafe policy) | | `CONFIG:` | Device Config State Change | 5019 | Policy load/reload, Landlock, TLS setup, inference routes | | `LIFECYCLE:` | Application Lifecycle | 6002 | Sandbox supervisor start, SSH server ready | ## Reading the Shorthand Format The shorthand format follows this pattern: ```text CLASS:ACTIVITY [SEVERITY] ACTION DETAILS [CONTEXT] ``` ### Components **Class and activity** (`NET:OPEN`, `HTTP:GET`, `PROC:LAUNCH`) identify the OCSF event class and what happened. The class name always starts at the same column position for vertical scanning. **Severity** indicates the OCSF severity of the event: | Tag | Meaning | When used | |---|---|---| | `[INFO]` | Informational | Allowed connections, successful operations | | `[LOW]` | Low | DNS failures, operational warnings | | `[MED]` | Medium | Denied connections, policy violations | | `[HIGH]` | High | Security findings (nonce replay, bypass detection) | | `[CRIT]` | Critical | Process timeout kills | | `[FATAL]` | Fatal | Unrecoverable failures | **Action** (`ALLOWED`, `DENIED`, `BLOCKED`) is the security control disposition. Not all events have an action; informational config events, for example, do not. **Details** vary by event class: - Network: `process(pid) -> host:port` with the process identity and destination - HTTP: `METHOD url` with the HTTP method and target - SSH: peer address and authentication type - Process: `name(pid)` with exit code or command line - Config: description of what changed - Finding: quoted title with the stable finding type, optional confidence, and source-specific context attributes when available **Context** in brackets provides structured fields such as policy provenance, source-specific attributes, and denial reasons. ### Examples An allowed HTTPS connection: ```text OCSF NET:OPEN [INFO] ALLOWED /usr/bin/curl(58) -> api.github.com:443 [policy:github_api engine:opa] ``` An L7 read-only policy denying a POST: ```text OCSF HTTP:POST [MED] DENIED POST http://api.github.com/user/repos [policy:github_api engine:opa] ``` A connection denied because no policy matched: ```text OCSF NET:OPEN [MED] DENIED /usr/bin/curl(64) -> httpbin.org:443 [policy:- engine:opa] [reason:no matching policy] ``` A connection denied because the destination resolves to an always-blocked address: ```text OCSF NET:OPEN [MED] DENIED /usr/bin/curl(1618) -> 169.254.169.254:80 [policy:- engine:ssrf] [reason:resolves to always-blocked address] ``` An HTTP request to a non-default port. HTTP log URLs include the port whenever it differs from the scheme default (80 for `http`, 443 for `https`): ```text OCSF HTTP:GET [INFO] ALLOWED GET http://api.internal.corp:8080/v1/status [policy:internal_api engine:opa] ``` A supervisor middleware HTTP event records whether it transformed the request. If the middleware also emits a finding, that remains a separate event: ```text OCSF HTTP:POST [INFO] ALLOWED POST http://httpbin.org:443/anything [policy:httpbin engine:middleware] [failed:false transformed:true] OCSF FINDING:CREATE [MED] "configured content matched" [type:content_guard.match count:1 middleware:prototype-content-guard] ``` WebSocket middleware emits one safe event per preflight, session-start, or client text-message decision. The event includes the policy-local config, registered implementation, sequence, byte counts, transformation flag, and validated reason code. It never includes the message payload or service-provided free-form reason: ```text OCSF NET:OTHER [INFO] WEBSOCKET_MIDDLEWARE allow config=api-redactor implementation=openshell/regex sequence=3 input_bytes=128 replacement_bytes=96 transformed=true reason_code=- ``` Coverage events are separate from invocation decisions. `binding_not_selected` means a host-matched attachment did not advertise the WebSocket operation. `unsupported_message_type` means an active WebSocket stage encountered a binary message, which V1 passes through without inspection. Both are informational, do not apply `on_error`, and never claim the traffic was inspected: ```text OCSF NET:OTHER [INFO] WEBSOCKET_MIDDLEWARE_COVERAGE state=binding_not_selected config=http-dlp implementation=example/http-dlp sequence=- message_type=- input_bytes=0 OCSF NET:OTHER [INFO] WEBSOCKET_MIDDLEWARE_COVERAGE state=unsupported_message_type config=api-redactor implementation=openshell/regex sequence=4 message_type=binary input_bytes=512 ``` A fail-open stream error emits both a middleware failure and `openshell.middleware.websocket_stage_disabled`. The latter records that OpenShell will bypass that stage for later messages on the same connection. Waiting for saturated admission capacity also emits a detection finding without payload content. When both active capacity and the bounded wait queue are full, HTTP work is rejected before its payload is buffered, returns `503 Service Unavailable`, and emits `openshell.middleware.admission_exhausted`. Proxy and SSH servers ready: ```text OCSF NET:LISTEN [INFO] 10.200.0.1:3128 OCSF SSH:LISTEN [INFO] ``` An SSH connection accepted (one event per invocation, arriving over the supervisor's Unix socket, so there is no network peer address to log): ```text OCSF SSH:OPEN [INFO] ALLOWED ``` A process launched inside the sandbox: ```text OCSF PROC:LAUNCH [INFO] sleep(49) ``` A policy reload after a settings change: ```text OCSF CONFIG:DETECTED [INFO] Settings poll: config change detected [old_revision:2915564174587774909 new_revision:11008534403127604466 policy_changed:true] OCSF CONFIG:LOADED [INFO] Policy reloaded successfully [policy_hash:0cc0c2b525573c07] ``` ## Denial Reasons Denied `NET:` and `HTTP:` events carry a `[reason:...]` suffix that surfaces the decision detail from the event's `status_detail` field. The reason helps distinguish between policy misses, SSRF hardening, and L7 enforcement without inspecting the full OCSF JSONL record. For supervisor middleware denials, `status_detail` contains a platform-owned reason derived from the policy-local middleware config name and optional validated reason code. Middleware failure details also use platform-owned error codes. OpenShell does not copy per-request service text or WebSocket message content into logs. Common reason phrases emitted by the sandbox include: | Reason | Meaning | |---|---| | `no matching policy` | OPA evaluated the request and no allow rule matched. | | `resolves to always-blocked address` | The destination resolved to loopback, link-local, or unspecified. These ranges are always blocked, even when listed in `allowed_ips`. | | `resolves to which is not in allowed_ips, connection rejected` | The destination resolved to an IP outside the policy's `allowed_ips` allowlist. | | `DNS resolution failed for :` | The proxy could not resolve the destination. | | `port is a blocked control-plane port, connection rejected` | The destination port matches a control-plane port (etcd, Kubernetes API, kubelet) and is always blocked. | | `request-target contains an encoded '/' (%2F)` | The L7 HTTP parser rejected an encoded slash. Configure `allow_encoded_slash: true` on a REST endpoint when the upstream requires encoded slashes. | | `l7 deny` | An L7 policy rule denied the request. | Invalid `allowed_ips` entries and entries that overlap always-blocked ranges are rejected at policy-load time, so they never reach the runtime denial path. The phrases above come from the proxy's per-CONNECT `allowed_ips` and SSRF checks, not from policy validation. ## Proxy Error Responses When the HTTP CONNECT proxy denies a request or cannot reach the upstream, it returns an HTTP error response with a JSON body. Clients can parse the body to surface actionable failure details instead of treating the status code alone. A denied CONNECT returns `403 Forbidden`: ```json { "error": "policy_denied", "detail": "CONNECT api.example.com:443 not permitted by policy", "reason": "binary '/usr/bin/node' not allowed in policy 'allow_api' (ancestors: [/usr/local/bin/claude])" } ``` The `reason` field is included when the policy engine provides a specific denial reason (for example, which binary or rule caused the rejection). It is omitted when no additional detail is available. An upstream that the proxy cannot reach returns `502 Bad Gateway`: ```json { "error": "upstream_unreachable", "detail": "connection to api.example.com:443 failed" } ``` 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. ## Filesystem Sandbox Logs Landlock filesystem restrictions emit `CONFIG:` events at startup and whenever the sandbox has to skip a requested path. On startup, the probe reports the kernel's supported Landlock ABI version alongside the requested path counts: ```text OCSF CONFIG:ENABLED [INFO] Landlock filesystem sandbox available [abi:v2 compat:BestEffort ro:4 rw:2] OCSF CONFIG:ENABLED [INFO] Applying Landlock filesystem sandbox [abi:V2 compat:BestEffort ro:4 rw:2] OCSF CONFIG:ENABLED [INFO] Landlock ruleset built [rules_applied:5 skipped:1] ``` When `landlock.compatibility` is `best_effort` and a requested path fails to open for reasons other than `NotFound` (for example, permission denied or a symlink loop), the sandbox continues without that path and emits a `[MED]` event so the degradation is not silent: ```text OCSF CONFIG:OTHER [MED] Skipping inaccessible Landlock path (best-effort) [path:/opt/data error:Permission denied (os error 13)] ``` Set `landlock.compatibility` to `hard_requirement` in the policy to make these failures fatal instead of degraded. ## Log File Location Inside the sandbox, logs are written to `/var/log/`: | File | Format | Rotation | |---|---|---| | `openshell.YYYY-MM-DD.log` | Shorthand + standard tracing | Daily, 3 files max | | `openshell-ocsf.YYYY-MM-DD.log` | OCSF JSONL when enabled | Daily, 3 files max | Both files rotate daily and retain the 3 most recent files to bound disk usage. ## Next Steps - [Access logs](/observability/accessing-logs) through the CLI, TUI, or sandbox filesystem. - [Enable OCSF JSON export](/observability/ocsf-json-export) for SIEM integration and compliance. - Learn about [network policies](/sandboxes/policies) that generate these events. ---