feat(supervisor): port streamed configuration to RFC 0012

Signed-off-by: Piotr Mlocek <pmlocek@nvidia.com>
This commit is contained in:
Piotr Mlocek
2026-09-16 11:09:31 -07:00
428 changed files with 65234 additions and 33939 deletions
+8
View File
@@ -52,6 +52,9 @@ The `audience` value must match the client ID configured in your identity provid
| Value | Default | Purpose |
|---|---|---|
| `server.oidc.issuer` | `""` | OIDC issuer URL. Empty disables OIDC. |
| `server.oidc.dangerouslyAllowInsecureHttp` | `false` | Development-only acknowledgement for numeric-loopback HTTP. It does not allow cluster-service or remote HTTP issuers. |
| `server.oidc.jwksAllowedOrigins` | `[]` | Additional trusted HTTPS origins allowed to serve JWKS. |
| `server.oidc.caConfigMapName` | `""` | ConfigMap containing the private issuer CA in `ca.crt`. |
| `server.oidc.audience` | `openshell-cli` | Expected `aud` claim in the JWT. |
| `server.oidc.jwksTtl` | `3600` | JWKS key cache TTL in seconds. Must be greater than zero. |
| `server.oidc.rolesClaim` | `""` | Dot-separated path to the roles array in JWT claims. |
@@ -59,6 +62,11 @@ The `audience` value must match the client ID configured in your identity provid
| `server.oidc.userRole` | `""` | Role name that grants standard user access. |
| `server.oidc.scopesClaim` | `""` | Dot-separated path to the scopes array in JWT claims. |
The issuer must use HTTPS. The gateway rejects discovery and JWKS redirects,
limits response sizes, requires a JSON media type, and rejects a `jwks_uri` on
a different origin unless that origin appears in `jwksAllowedOrigins`. Use
`server.oidc.caConfigMapName` for issuers signed by a private CA.
### 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.
+2
View File
@@ -57,6 +57,7 @@ helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true \
--set grpcRoute.enabled=true \
--set grpcRoute.gateway.create=true \
--set grpcRoute.gateway.className=eg
@@ -113,6 +114,7 @@ helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true \
--set grpcRoute.enabled=true \
--set grpcRoute.gateway.create=true \
--set grpcRoute.gateway.className=eg \
@@ -52,6 +52,7 @@ helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true \
--set certManager.enabled=true
```
@@ -73,6 +74,7 @@ helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true \
--set certManager.enabled=true \
--set certManager.serverIssuerRef.name=letsencrypt-prod \
--set certManager.serverIssuerRef.kind=ClusterIssuer \
+26 -51
View File
@@ -3,87 +3,62 @@
# 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."
description: "Install the OpenShell Helm chart on OpenShift with capability-free sandbox workloads."
keywords: "Generative AI, Cybersecurity, Kubernetes, OpenShift, SCC, Security Context Constraints, Helm, Gateway, Installation"
position: 6
---
The Kubernetes driver resolves the UID range assigned to each OpenShift
namespace and renders the sandbox and supervisor with a numeric non-root
identity from that range. OpenShell does not require the `privileged` SCC or any
added Linux capability.
<Warning>
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.
Verify that the selected OpenShift runtime profile permits an unprivileged
process to install a nested seccomp user-notification filter and use Landlock.
OpenShell fails sandbox startup when either capability-free runtime probe fails.
</Warning>
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
- OpenShift 4.x cluster with `oc` configured.
- Helm 3.x.
- [Agent Sandbox](/kubernetes/setup#install-agent-sandbox) controller and CRDs.
- A CNI that enforces ingress and egress `NetworkPolicy` in sandbox namespaces.
## Install
## Install OpenShell
<Steps>
## Create the namespace
Pre-create the namespace so the SCC binding can be applied before the chart installs:
Pre-create the namespace, then install the chart. Keep the default restricted
security posture and acknowledge NetworkPolicy only after validating the CNI.
```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 <version> \
--namespace openshell \
--set server.disableTls=true \
--set podSecurityContext.fsGroup=null \
--set securityContext.runAsUser=null
--set supervisor.sandboxRuntime.networkPolicyEnforced=true
```
| 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. |
The driver reads the namespace's `openshift.io/sa.scc.uid-range` annotation and
uses the resulting UID/GID for the sandbox, agent, trusted init containers, and
supervisor. Each container sets `allowPrivilegeEscalation: false`, drops all
Linux capabilities, and uses `RuntimeDefault` seccomp.
## Wait for the gateway to be ready
Wait for the gateway:
```shell
oc -n openshell rollout status statefulset/openshell
```
If you set `workload.kind=deployment`, use
`oc -n openshell rollout status deployment/openshell` instead.
If you set `workload.kind=deployment`, wait for `deployment/openshell` instead.
</Steps>
## Connect to the Gateway
## Connect to the gateway
The gateway is now running over plaintext HTTP. Connect with `oc port-forward`:
Forward the gateway port for local evaluation:
```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 gateway add https://127.0.0.1:8080 --local --name openshift
openshell status
```
+128
View File
@@ -0,0 +1,128 @@
---
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
title: "Kubernetes Sandbox Runtime"
sidebar-title: "Sandbox runtime"
description: "Understand how Kubernetes places and protects the sandbox runtime and supervisor."
keywords: "Generative AI, Cybersecurity, Kubernetes, Sandboxing, Network Policy, Seccomp, Landlock"
position: 2
---
OpenShell runs each Kubernetes sandbox as two separately scheduled workloads.
The sandbox owns the agent process. The supervisor owns gateway credentials,
policy decisions, and upstream connections.
## Understand the Components
The Kubernetes driver always uses this placement:
```mermaid
flowchart LR
Gateway[OpenShell gateway]
Supervisor[Supervisor Pod]
Sandbox[Sandbox workload Pod]
Agent[Agent processes]
External[External services]
Gateway <-->|JWT-authenticated session| Supervisor
Supervisor <-->|TLS and bootstrap token| Sandbox
Sandbox --> Agent
Supervisor --> External
```
`openshell-sandbox` runs as PID 1 in the workload container. It launches the
agent, applies Landlock and child seccomp filters, identifies the process behind
each network operation, and relays approved streams. `openshell-supervisor` runs
in a directly managed Pod. It authenticates to the gateway, evaluates policy,
handles L7 and provider transformations, and opens upstream connections.
Both containers run as the same namespace-resolved non-root UID and GID. Their
Pod specs set `allowPrivilegeEscalation: false`, drop every Linux capability,
and use `RuntimeDefault` seccomp. The sandbox installs an additional nested
seccomp user-notification filter without requesting a capability. Startup fails
closed if the runtime blocks the required seccomp or Landlock operations.
The supervisor Pod points directly to the Sandbox resource with a non-controller
owner reference. Kubernetes therefore removes it when the Sandbox is deleted,
while the Agent Sandbox controller remains the sole controller of the workload
Pod.
## Enforce Network Isolation
The driver creates one workload fence per sandbox namespace before it releases
any workload Pod:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
spec:
podSelector:
matchLabels:
openshell.ai/boundary-role: workload
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector:
matchLabels:
openshell.ai/boundary-role: supervisor
ports:
- protocol: TCP
port: 5500
egress: []
```
The empty egress list blocks direct DNS, gateway, node, metadata, and Internet
connections from every OpenShell workload in the namespace. The ingress rule
allows OpenShell supervisor Pods to reach sandbox TLS listeners. TLS, JWT
claims, session generation, and recorded Pod UIDs—not the NetworkPolicy—bind a
supervisor to its exact sandbox. Supervisors can reach cluster DNS, the gateway,
and policy-approved upstream destinations unless another namespace policy
restricts them.
Kubernetes policies are additive. Keep sandbox namespaces under administrative
control so another principal cannot add permissive policies, create Pods with
OpenShell labels, or read bootstrap Secrets. Set
`supervisor.sandboxRuntime.networkPolicyEnforced: true` only after you verify that the
cluster CNI enforces both ingress and egress policies for these namespaces.
## Bootstrap a Sandbox
The driver creates each sandbox generation in a fail-closed order:
1. Create and validate the workload egress fence.
2. Create the Sandbox resource with a scheduling gate.
3. Inspect the admitted Pod identity, security context, DNS settings, and
generation-specific Secret reference.
4. Create a gated supervisor Pod and separate immutable Secrets for sandbox and
supervisor trust material.
5. Remove both scheduling gates.
6. Publish readiness only after the supervisor attaches, confirms enforcement,
and registers the gateway relay.
The trusted sandbox init container copies its Secret into a memory-backed
volume. The main container never mounts the projected Secret and removes the
staged bootstrap before it launches untrusted code. The supervisor receives an
audience-bound Kubernetes token, exchanges it for a sandbox-scoped JWT, and
keeps gateway and provider credentials outside the workload Pod.
Stopping a sandbox removes both the workload and supervisor Pods. Starting it
creates a new generation with new Secrets and a replacement supervisor Pod
while preserving the workspace PVC. The namespace-wide workload fence remains
in place across sandbox generations.
## Check Cluster Requirements
This architecture requires the following cluster behavior:
- Linux nodes and a container runtime that permits an unprivileged process to
install a nested seccomp user-notification filter under `RuntimeDefault`.
- Landlock enabled and usable by the non-root sandbox process.
- A CNI that enforces `networking.k8s.io/v1` ingress and egress policies,
including node-local and metadata destinations.
- Support for Pod scheduling gates and the safe
`net.ipv4.ip_unprivileged_port_start=0` sysctl.
- Administrative control of sandbox namespaces and OpenShell role labels.
OpenShell actively probes the Linux primitives and validates the admitted Pod
before starting the agent. Treat a failed probe or changed security posture as
an unsupported runtime, not a degraded mode.
+12 -17
View File
@@ -85,7 +85,8 @@ Install from the OCI registry on GHCR. Replace `<version>` with the chart versio
helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true
```
To use the latest development build instead of a stable release:
@@ -94,7 +95,8 @@ To use the latest development build instead of a stable release:
helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version 0.0.0-dev \
--namespace openshell
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true
```
The chart automatically generates PKI secrets on first install using pre-install Helm hooks. No manual secret creation is required.
@@ -109,6 +111,7 @@ helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true \
--set workspaceResources.enabled=false \
--set server.sandboxNamespace=app-a
@@ -199,14 +202,12 @@ The most commonly changed values are:
| `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. |
| `supervisor.sandboxRuntime.networkPolicyEnforced` | Required acknowledgement that the cluster CNI enforces ingress and egress `NetworkPolicy` in sandbox namespaces. |
| `supervisor.sandboxRuntime.boundaryPort` | Non-privileged TLS port used between paired supervisor and sandbox Pods. |
| `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:
@@ -216,16 +217,10 @@ helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true \
--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/<profile-name>` 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:
@@ -265,13 +260,12 @@ upstreamProxy:
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.
The credential mounts only in the separately scheduled supervisor Pod. The
sandbox workload cannot read it through its environment or volumes.
## RBAC
@@ -306,6 +300,7 @@ helm upgrade --install openshell \
oci://ghcr.io/nvidia/openshell/helm-chart \
--version <version> \
--namespace openshell \
--set supervisor.sandboxRuntime.networkPolicyEnforced=true \
--set serviceAccount.create=false \
--set serviceAccount.name=my-existing-sa
```
@@ -321,7 +316,7 @@ The gateway exposes `/healthz` for process liveness and `/readyz` for dependency
## Next Steps
- To choose between combined and sidecar sandbox pods, refer to [Topology](/kubernetes/topology).
- Kubernetes sandboxes use separate workload and directly managed supervisor Pods; refer to [Sandbox runtime](/kubernetes/sandbox-runtime).
- 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).
-249
View File
@@ -1,249 +0,0 @@
---
# 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<br/>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<br/>root setup capabilities"]
State["shared state + TLS volumes"]
NetNS["pod network namespace"]
subgraph Agent["agent container"]
ProcessSupervisor["process supervisor<br/>network-only"]
Workload["Agent workload"]
end
NetworkSidecar["network supervisor sidecar<br/>UID 0 by default"]
SshEndpoint["abstract SSH relay socket<br/>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.
<Warning>
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/<entrypoint-pid>`.
</Warning>
## 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 at
least `1000` 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).
+4
View File
@@ -168,6 +168,10 @@ The `class_uid` field identifies the event type:
| 5019 | Device Config State Change | `CONFIG:` |
| 6002 | Application Lifecycle | `LIFECYCLE:` |
HTTP Activity records include request or response details. Rejections that have
parsed a request, including early credential-binding failures, include safe
method-only request context and the generated response.
## SIEM Schema Version Compatibility
OpenShell emits OCSF v1.8.0 events internally, but many SIEMs only support older schema versions. The `ocsf_schema_version` setting tells the JSONL layer to downgrade events before writing, stripping fields and profiles that don't exist in the target version.
+85
View File
@@ -0,0 +1,85 @@
---
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
title: "API Errors"
sidebar-title: "API Errors"
description: "Inspect structured gateway failures and choose an appropriate recovery action."
keywords: "OpenShell, SDK, gRPC, API errors, retries"
---
Gateway errors include a gRPC status code and a message. Shared request validation
and concurrency checks also return standard protobuf details in
`grpc-status-details-bin`. Use the status code and structured fields for decisions.
Treat messages as explanations whose text can change.
## Structured details
The SDKs decode these standard detail types while retaining the original failure.
Older gateways and checks that have not adopted structured details can return only
a code and message. Missing details do not change the meaning of the status code.
| Detail | Meaning |
|---|---|
| `google.rpc.BadRequest` | `field_violations` identifies rejected fields and explains each violation. Shared sandbox, exec, provider size, and workspace selector checks supply these details. |
| `google.rpc.ErrorInfo` | `reason`, `domain`, and `metadata` identify a failure without parsing its message. Gateway reasons use the `openshell.nvidia.com` domain. |
| `google.rpc.RetryInfo` | `retry_delay` gives a minimum delay before an otherwise safe retry. Its presence does not guarantee that a mutation has not already committed. |
Recognized gateway reasons include the following.
| Reason | Code | Recovery |
|---|---|---|
| `INVALID_ARGUMENT` | `INVALID_ARGUMENT` | Correct the fields listed in `BadRequest`. |
| `RESOURCE_VERSION_CONFLICT` | `ABORTED` | Read the resource again and construct a new conditional write. `metadata.recovery` is `REFRESH_STATE`; `current_resource_version` is included when known. |
| `PROFILE_SOURCE_UNAVAILABLE` | `UNAVAILABLE` | Retry a profile snapshot read after at least the supplied delay. |
## Status and retry guidance
A timeout or disconnected transport can occur after a mutation commits. Do not
automatically repeat creates, credential rotation, or command execution based
only on a transient status. Retry a mutation only when its documented operation
contract makes the repeated request safe. A correlation ID does not provide that
guarantee.
| Status | Recovery |
|---|---|
| `INVALID_ARGUMENT`, `OUT_OF_RANGE` | Correct the request. |
| `UNAUTHENTICATED` | Refresh or replace credentials before a new attempt. |
| `PERMISSION_DENIED` | Obtain the required authorization. |
| `NOT_FOUND` | Check the resource and workspace. Follow the operation's missing-resource contract. |
| `ALREADY_EXISTS` | Inspect the existing resource before deciding whether it satisfies the request. |
| `FAILED_PRECONDITION` | Resolve the reported state or configuration requirement. |
| `ABORTED` | Read fresh state before retrying a conditional operation. |
| `UNAVAILABLE`, `RESOURCE_EXHAUSTED` | A transient condition may recover. Apply backoff and any minimum retry delay only to a retry-safe operation. |
| `DEADLINE_EXCEEDED`, `CANCELLED` | A mutation's outcome can be unknown. Cancellation does not imply rollback. |
| `UNIMPLEMENTED` | Check gateway and SDK version compatibility. |
| `INTERNAL`, `UNKNOWN`, `DATA_LOSS` | Preserve the status and correlation metadata for diagnosis. Do not blindly retry mutations. |
## SDK access
Each SDK exposes decoded fields and an escape hatch for complete transport data.
Unknown details remain available through the original error even when the SDK
does not recognize their message type.
| SDK | Decoded details | Original failure |
|---|---|---|
| Rust | `SdkError::error_details()`, `retry_delay()` | `SdkError::grpc_status()` returns the original `tonic::Status`, including details bytes and metadata. |
| Go | `StatusError.FieldViolations`, `ErrorInfo`, `RetryDelay`, `GRPCCode` | `StatusError.Cause` retains the gRPC error. `status.FromError` can recover its details through error wrapping. |
| TypeScript | `SdkError.fieldViolations`, `errorInfo`, `retryDelayMs`, `connectCode` | `SdkError.cause` retains the `ConnectError`, including all details and metadata. Use `fromConnect` for raw calls. |
| Python | `GatewayError.field_violations`, `error_info`, `retry_delay` | `raw_error` retains the gRPC exception and metadata; `raw_status` retains the parsed envelope. Use `from_grpc_error` for raw calls. |
Retry delays are Rust and Go durations, TypeScript milliseconds, and Python
seconds. Absent or invalid delay details do not produce a suggested delay.
## Migration
Existing error codes and human-readable messages remain available. Python curated
clients now raise `GatewayError`, which remains a `grpc.RpcError`; existing
`except grpc.RpcError` handlers continue to work. `GatewayError` is not a
`grpc.Call`. If your handler checks that interface, inspect `raw_error` before
checking the status. The Python SDK's deletion wait and managed-sandbox cleanup
use the original call to recognize `NOT_FOUND`; other failures still propagate.
Rust error variants now retain
status fields; use `..` when destructuring variants that do not need those fields.
Go and TypeScript add typed detail fields to their existing error types.
No SDK automatically retries a mutation as a result of decoding these details.
+12
View File
@@ -90,6 +90,8 @@ The same settings are available through environment variables:
| Environment variable | Purpose | Default |
|---|---|---|
| `OPENSHELL_OIDC_ISSUER` | OIDC issuer URL. The gateway discovers `/.well-known/openid-configuration` from this URL. | None |
| `OPENSHELL_OIDC_DANGEROUSLY_ALLOW_INSECURE_HTTP` | Development-only acknowledgement for HTTP issuers on numeric loopback addresses. HTTP hostnames and non-loopback addresses remain rejected. | `false` |
| `OPENSHELL_OIDC_JWKS_ALLOWED_ORIGINS` | Comma-separated additional HTTPS origins trusted to serve JWKS. | Empty |
| `OPENSHELL_OIDC_AUDIENCE` | Expected JWT `aud` claim. | `openshell-cli` |
| `OPENSHELL_OIDC_JWKS_TTL` | JWKS cache TTL in seconds. The gateway also refreshes on an unknown key ID. Must be greater than zero. | `3600` |
| `OPENSHELL_OIDC_ROLES_CLAIM` | Dot-separated claim path containing roles. | `realm_access.roles` |
@@ -104,12 +106,22 @@ server:
oidc:
issuer: https://idp.example.com/realms/openshell
audience: openshell-cli
# Only needed when discovery returns a deliberately separate JWKS origin.
jwksAllowedOrigins:
- https://keys.example.com
rolesClaim: realm_access.roles
adminRole: openshell-admin
userRole: openshell-user
scopesClaim: ""
```
The gateway requires HTTPS for OIDC discovery and JWKS retrieval, rejects
redirects, and trusts the issuer origin for JWKS by default. Add a
`jwksAllowedOrigins` entry only when the identity provider intentionally serves
keys from another trusted origin. For local tests, numeric-loopback HTTP also
requires `dangerouslyAllowInsecureHttp: true`; this setting never permits an
HTTP hostname or non-loopback address.
Register an OIDC gateway with the CLI:
```shell
+65 -108
View File
@@ -22,7 +22,7 @@ Gateway CLI flag > gateway OPENSHELL_* env var > TOML file > built-in defa
## Supervisor connections and policy recovery
Current supervisors open `ConnectSupervisor` before initializing gateway-owned runtime state and require a complete configuration bootstrap before the gateway accepts the session as initialized. They apply later policy, settings, middleware, and provider snapshots from that stream without configuration fetch polling. If first-start image policy discovery or image-specific baseline enrichment changes policy, the supervisor commits that repair and reconnects before installing the fresh stream bootstrap; it does not initialize from the mutation response. The gateway gives bootstrap construction the same bounded 45-second window as other snapshot builds and rejects the connection when construction fails or times out. Immediately previous supervisors retain the Stage 1 compatibility behavior: the gateway waits at most one second for an optional bootstrap, accepts the session without it when necessary, and relies on supervisor polling for configuration.
During supervisor reconnects, the gateway waits at most one second for the optional configuration bootstrap. If a credential backend stalls, the gateway accepts the session without that bootstrap so SSH, exec, and service connections can resume. Supervisor polling continues to supply configuration.
On startup, the gateway repairs missing legacy policy history and skips invalid stored policies. A valid global policy can still override an invalid local policy; otherwise, that sandbox's configuration reads continue to report the validation failure.
@@ -100,7 +100,7 @@ future version. To migrate an existing file:
omit Docker or Podman `sandbox_pids_limit` to use OpenShell's default limit
of 2048, and omit Podman `health_check_interval_secs` to disable health
checks. Explicit zero values are invalid.
7. Remove local Docker, Podman, or VM `grpc_endpoint` when the topology-derived
7. Remove local Docker, Podman, or VM `grpc_endpoint` when the driver-derived
callback is correct, or retain it as an explicit override. Kubernetes raw
TOML requires an explicit endpoint; Helm derives one from the release's
gateway Service. New VM root filesystems use an image-provided `sandbox`
@@ -219,13 +219,15 @@ endpoint = "http://otel-collector.observability.svc:4317"
service_name = "openshell-gateway"
[openshell.gateway.oidc]
issuer = "https://idp.example.com/realms/openshell"
audience = "openshell-cli"
jwks_ttl_secs = 3600 # Must be greater than zero.
roles_claim = "realm_access.roles"
admin_role = "openshell-admin"
user_role = "openshell-user"
scopes_claim = ""
issuer = "https://idp.example.com/realms/openshell"
audience = "openshell-cli"
jwks_ttl_secs = 3600 # Must be greater than zero.
jwks_allowed_origins = ["https://keys.example.com"] # Default: issuer origin only.
dangerously_allow_insecure_http = false # Development-only numeric-loopback escape hatch.
roles_claim = "realm_access.roles"
admin_role = "openshell-admin"
user_role = "openshell-user"
scopes_claim = ""
[[openshell.gateway.interceptors]]
name = "quota"
@@ -252,10 +254,10 @@ namespace = "openshell"
# Required in raw TOML; Helm derives this from the gateway Service.
grpc_endpoint = "https://openshell-gateway.openshell.svc:8080"
default_image = "ghcr.io/nvidia/openshell/sandbox:latest"
# Defaults to the gateway version. The gateway accepts its current internal
# supervisor protocol revision, the immediately previous revision through
# polling compatibility, and pre-handshake supervisors for one release. Other
# revisions reject the session. Compatibility sessions emit gateway warnings.
# Defaults to the gateway version. The current internal supervisor protocol
# streams authoritative configuration snapshots. The immediately previous
# revision remains polling-compatible, and pre-handshake supervisors remain
# compatible for one release. Other revisions reject the session.
# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
client_tls_secret_name = "openshell-client-tls"
service_account_name = "openshell-sandbox"
@@ -450,7 +452,8 @@ For Vault instead:
credential_drivers = ["vault"]
[openshell.credential_drivers.vault]
address = "http://vault.vault.svc.cluster.local:8200"
address = "https://vault.vault.svc.cluster.local:8200"
ca_bundle = "/etc/openshell/vault/ca.pem"
mount = "secret"
kv_version = "2"
auth_method = "kubernetes"
@@ -470,7 +473,9 @@ server:
For `kubernetes-secrets`, `namespace` sets where OpenShell-managed provider Secret objects are stored. When omitted, the driver uses the in-cluster ServiceAccount namespace when available, otherwise `default`. The Helm chart creates a Role granting the gateway access to all Secrets in the credential namespace because OpenShell-managed Secret names are dynamic SHA-256 hashes that cannot be restricted with `resourceNames`. Deploy credential Secrets in a dedicated namespace (`server.credentialDrivers.kubernetesSecrets.namespace`) to limit the RBAC blast radius.
For `vault`, `address` points at the Vault service, `mount` and `kv_version` describe the KV engine where OpenShell-managed provider secrets are stored, and `auth_method = "kubernetes"` logs in with the gateway Pod's ServiceAccount token. For local or development validation, use `auth_method = "token_file"` with `token_path = "/path/to/token"`. Do not put literal Vault tokens in TOML.
For `vault`, `address` points at the Vault service. Non-loopback endpoints must use HTTPS; plaintext HTTP is accepted only for `localhost` or an IP loopback address during local development. Vault requests do not follow redirects, preventing credentials from being replayed to a downgraded or substituted endpoint. HTTPS uses platform trust roots by default. Set `ca_bundle` to a certificate-only PEM bundle for a private Vault CA; the bundle augments platform roots and normal hostname verification remains enabled. `mount` and `kv_version` describe the KV engine where OpenShell-managed provider secrets are stored, and `auth_method = "kubernetes"` logs in with the gateway Pod's ServiceAccount token. For local or development validation, use `auth_method = "token_file"` with `token_path = "/path/to/token"`. Do not put literal Vault tokens in TOML.
For Helm deployments, set `server.credentialDrivers.vault.caConfigMapName` to a ConfigMap containing the private CA bundle under the `ca.crt` key. The chart mounts that key and renders `ca_bundle` automatically.
Provider records that already contain inline database credentials remain readable for upgrade compatibility. New provider create/update requests still submit credential values through the normal API, but the gateway stores those values through the active credential storage path and persists only handles. Before OpenShell 0.1.0, OpenShell does not automatically migrate inline refresh material or credential handles between drivers. Reconfigure refresh grants after an upgrade. Before changing credential drivers, remove affected credentials while the original driver is still available, then select the new driver and create them again. Do not run mixed gateway versions against the same refresh records.
@@ -501,7 +506,8 @@ transport = "uds"
socket_path = "/run/openshell/credential-drivers/vault.sock"
command = "/usr/libexec/openshell/openshell-driver-vault"
args = [
"--address", "http://vault.vault.svc.cluster.local:8200",
"--address", "https://vault.vault.svc.cluster.local:8200",
"--ca-bundle", "/etc/openshell/vault/ca.pem",
"--auth-method", "kubernetes",
"--role", "openshell-gateway",
]
@@ -554,20 +560,16 @@ service_account_name = "openshell-sandbox"
default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"
image_pull_policy = "if_not_present"
image_pull_secrets = ["regcred"]
# Defaults to the gateway version. The gateway accepts its current internal
# supervisor protocol revision, the immediately previous revision through
# polling compatibility, and pre-handshake supervisors for one release. Other
# revisions reject the session. Compatibility sessions emit gateway warnings.
# Defaults to the gateway version; override to pin a specific build.
# sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:<version>"
sandbox_runtime_image_pull_policy = "if_not_present"
# Defaults to the gateway version. Custom builds must match the gateway's
# internal supervisor protocol revision; mismatched peers reject the session.
# Supervisors from releases before the handshake existed remain compatible for
# one release.
# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
supervisor_image_pull_policy = "if_not_present"
# Use the image volume on Kubernetes >= 1.35 (GA in 1.36); switch to "init-container"
# on older clusters or where the ImageVolume feature gate is off.
supervisor_sideload_method = "image-volume"
# "combined" runs the existing single supervisor container with full process,
# filesystem, and network enforcement in the agent container. "sidecar" moves
# pod-level network enforcement and gateway session handling into a network sidecar.
topology = "combined"
# Optional corporate HTTP forward proxy for policy-approved TLS egress. The
# sandbox workload cannot select or override these settings. Only http:// proxy
# endpoints and TLS CONNECT traffic are supported; plain HTTP egress remains
@@ -585,9 +587,7 @@ topology = "combined"
# Secret prevents that Pod from starting; unreadable or malformed `user:pass`
# content is validated fail-closed by the supervisor at startup and never
# falls back to direct egress.
# Proxy credential Secrets require `topology = "sidecar"`. Combined topology
# shares its credential mount with the workload and can make it readable by the
# sandbox group through Kubernetes `fsGroup` volume permission handling.
# Proxy credentials mount only in the separately scheduled supervisor Pod.
# Required with a credential Secret: Basic authentication to an http:// proxy
# is cleartext on the connection to that proxy.
# proxy_auth_allow_insecure = true
@@ -602,7 +602,6 @@ ssh_socket_path = "/run/openshell/ssh.sock"
client_tls_secret_name = "openshell-client-tls"
host_gateway_ip = "10.0.0.1"
enable_user_namespaces = false
app_armor_profile = "Unconfined"
workspace_default_storage_size = "10Gi"
# Kubernetes StorageClass for the workspace PVC. Empty (default) omits the
# field, using the cluster's default StorageClass. Set this on clusters with no
@@ -637,19 +636,12 @@ enabled = true
gateway_namespace = "openshell"
gateway_pod_selector = { "app.kubernetes.io/name" = "openshell", "app.kubernetes.io/instance" = "openshell" }
[openshell.drivers.kubernetes.sidecar]
# UID used by relaxed long-running network sidecars. Strict process/binary-aware
# sidecars run as UID 0 so Kubernetes grants the required /proc inspection
# capabilities into the effective set. In sidecar topology the network init
# container installs nftables rules that exempt the effective sidecar UID, so
# this dedicated infrastructure UID must remain at least 1000 and must not
# match the sandbox workload UID.
proxy_uid = 1337
# Keep process/binary-aware network policy enabled in sidecar topology. Set
# false to run the sidecar as proxy_uid, drop the sidecar's extra /proc
# inspection capabilities, and enforce endpoint/L7 policy without matching
# policy.binaries.
process_binary_aware_network_policy = true
[openshell.drivers.kubernetes.sandbox_runtime]
# Required acknowledgement that the cluster CNI enforces NetworkPolicy and the
# sandbox namespaces prevent untrusted policy, pod, label, and Secret changes.
network_policy_enforced = true
# TLS-protected boundary listener reached only by the paired control pod.
boundary_port = 5500
```
In managed workspace mode, the Kubernetes driver copies each explicitly named
@@ -672,12 +664,12 @@ map Windows Sandboxing provider events into the gateway's OCSF stream.
```toml
[openshell]
version = 1
version = 2
[openshell.gateway]
bind_address = "127.0.0.1:17670"
log_level = "info"
compute_drivers = ["mxc"]
compute_driver = "mxc"
[openshell.drivers.mxc]
wxc_exec_path = "C:\\mxc-kit\\bin\\wxc-exec.exe"
@@ -685,10 +677,6 @@ backend = "process_container"
default_configuration_id = "composable"
pc_least_privilege = false
pc_capabilities = []
# Pattern-C governed egress. The address is a loopback seed; the driver
# allocates a unique ephemeral port for each sandbox.
egress_proxy = false
egress_proxy_addr = ""
debug = false
etw_audit = true
```
@@ -698,19 +686,6 @@ administrator or belong to the Windows Performance Log Users group. Workload
commands and working directories remain sandbox-scoped and must be supplied in
the `mxc` driver configuration when creating a sandbox.
Set `egress_proxy = true` with a loopback seed such as
`egress_proxy_addr = "127.0.0.1:18080"` to enable the Windows Pattern-C split.
MXC redirects sandbox traffic to a sandbox-unique port on the configured
loopback IP, where the host CONNECT proxy enforces the trimmed network policy.
The driver exposes the proxy's public CA bundle through a sandbox-unique
internal filesystem grant so HTTPS clients can read the injected trust path;
the CA private key remains in gateway memory.
Governed egress requires `process_container` until MXC supports `network.proxy`
for isolation sessions.
MXC rejects policies containing `network_middlewares` before launch because
this host-proxy path does not yet receive the gateway's built-in and remote
middleware registry.
The driver records executable identity in process audit events and omits raw
command arguments because they can contain credentials or personal data. See
[OCSF JSON Export](/observability/ocsf-json-export) for durable Windows audit
@@ -741,19 +716,17 @@ image_pull_policy = "if_not_present"
# Value assigned to the openshell.sandbox_namespace label on sandbox containers.
sandbox_label = "docker-dev"
# Optional override. When omitted, the gateway derives
# https://host.openshell.internal:<gateway-port> for this topology.
# https://host.openshell.internal:<gateway-port> for this driver.
grpc_endpoint = "https://host.openshell.internal:17670"
# Skip the image-pull-and-extract step by pointing at a locally built binary.
supervisor_bin = "/usr/local/libexec/openshell/openshell-sandbox"
# When supervisor_bin is omitted, Docker extracts /openshell-sandbox from this image.
# Defaults to the gateway version. The gateway accepts its current internal
# supervisor protocol revision, the immediately previous revision through
# polling compatibility, and pre-handshake supervisors for one release. Other
# revisions reject the session. Compatibility sessions emit gateway warnings.
# Workload-side runtime. Defaults to the gateway version.
# sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:<version>"
# Supervisor runtime defaults to the gateway version. The current internal
# protocol streams authoritative configuration snapshots; the immediately
# previous revision uses polling compatibility. Pre-handshake supervisors
# remain compatible for one release. Other revisions reject the session.
# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
network_name = "openshell-docker"
host_gateway_ip = "172.17.0.1"
ssh_socket_path = "/run/openshell/ssh.sock"
# Unsafe operator override. Host bind mounts, including Docker local-driver
# bind-backed volumes, expose gateway-host paths inside sandboxes and can
# negate OpenShell isolation and filesystem controls.
@@ -780,7 +753,7 @@ Use `sandbox_label` for Docker configurations. The legacy
### Podman
Sandboxes run as Podman containers on a user-mode bridge network. The supervisor image is mounted read-only via Podman's `type=image` mount. Configure guest mTLS paths once under `[openshell.gateway]`; the gateway validates and injects the bundle into the selected local driver.
Each Podman sandbox uses two containers. The workload container runs `openshell-sandbox` with `network=none`; the supervisor container runs on the host network and initiates policy-approved upstream connections. A private volume carries their authenticated Unix-domain socket. Configure guest mTLS paths once under `[openshell.gateway]`; the gateway validates and injects the bundle into the selected local driver.
```toml
[openshell]
@@ -813,10 +786,13 @@ network_name = "openshell"
# host_gateway_ip = "192.168.127.254"
ssh_socket_path = "/run/openshell/ssh.sock"
stop_timeout_secs = 45
# Defaults to the gateway version. The gateway accepts its current internal
# supervisor protocol revision, the immediately previous revision through
# polling compatibility, and pre-handshake supervisors for one release. Other
# revisions reject the session. Compatibility sessions emit gateway warnings.
# Statically linked workload-side runtime. Defaults to the gateway version.
# sandbox_runtime_image = "ghcr.io/nvidia/openshell/sandbox:<version>"
# Dynamically linked supervisor runtime defaults to the gateway version. The
# current internal protocol streams authoritative configuration snapshots; the
# immediately previous revision uses polling compatibility. Pre-handshake
# supervisors remain compatible for one release. Other revisions reject the
# session.
# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
# Unsafe operator override. Host bind mounts, including Podman local-driver
# bind-backed volumes, expose gateway-host paths inside sandboxes and can
@@ -919,18 +895,17 @@ health_check_interval_secs = 10
# explicit container-reachable TCP endpoint, for provider token exchange.
# provider_spiffe_workload_api_socket = "/run/spire/agent.sock"
# provider_spiffe_workload_api_socket = "tcp:169.254.1.2:8081"
# Omit app_armor_profile to preserve Podman's runtime-selected profile.
# Set Unconfined only when the supervisor's mount setup requires it.
# Omit app_armor_profile to preserve Podman's runtime-selected workload profile.
# Explicit RuntimeDefault and Localhost/<name> require Podman to report
# AppArmor support.
# app_armor_profile = "Unconfined"
# app_armor_profile = "RuntimeDefault"
```
Use `ssh_socket_path` for Podman configurations. The legacy
`sandbox_ssh_socket_path` key is rejected. When `app_armor_profile` is omitted,
OpenShell sends no override and Podman applies its runtime-selected profile.
Set `Unconfined` explicitly only when the deployment requires the supervisor's
mount setup to bypass that profile.
The setting applies to the workload container; the supervisor retains Podman's
runtime-selected profile.
### MicroVM
@@ -956,7 +931,7 @@ state_dir = "/var/lib/openshell/vm"
driver_dir = "/usr/local/libexec/openshell"
default_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"
# Optional override. When omitted, the gateway derives
# https://host.openshell.internal:<gateway-port> for the VM topology.
# https://host.openshell.internal:<gateway-port> for the VM driver.
grpc_endpoint = "https://host.openshell.internal:17670"
# Empty falls back to default_image.
bootstrap_image = "ghcr.io/nvidia/openshell-community/sandboxes/base:latest"
@@ -979,35 +954,17 @@ overlay_disk_mib = 4096
# any present-but-invalid value is rejected at gateway startup rather than
# degrading to a direct dial. proxy_auth_file is a path on the gateway host.
#
# The sandbox cannot select or override these settings. They reach the guest
# supervisor through a protected per-sandbox argument file the driver writes
# into the overlay upperdir on every launch, so a sandbox image cannot supply
# its own values or disable the operator's by baking a file at that path.
# The sandbox cannot select or override these settings. The driver passes them
# only to the host supervisor.
#
# Reachability: a proxy on the corporate network needs no special address and
# works on every VM sandbox. The guest's callback to the gateway is unaffected
# and never traverses the proxy.
# works on every VM sandbox.
#
# A proxy on the gateway host itself is reachable only from libkrun-backed
# (non-GPU) sandboxes: their egress leaves through gvproxy, which NATs
# 192.168.127.254 to the host's 127.0.0.1, so address it as
# http://host.openshell.internal:<port> rather than http://127.0.0.1:<port>.
# GPU sandboxes run on the QEMU/TAP backend, which has no such NAT —
# host.openshell.internal resolves to the TAP host address, and the driver's
# nftables rules let the guest reach only the gateway port on the host. A
# gateway-host proxy URL is therefore rejected when the sandbox launches on
# QEMU, rather than timing out on every CONNECT; give GPU sandboxes a proxy
# address routable from the guest's masqueraded egress.
# A proxy on the gateway host is addressed as
# http://host.openshell.internal:<port>. The host supervisor normalizes that
# name to host loopback for both libkrun and QEMU sandboxes.
#
# Because a microVM has no bind mounts or container secrets, the driver stages
# the credential and optional CA bundle into the per-sandbox overlay disk and
# removes them with the sandbox. The credential is root-only; the non-secret CA
# bundle is mode 0644. Both are referenced from the protected supervisor
# argument file by fixed guest paths, never their gateway-host paths. The
# credential is therefore at rest in that overlay image on the gateway host —
# the same delivery the per-sandbox gateway token already uses, and a difference
# from the Podman secret model worth noting when choosing where to keep proxy
# credentials.
# Credentials and private CA material remain host-side with the supervisor.
# https_proxy = "http://host.openshell.internal:8080"
# no_proxy = "10.0.0.0/8,.internal.example"
# proxy_auth_file = "/etc/openshell/secrets/proxy-auth"
+3
View File
@@ -32,6 +32,8 @@ network_middlewares: { ... }
| `network_policies` | map | No | Dynamic | Declares which binaries can reach which network endpoints. |
| `network_middlewares` | map | No | Dynamic | Attaches ordered middleware by destination host; each implementation's manifest selects its supported HTTP and WebSocket operations. |
The YAML root and each present top-level section listed as an object or map must be mappings. Each named network policy or middleware entry must also be a mapping. The OPA loader rejects the first malformed container before normalization or access preset expansion, with a structural error that excludes authored keys and values.
Static fields are set at sandbox creation time. Changing them requires destroying and recreating the sandbox. Dynamic fields can be updated on a running sandbox with `openshell policy update` for incremental merges or `openshell policy set` for full replacement, and take effect without restarting.
## Version
@@ -193,6 +195,7 @@ Each endpoint defines a reachable destination and optional inspection rules.
**Validation constraints:**
- When present, `endpoints`, `binaries`, `rules`, and `deny_rules` must be lists of objects, even when they contain only one entry. `null` cannot replace a list or an object entry.
- `access` and `rules` are mutually exclusive; setting both is rejected.
- `protocol: tcp` requires a valid DNS hostname. Hostless `allowed_ips`, IP-literal hosts, trailing-dot names, and malformed DNS selectors are rejected with a policy-validation error.
- `protocol: tcp` requires at least one port and rejects L7-only fields, including `path`, `enforcement`, `access`, `rules`, `deny_rules`, request rewriting and credential signing fields, and GraphQL, JSON-RPC, or MCP options.
+49 -118
View File
@@ -40,36 +40,9 @@ an exited canonical process remains a terminal sandbox result. Exit code zero
produces `Completed`; a nonzero or signal-normalized exit produces `Error`
with the exact exit code. Driver and supervisor failures remain `Error`.
## Build with Selected Compute Drivers
Source builds of `openshell-gateway` can include any subset of the Docker,
Podman, Kubernetes, VM, and MXC drivers. Enable the corresponding
`compute-driver-docker`, `compute-driver-podman`, `compute-driver-kubernetes`,
`compute-driver-vm`, or `compute-driver-mxc` Cargo features. For example, build
a Docker-only gateway with telemetry support:
```shell
cargo build --release -p openshell-gateway --no-default-features --features telemetry,compute-driver-docker
```
On Windows, select only MXC with:
```shell
cargo build --release -p openshell-gateway --no-default-features --features telemetry,compute-driver-mxc,bundled-z3
```
The default `in-tree-compute-drivers` feature retains the full platform driver
set, including MXC on Windows. MXC links only on Windows. The other four
features link drivers on non-Windows platforms; on Windows they install
registrations that report the driver as unsupported. A build with no default
features and no driver features connects to external drivers only.
Auto-detection probes only compiled registrations.
To select a driver omitted from a custom build, configure its external
`socket_path` as described below.
## Configure a Compute Driver
Configure the compute driver on the gateway. Current releases accept one driver per gateway. Set the singular `compute_driver` key in the gateway TOML file:
Configure the compute driver on the gateway. Current releases accept one driver per gateway. Set `compute_driver` in the gateway TOML file:
```toml
[openshell.gateway]
@@ -83,15 +56,13 @@ Non-reserved names select an extension driver and require a
When `compute_driver` is unset, the gateway auto-detects Kubernetes, then Podman, then Docker. Docker must respond on a known API socket. Podman first probes known API sockets and then asks the `podman` CLI for the active native or machine-backed socket. The VM driver is never auto-detected; configure it explicitly with `compute_driver = "vm"` or set `OPENSHELL_COMPUTE_DRIVER=vm` in the launch environment.
`compute_driver` accepts exactly one scalar driver name. The legacy `compute_drivers` list is rejected by schema version 2.
Common gateway options:
| Gateway TOML option | Description |
|---|---|
| `compute_driver = "<driver>"` | Select the compute driver. Built-in values are `docker`, `podman`, `kubernetes`, and `vm`; custom names require `[openshell.drivers.<name>].socket_path`. |
Set driver-specific values such as sandbox images, callback endpoints, network names, and VM sizing in the gateway TOML file. A TLS-enabled gateway-managed Docker, Podman, or VM driver requires a complete `guest_tls_ca`, `guest_tls_cert`, and `guest_tls_key` bundle in `[openshell.gateway]`; package-managed local TLS supplies it automatically. Driver tables reject those gateway-owned fields. Kubernetes projects guest TLS through a Secret instead. See the [Gateway Configuration File](./gateway-config) reference for the full schema and migration steps.
Set driver-specific values such as sandbox images, callback endpoints, network names, TLS material, and VM sizing in the gateway TOML file. See the [Gateway Configuration File](./gateway-config) reference for the full `[openshell.drivers.<name>]` schema.
Extension drivers use the same `compute_driver.proto` gRPC surface as the
managed VM driver. For an out-of-tree driver, choose a driver name and point
@@ -110,8 +81,8 @@ socket path. The endpoint replaces normal driver construction for that name,
including canonical built-in names:
```shell
openshell-gateway --compute-driver kyma --compute-driver-socket /run/openshell/kyma.sock
openshell-gateway --compute-driver docker --compute-driver-socket /run/openshell/docker.sock
openshell-gateway --drivers kyma --compute-driver-socket /run/openshell/kyma.sock
openshell-gateway --drivers docker --compute-driver-socket /run/openshell/docker.sock
```
The gateway connects to the operator-provided endpoint; it does not provision
@@ -186,7 +157,7 @@ the gateway. If the primary listener covers that address, the gateway reuses
it and sandbox JWT authentication restricts the supervisor to its callback RPC
allowlist. If the primary listener is not reachable through that address, the
gateway creates an additional callback-only listener. Use the primary endpoint
for CLI, administrator, health, reflection, provider management, and
for CLI, administrator, health, reflection, inference-route management, and
HTTP requests. A `PermissionDenied` response from an additional callback-only
listener is expected for those requests. Do not broaden the primary listener
to `0.0.0.0` solely to make sandbox callbacks reachable.
@@ -195,7 +166,7 @@ to `0.0.0.0` solely to make sandbox callbacks reachable.
[Docker](https://www.docker.com/get-started/)-backed sandboxes run as containers on the gateway host. Use Docker for local development, single-machine gateways, and hosts that already use Docker Desktop or Docker Engine.
The gateway talks to the Docker daemon to create sandbox containers.
The gateway talks to the Docker daemon to create sandbox containers. Docker is also required for local image builds from directories or Dockerfiles.
Docker Desktop and compatible macOS runtimes route `host.openshell.internal`
through an IPv4 host-gateway alias. The gateway reuses an IPv4 primary listener
@@ -204,7 +175,7 @@ that already covers loopback. Otherwise, the Docker driver requests a separate
For maintainer-level implementation details, refer to the [Docker driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-docker/README.md).
Select Docker with `compute_driver = "docker"` in `[openshell.gateway]`. Configure Docker driver values such as `socket_path`, `grpc_endpoint`, `network_name`, `sandbox_label`, `supervisor_bin`, `supervisor_image`, `image_pull_policy`, `ssh_socket_path`, and `sandbox_pids_limit` in `[openshell.drivers.docker]`. When `socket_path` is unset, the driver uses the same responsive local socket selected by auto-detection. An explicitly selected Docker driver falls back to `/var/run/docker.sock` when no candidate responds.
Select Docker with `compute_driver = "docker"` in `[openshell.gateway]`. Configure Docker driver values such as `socket_path`, `grpc_endpoint`, `network_name`, `sandbox_runtime_image`, `supervisor_image`, `image_pull_policy`, `sandbox_pids_limit`, and `guest_tls_*` in `[openshell.drivers.docker]`. The sandbox runtime image contains `/openshell-sandbox`; the supervisor image contains `/openshell-supervisor`. When `socket_path` is unset, the driver uses the same responsive local socket selected by auto-detection. An explicitly selected Docker driver falls back to `/var/run/docker.sock` when no candidate responds.
When operating `openshell-driver-docker` as an external driver, set
`OPENSHELL_OTLP_ENDPOINT` to export its spans. The driver continues W3C trace
@@ -271,7 +242,7 @@ OpenShell rejects mount `source`, `target`, and Docker volume `subpath` values
with surrounding whitespace. OpenShell also rejects mount targets that replace
the workspace root or container root, or contain or are contained by the
configured SSH socket or reserved `/opt/openshell`, `/etc/openshell`,
`/etc/openshell-tls`, `/run/openshell`, `/run/openshell-sidecar`, and network
`/etc/openshell-tls`, `/run/openshell`, and network
namespace roots. These checks do not make host bind mounts safe.
## Podman Driver
@@ -280,29 +251,11 @@ namespace roots. These checks do not make host bind mounts safe.
The gateway talks to the Podman API socket. The Podman driver requires Podman 5.x, cgroups v2, rootless networking, and an active Podman user socket. When `socket_path` is not set, the driver probes known socket paths, then uses the `podman` CLI to resolve the active native or machine-backed connection. It fails to start if neither method finds a socket.
The agent workload uses `network=none`. Its trusted supervisor companion uses Podman's host network for gateway callbacks and policy-approved upstream connections.
For maintainer-level implementation details, refer to the [Podman driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-podman/README.md) and [Podman networking notes](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-podman/NETWORKING.md).
Select Podman with `compute_driver = "podman"` in `[openshell.gateway]`. Configure Podman driver values such as `socket_path`, `network_name`, `supervisor_image`, `stop_timeout_secs`, `image_pull_policy`, `grpc_endpoint`, `host_gateway_ip`, `ssh_socket_path`, and `sandbox_pids_limit` in `[openshell.drivers.podman]`.
### macOS Podman Socket Path
On macOS, Homebrew-installed Podman does not create the default socket path
that the driver probes (`~/.local/share/containers/podman/machine/podman.sock`).
The actual API socket lives under `/var/folders/` in a path that macOS can
rotate after a reboot.
If the gateway fails with `Podman socket not found; is podman machine running?`
while `podman machine list` shows a running machine, set the
`OPENSHELL_PODMAN_SOCKET` environment variable to the dynamic socket path:
```shell
export OPENSHELL_PODMAN_SOCKET="$(podman machine inspect --format '{{.ConnectionInfo.PodmanSocket.Path}}')"
```
Add this to your shell profile or gateway launch environment so it resolves
correctly after each reboot. Alternatively, set `socket_path` in
`[openshell.drivers.podman]` to the current path, but note that the path may
change when macOS rotates `/var/folders/`.
Select Podman with `compute_driver = "podman"` in `[openshell.gateway]`. Configure Podman driver values such as `socket_path`, `network_name`, `sandbox_runtime_image`, `supervisor_image`, `stop_timeout_secs`, `image_pull_policy`, `grpc_endpoint`, `host_gateway_ip`, `ssh_socket_path`, `sandbox_pids_limit`, and `guest_tls_*` in `[openshell.drivers.podman]`.
Podman sandboxes default to a 45-second graceful stop window before Podman escalates from `SIGTERM` to `SIGKILL`. Set `stop_timeout_secs` in gateway config, or `OPENSHELL_STOP_TIMEOUT` for the standalone driver, when a local runtime needs a different teardown window.
@@ -316,12 +269,6 @@ stopped sandboxes alone.
For proxy-required networks, the Podman driver also accepts the corporate egress proxy keys `https_proxy`, `no_proxy`, `proxy_auth_file`, `proxy_auth_allow_insecure`, and `proxy_connect_by_hostname`. The supervisor chains policy-approved TLS tunnels through the proxy with HTTP CONNECT instead of dialing destinations directly. See the [Gateway Configuration File](./gateway-config) reference for the full contract, including the cleartext-credential acknowledgement and the validated-IP CONNECT behavior.
Podman preserves its runtime-selected AppArmor profile when
`app_armor_profile` is omitted. Set `Unconfined` explicitly only when the
supervisor's mount setup requires it. Explicit `RuntimeDefault` and
`Localhost/<profile>` selections fail startup when Podman reports that AppArmor
is unavailable.
On macOS with `podman machine`, the driver uses gvproxy's host-loopback IP, `192.168.127.254`, for sandbox host aliases by default. Set `host_gateway_ip` only when your Podman machine uses a non-standard host-loopback address. On Linux, an empty `host_gateway_ip` keeps Podman's `host-gateway` resolver behavior. Direct local callbacks from rootless Podman require Podman to report the pasta network helper. Slirp4netns, other helpers, and Podman versions that do not report their helper require an explicitly remote `grpc_endpoint`; otherwise the gateway fails startup rather than leaving sandbox callbacks unreachable. Rootful Podman continues to use the configured network's bridge gateway address.
### Podman Driver Config Mounts
@@ -411,16 +358,10 @@ compute_driver = "vm"
For a launch-time override, set `OPENSHELL_COMPUTE_DRIVER=vm` in the gateway environment and restart the service.
Configure VM driver values such as `grpc_endpoint`, `driver_dir`, `state_dir`, `default_image`, `bootstrap_image`, `vcpus`, `mem_mib`, `overlay_disk_mib`, and `krun_log_level` in `[openshell.drivers.vm]`. The VM `state_dir` stores overlay disks, console logs, runtime state, image-rootfs cache, and the private `run/compute-driver.sock` socket. The VM socket path is managed by the gateway and is not configurable through remote endpoint settings.
Configure VM driver values such as `grpc_endpoint`, `driver_dir`, `state_dir`, `default_image`, `bootstrap_image`, `vcpus`, `mem_mib`, `overlay_disk_mib`, `krun_log_level`, and `guest_tls_*` in `[openshell.drivers.vm]`. The VM `state_dir` stores overlay disks, console logs, runtime state, image-rootfs cache, and the private `run/compute-driver.sock` socket. The VM socket path is managed by the gateway and is not configurable through remote endpoint settings.
The gateway starts `openshell-driver-vm` over a private Unix socket and passes its process ID so the driver can reject unexpected local clients. The driver's standalone TCP listener is disabled unless `--allow-unauthenticated-tcp` is set for local development.
Scripts that invoke the experimental standalone driver directly must use
`--grpc-endpoint` and the `--upstream-proxy*` option family. Schema v2 removes
the previous `--openshell-endpoint`, `--https-proxy`, `--no-proxy`, and
`--proxy-*` spellings; gateway-managed deployments do not use those options
directly.
### Local image resolution
The VM driver resolves sandbox images from a local container engine before falling back to registry pulls. It tries Docker first, then uses the same Podman socket discovery as the Podman driver. On Linux with Podman, enable the API socket so the driver can find local images:
@@ -429,23 +370,28 @@ The VM driver resolves sandbox images from a local container engine before falli
systemctl --user start podman.socket
```
### Host Firewall
### Network isolation
The VM driver creates nftables rules on the host for each sandbox VM's TAP network interface. These rules provide NAT for VM connectivity and defense-in-depth isolation: unsolicited inbound connections to the VM are dropped, and the VM can only reach the gateway port on the host. Primary security enforcement (proxy-only egress and bypass detection) is handled by the sandbox supervisor inside the VM guest.
On hosts with restrictive firewalls (e.g. firewalld), the host firewall may additionally block VM traffic that the driver's rules accept. If VM sandboxes cannot reach the network, verify that the host firewall allows forwarding and input for `vmtap-*` interfaces. See the [VM driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-vm/README.md#host-side-nftables-rules) for details.
VM sandboxes boot without a virtual NIC. `openshell-sandbox` intercepts workload
network syscalls inside the guest and carries mediated streams over virtio-vsock
to the host `openshell-supervisor`, which owns DNS, policy evaluation, and
external connections. The driver does not create TAP interfaces or host
nftables rules.
### Corporate Proxy Egress
For proxy-required networks, the VM driver accepts the same corporate egress proxy keys as the Podman driver: `https_proxy`, `no_proxy`, `proxy_auth_file`, `proxy_auth_allow_insecure`, `proxy_connect_by_hostname`, and `proxy_ca_bundle`. The in-guest supervisor chains policy-approved TLS tunnels through the proxy with HTTP CONNECT instead of dialing destinations directly.
For proxy-required networks, the VM driver accepts the same corporate egress proxy keys as the Podman driver: `https_proxy`, `no_proxy`, `proxy_auth_file`, `proxy_auth_allow_insecure`, `proxy_connect_by_hostname`, and `proxy_ca_bundle`. The host supervisor chains policy-approved TLS tunnels through the proxy with HTTP CONNECT instead of dialing destinations directly.
The settings reach the guest supervisor on its command line through a per-sandbox argument file the driver writes into the overlay upperdir on every launch, so a sandbox cannot select, alter, or disable the proxy from inside the guest — including through image `ENV`, the sandbox environment, or files baked into the image at the paths the driver uses.
The settings reach the host supervisor through driver-owned arguments, so a sandbox cannot select, alter, or disable the proxy from inside the guest.
A proxy on the corporate network needs no special address and works on every VM sandbox. The guest's callback to the gateway never traverses the proxy.
A proxy on the gateway host itself works only for libkrun-backed (non-GPU) sandboxes, whose egress leaves through gvproxy: configure `https_proxy = "http://host.openshell.internal:<port>"` rather than a `127.0.0.1` URL, because gvproxy NATs that alias to the host's `127.0.0.1`. GPU sandboxes use the QEMU/TAP backend, where `host.openshell.internal` resolves to the TAP host address and the driver's [host firewall rules](#host-firewall) allow the guest to reach only the gateway port on the host. The driver rejects a gateway-host proxy URL when a sandbox launches on QEMU instead of letting every CONNECT time out, so give GPU sandboxes a proxy address routable from the guest's masqueraded egress.
A proxy on the gateway host works for both libkrun and QEMU sandboxes. Configure
`https_proxy = "http://host.openshell.internal:<port>"`; the host supervisor
normalizes that name to host loopback.
Because a microVM has no bind mounts or container secrets, the driver stages the credential (root-only) and the CA bundle into the per-sandbox overlay disk and removes them with the sandbox. See the [Gateway Configuration File](./gateway-config) reference for the full contract, including the cleartext-credential acknowledgement and the validated-IP CONNECT behavior.
The credential and private CA material stay with the host supervisor rather
than being staged into the guest. See the [Gateway Configuration File](./gateway-config) reference for the full contract, including the cleartext-credential acknowledgement and the validated-IP CONNECT behavior.
## Kubernetes Driver
@@ -462,7 +408,7 @@ owner references or use the sandbox ServiceAccount. The operator namespace
allowlist is a trust grant, not a tenant isolation mechanism.
</Warning>
Helm deployments set Kubernetes driver values through the chart. Canonical TOML places `namespace`, `service_account_name`, and `enable_user_namespaces` in `[openshell.drivers.kubernetes]`; schema version 2 rejects their historical `[openshell.gateway]` locations.
Helm deployments set Kubernetes driver values through the chart.
For maintainer-level implementation details, refer to the [Kubernetes driver README](https://github.com/NVIDIA/OpenShell/blob/main/crates/openshell-driver-kubernetes/README.md).
@@ -470,27 +416,25 @@ For maintainer-level implementation details, refer to the [Kubernetes driver REA
|---|---|---|
| `compute_driver = "kubernetes"` | Not applicable | Select the Kubernetes compute driver. |
| `[openshell.drivers.kubernetes].namespace` | `server.sandboxNamespace` | Set the namespace for sandbox resources. The Helm chart defaults to the release namespace when left empty. |
| `[openshell.drivers.kubernetes].service_account_name` | `sandboxServiceAccount.name` | Set the Kubernetes service account assigned to sandbox pods and accepted by the Kubernetes driver's TokenReview bootstrap path. The Helm chart creates a dedicated sandbox service account by default. |
| `[openshell.drivers.kubernetes].enable_user_namespaces` | `server.enableUserNamespaces` | Enable Kubernetes user namespaces for sandbox pods. |
| `service_account_name` | `sandboxServiceAccount.name` | Set the Kubernetes service account assigned to sandbox pods and accepted by the Kubernetes driver's TokenReview bootstrap path. The Helm chart creates a dedicated sandbox service account by default. |
| `default_image` | `server.sandboxImage` | Set the default sandbox image. |
| `image_pull_policy` | `server.sandboxImagePullPolicy` | Set the canonical sandbox pull policy: `always`, `if_not_present`, or `never`. `newer` is Podman-only. |
| `image_pull_policy` | `server.sandboxImagePullPolicy` | Set the Kubernetes image pull policy for sandbox pods. |
| `image_pull_secrets` | `server.sandboxImagePullSecrets` | Attach Kubernetes image-pull Secrets to sandbox pods. Managed mode copies these explicitly named Secrets from the configured source namespace into each workspace namespace. In shared and operator modes, the Secrets must already exist in the sandbox namespace. |
| `[managed_ssh_ingress]` | `networkPolicy.enabled` | In managed mode, create an SSH ingress policy in every workspace namespace. Helm configures the gateway namespace and pod selector automatically. Operator mode leaves namespace policy management to the platform operator. |
| `grpc_endpoint` | `server.grpcEndpoint` | Set the gateway callback endpoint reachable from sandbox pods. Raw TOML and the standalone Kubernetes driver require an explicit endpoint because the sandbox namespace does not identify the gateway Service. Helm derives it from the release's gateway Service when the value is empty. |
| `grpc_endpoint` | `server.grpcEndpoint` | Set the gateway callback endpoint reachable from sandbox pods. |
| `client_tls_secret_name` | `server.tls.clientTlsSecretName` | Mount sandbox client TLS materials from a Kubernetes secret. |
| `supervisor_image` | `supervisor.image.repository` / `supervisor.image.tag` | Override the supervisor image that provides the `openshell-sandbox` binary. The default repository with an empty tag uses the version-pinned image built into the gateway. Changing the repository uses the effective gateway image tag, while setting a tag pins that version explicitly. |
| `supervisor_image_pull_policy` | `supervisor.image.pullPolicy` | Set the canonical supervisor pull policy: `always`, `if_not_present`, or `never`. `newer` is Podman-only. |
| `supervisor_sideload_method` | `supervisor.sideloadMethod` | How the supervisor binary is delivered into sandbox pods. Leave empty to auto-detect from cluster version. Set to `image-volume` to mount the supervisor OCI image directly as a volume (requires Kubernetes 1.33+ with the ImageVolume feature gate; GA in 1.36), or `init-container` to copy it through an init container on older clusters. |
| `topology` | `supervisor.topology` | Set `combined` for the default single supervisor path, or `sidecar` to move pod-level network enforcement and the gateway session into a dedicated sidecar. |
| `sandbox_runtime_image` | `sandboxRuntime.image.repository` / `sandboxRuntime.image.tag` | Override the image that provides `openshell-sandbox`. The default repository with an empty tag uses the version pinned into the gateway. |
| `sandbox_runtime_image_pull_policy` | `sandboxRuntime.image.pullPolicy` | Set the Kubernetes image pull policy for the sandbox runtime image. |
| `supervisor_image` | `supervisor.image.repository` / `supervisor.image.tag` | Override the image that provides `openshell-supervisor`. The default repository with an empty tag uses the version pinned into the gateway. |
| `supervisor_image_pull_policy` | `supervisor.image.pullPolicy` | Set the Kubernetes image pull policy for the supervisor image. |
| `sandbox_runtime.network_policy_enforced` | `supervisor.sandboxRuntime.networkPolicyEnforced` | Acknowledge that the cluster CNI enforces ingress and egress `NetworkPolicy` in sandbox namespaces. This must be `true`. |
| `sandbox_runtime.boundary_port` | `supervisor.sandboxRuntime.boundaryPort` | Set the non-privileged TLS port used between the paired supervisor and sandbox Pods. |
| `https_proxy` | `upstreamProxy.url` | Set the operator-owned `http://host:port` corporate forward proxy used for policy-approved TLS CONNECT egress. |
| `no_proxy` | `upstreamProxy.noProxy` | Set destinations that bypass only the corporate proxy. OpenShell policy evaluation still applies. |
| `proxy_auth_secret_name` | `upstreamProxy.authSecret.name` | Set the existing Secret name in the sandbox namespace that contains the proxy credential. Requires `sidecar` topology. |
| `proxy_auth_secret_key` | `upstreamProxy.authSecret.key` | Set the Secret key containing the `user:pass` credential. Requires `sidecar` topology. |
| `proxy_auth_secret_name` | `upstreamProxy.authSecret.name` | Set the existing Secret name in the sandbox namespace that contains the proxy credential. The Secret mounts only in the supervisor Pod. |
| `proxy_auth_secret_key` | `upstreamProxy.authSecret.key` | Set the Secret key containing the `user:pass` credential. |
| `proxy_auth_allow_insecure` | `upstreamProxy.authAllowInsecure` | Set `true` to acknowledge that Basic authentication to an HTTP proxy is cleartext. Required with a proxy credential Secret. |
| `proxy_connect_by_hostname` | `upstreamProxy.connectByHostname` | Send hostnames rather than validated IPs in CONNECT requests. Use only when proxy ACLs require hostname targets. |
| `sidecar.proxy_uid` | `supervisor.sidecar.proxyUid` | Dedicated UID of at least `1000` used by the relaxed sidecar when process/binary-aware network policy is disabled. It must not match the workload UID. The default binary-aware sidecar runs as UID 0. The network init container exempts the effective sidecar UID from proxy redirection. |
| `sidecar.process_binary_aware_network_policy` | `supervisor.sidecar.processBinaryAwareNetworkPolicy` | Keep process/binary-aware network policy enabled in `sidecar` topology. The default runs the sidecar as UID 0 with `SYS_PTRACE` and `DAC_READ_SEARCH`. Set false to run as `proxy_uid`, drop both capabilities, and enforce endpoint/L7 policy without matching `policy.binaries`. |
| `app_armor_profile` | `server.appArmorProfile` | Set the sandbox agent container's AppArmor profile. Helm defaults this to `Unconfined` so AppArmor-enabled nodes do not block supervisor network namespace setup. Set the Helm value to an empty string to omit the field, or use `RuntimeDefault` or `Localhost/<profile-name>` for operator-managed profiles. |
| `workspace_default_storage_size` | `server.workspaceDefaultStorageSize` | Set the default workspace PVC size for new sandboxes. |
| `workspace_storage_class` | `server.workspaceStorageClass` | Set the `StorageClass` for the workspace PVC. Empty (default) omits `storageClassName` and uses the cluster's default `StorageClass`. Set this on clusters with no default `StorageClass`, otherwise the workspace PVC stays `Pending` and the sandbox never starts. |
| `sa_token_ttl_secs` | `server.sandboxJwt.k8sSaTokenTtlSecs` | Set the projected ServiceAccount token TTL used for the bootstrap token exchange. |
@@ -502,30 +446,17 @@ remain limited to the explicitly configured TLS and image-pull Secret names.
The driver creates copies only in gateway-owned managed namespaces. Do not
reuse the gateway ServiceAccount for unrelated workloads.
In `combined` topology, the agent container carries the Linux capabilities
needed by the supervisor for network namespace setup, Landlock filesystem
policy, process privilege changes, and network policy enforcement. In `sidecar`
topology, the agent container runs as the resolved sandbox UID/GID with no added
Linux capabilities. A root init container performs the nftables setup, and the
long-running binary-aware sidecar runs as UID 0, drops default capabilities,
and adds `SYS_PTRACE` plus `DAC_READ_SEARCH` for workload process identity
resolution through shared `/proc`. The
`sidecar.process_binary_aware_network_policy = false` setting runs it as the
configured non-root `proxy_uid`, removes both capabilities, and relaxes network
policy to endpoint/L7 matching only. The
network sidecar owns gateway authentication and writes local policy/provider
state to the process supervisor over a local control socket, so the agent
container does not mount the sandbox bootstrap token or client TLS secret in
the default sidecar path. The provider environment is refreshed by the network
sidecar after settings polls and streamed to the process supervisor so future
child processes can see updated provider env without gateway access in the
agent container.
Sidecar mode keeps gateway session and SSH behavior. The process supervisor
applies Landlock filesystem policy and child seccomp filters where supported,
but it does not perform root-to-sandbox privilege dropping or supervisor
identity mount isolation. Network policy still runs in the sidecar, and sidecar
pods set `shareProcessNamespace: true` so the network sidecar can resolve
process/binary identity through `/proc/<entrypoint-pid>`.
The Kubernetes driver always places the sandbox runtime in the workload Pod and
the supervisor in a separate, directly managed Pod. The
workload Pod runs `openshell-sandbox` as the same non-root UID/GID as the agent
and requests no added Linux capabilities. The supervisor Pod runs
`openshell-supervisor`, authenticates to the gateway with a sandbox JWT, and
owns upstream connections. Both containers disable privilege escalation, drop
all capabilities, and use `RuntimeDefault` seccomp. The sandbox adds a nested
seccomp user-notification filter and Landlock restrictions before it launches
the agent. One namespace-wide, empty-egress `NetworkPolicy` is the mandatory
outer fence for all OpenShell workload Pods. It permits supervisor Pods to
reach sandbox listeners; TLS and JWT identity enforce the exact pairing.
The Kubernetes driver creates namespaced `agents.x-k8s.io` `Sandbox` resources from the Kubernetes SIG Apps [agent-sandbox](https://github.com/kubernetes-sigs/agent-sandbox) project. It detects the served Sandbox API at runtime, caches the selected API version for the gateway process, and uses `v1beta1` when available before falling back to `v1alpha1`, so supported Agent Sandbox installations work without version-specific operator configuration. The Agent Sandbox controller turns those resources into sandbox pods and related storage.
@@ -670,7 +601,7 @@ The resolved UID/GID appear in:
### VM Driver
The VM driver preserves an image-provided `sandbox` account when `sandbox_uid` and `sandbox_gid` are omitted. Images without that account use UID/GID `1000`. Explicit values in `[openshell.drivers.vm]` override the image account. Persisted overlays retain the UID/GID recorded when they were created. An unmarked overlay recovers identity from concrete overlay or prepared-image state, an explicit override, or the current image; the driver never assigns legacy `10001:10001` without persisted evidence.
The VM driver injects the sandbox UID into the rootfs guest's `/etc/passwd`, `/etc/group`, and `/etc/gshadow` during rootfs preparation. Default UID is `10001`; configure `sandbox_uid` in `[openshell.drivers.vm]` to use a different value.
### Custom Images
+9 -3
View File
@@ -84,12 +84,18 @@ To override the default image references, use Helm values:
## Kernel Requirements
OpenShell enforces sandbox isolation through two Linux kernel security modules:
The sandbox boundary requires the following Linux kernel facilities, including
when it runs inside a container or microVM:
| Module | Requirement | Details |
| -------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [Landlock LSM](https://docs.kernel.org/security/landlock.html) | Recommended | Enforces filesystem access restrictions at the kernel level. The `best_effort` compatibility mode uses the highest Landlock ABI the host kernel supports. The `hard_requirement` mode fails sandbox creation if the required ABI is unavailable. |
| seccomp | Required | Filters dangerous system calls. Available on all modern Linux kernels (3.17+). |
| [Landlock LSM](https://docs.kernel.org/security/landlock.html) | Required | ABI 3 or newer, introduced in Linux 6.2, with Landlock enabled. The mandatory baseline protects private channel and bootstrap files, including against truncation. A filesystem policy's `best_effort` setting never disables this baseline. |
| seccomp | Required | Nested user-notification filters and atomic `SECCOMP_IOCTL_NOTIF_ADDFD` with `SECCOMP_ADDFD_FLAG_SEND`, usable under the runtime's existing seccomp profile without added capabilities. The sandbox actively probes these operations before admitting the workload. |
A kernel version alone does not establish support. A disabled Landlock LSM or a
runtime profile that blocks the required seccomp operations causes launch to
fail closed. An upstream Linux 6.2 or newer kernel provides the required
Landlock ABI; distribution backports must pass the same active qualification.
On macOS, these kernel modules run inside the Docker Desktop Linux VM, not on the host kernel.
+11
View File
@@ -46,6 +46,17 @@ captures the command result, and deletes the sandbox after the command exits:
openshell sandbox create --no-keep -- sh -c 'echo done; exit 0'
```
Combine `--detach` and `--no-keep` for a background workload whose lifecycle
belongs entirely to the gateway:
```shell
openshell sandbox create --detach --no-keep -- ./worker
```
The create command returns after the workload is ready. The gateway keeps the
canonical process running without a host-side attachment and deletes the
sandbox after that process exits.
`--upload` cannot yet be combined with a trailing main command because uploads
finish after the canonical process starts. Create a scratch sandbox, upload the
files, then launch the workload with `sandbox exec`, or build the files into the
+6
View File
@@ -90,6 +90,12 @@ to forward conversation text. Malformed placeholder candidates and candidates
longer than 4096 wire bytes are denied. Authentication headers and HTTP trailers
retain their credential restrictions.
When `openshell-sandbox` and the supervisor run separately, the supervisor mediates DNS by
hostname. DNS sender identity is unavailable because native socket writes can
come from a process that inherited the socket or replaced its executable.
Resolving a name does not authorize a connection: the supervisor still checks
the destination and calling binary when the workload opens TCP traffic.
## Supervisor Middleware
Supervisor middleware can inspect, deny, or replace admitted HTTP request bodies and client WebSocket text messages before provider credentials are injected. Middleware selection is independent of the `network_policies` rule that admitted the traffic: each keyed `network_middlewares` entry matches the destination host through `endpoints.include` and `endpoints.exclude`.
+11 -5
View File
@@ -159,14 +159,20 @@ Paths listed in `read_only` receive read-only access.
Paths listed in `read_write` receive full access.
All other paths are inaccessible.
Landlock setup runs in two phases. The parent supervisor probes the kernel ABI and opens the configured path file descriptors before forking. The child then applies the ruleset with `restrict_self()` after privilege drop. At startup, OpenShell emits the selected ABI version and the applied read-only and read-write rule counts so you can confirm what the kernel accepted.
The sandbox installs a mandatory Landlock baseline before running agent code.
It requires ABI 3 or newer, introduced in Linux 6.2, so denied writes include
file truncation. The baseline hides the private channel and bootstrap root from
the agent by granting access only to explicitly opened, permitted root children;
it never grants the filesystem root or follows a root-level symlink when
constructing that allowlist. Startup fails if this baseline cannot be enforced.
The configured filesystem policy adds restrictions on top of the baseline.
| Aspect | Detail |
|---|---|
| Default | `compatibility: best_effort`. Uses the highest kernel ABI available. Missing paths are skipped. If the kernel does not support Landlock or any configured path cannot be opened, the sandbox continues without those restrictions and emits a High-severity OCSF `DetectionFinding`. |
| What you can change | Set `compatibility: hard_requirement` to abort sandbox startup if Landlock is unavailable or any configured path cannot be opened. |
| Risk if relaxed | On kernels without Landlock (pre-5.13), or when all paths fail to open, the sandbox runs without kernel-level filesystem restrictions. The agent can access any file the process user can access. |
| Recommendation | Use `best_effort` for development. Use `hard_requirement` in environments where any gap in filesystem isolation is unacceptable. Treat High-severity Landlock findings as a signal to investigate the host kernel or the image. Run on Ubuntu 22.04+ or any kernel 5.13+ for Landlock support. |
| Default | The mandatory baseline always requires Landlock ABI 3 or newer. The additional filesystem policy defaults to `compatibility: best_effort`, which may skip unavailable policy paths and emits a High-severity OCSF `DetectionFinding` when that policy cannot be fully applied. |
| What you can change | Set `compatibility: hard_requirement` to reject startup when the additional filesystem policy cannot be applied. Neither setting relaxes the private-file baseline. |
| Risk if relaxed | A skipped additional policy can expose otherwise permitted image or workspace files to the agent. It cannot grant access to files excluded by the mandatory baseline. |
| Recommendation | Require Linux 6.2 or newer with Landlock enabled, or a qualified distribution backport providing ABI 3. Use `hard_requirement` where all configured filesystem restrictions must apply. Investigate High-severity Landlock findings rather than treating them as a supported degraded security mode. |
### Read-Only vs Read-Write Paths