mirror of
https://github.com/NVIDIA/OpenShell.git
synced 2026-10-02 07:34:45 +08:00
* feat(gateway): add TOML configuration file (RFC 0003)
Introduces an opt-in --config / OPENSHELL_GATEWAY_CONFIG flag that loads a
TOML file with gateway-wide settings and per-driver tables. Source
precedence is CLI > env > file > built-in default, implemented via clap's
ValueSource so existing flags and env vars keep their priority.
Driver crates (kubernetes, docker, podman, vm) now derive Deserialize on
their config structs. SupervisorSideloadMethod gains Deserialize with
kebab-case rename. A per-driver inheritance allowlist on the loader side
overlays [openshell.gateway] shared defaults (default_image,
supervisor_image, image_pull_policy, guest_tls_*, ssh_handshake_skew_secs,
client_tls_secret_name, host_gateway_ip, enable_user_namespaces) onto
each [openshell.drivers.<name>] table before deserialization.
The Helm chart renders a new gateway-config ConfigMap and mounts it at
/etc/openshell/gateway.toml. The migrated OPENSHELL_* env entries are
dropped from the StatefulSet — only the Secret-backed
OPENSHELL_SSH_HANDSHAKE_SECRET remains. database_url stays on --db-url.
Adds examples/gateway/gateway.example.toml and updates architecture/gateway.md
with the source precedence and inheritance rules.
* docs(gateway): drop ssh_handshake_skew_secs and ssh_handshake_secret from examples
Both fields are scheduled for removal. Remove the example values and the
env-only note so the gateway.toml example and the architecture doc stop
recommending settings that will not exist much longer.
* docs(rfc): correct OPENSHELL_CONFIG to OPENSHELL_GATEWAY_CONFIG in RFC 0003
* docs(gateway): add per-driver TOML example configurations
Adds focused single-driver examples next to the comprehensive
gateway.example.toml: kubernetes, docker, podman, and microvm. Each one
demonstrates the realistic settings for that driver plus how shared
[openshell.gateway] defaults inherit into the driver table.
A new unit test (`checked_in_examples_parse`) loads every example through
the config_file loader so schema drift fails CI rather than silently
shipping a broken example.
* refactor(gateway): drop image_pull_policy from shared inheritance
Kubernetes and Podman use mutually-incompatible vocabularies for the same
TOML key:
- Kubernetes: `Always | IfNotPresent | Never` (free-form string passed
verbatim to the K8s API).
- Podman: `always | missing | never | newer` (strict lowercase enum
deserialised into `ImagePullPolicy`).
No value means the same thing in both drivers. Sharing the key at
`[openshell.gateway]` scope and inheriting it into every active driver's
table meant any value safe for one driver was either wrong or silently
dropped for the other (`IfNotPresent` → `ImagePullPolicy::Missing` after
`.unwrap_or_default()`). Operators run one driver per gateway, so the
"shared default" never pays for itself.
Make `image_pull_policy` driver-local:
- Remove the field from `GatewayFileSection` and from
`inheritable_keys()` for both Kubernetes and Podman.
- Drop the file→`RunArgs` merge for the gateway-scope key.
- Stop unconditionally clobbering the driver value with
`config.sandbox_image_pull_policy` in the runtime wiring — only apply
the CLI/env override when it was set (and, for Podman, only when it
parses into the lowercase enum).
- Move the key under `[openshell.drivers.kubernetes]` and
`[openshell.drivers.podman]` in every example, the RFC, the
architecture doc, and the Helm-rendered gateway ConfigMap.
The supervisor pull policy follows the same shape: it is K8s-only and
moves into `[openshell.drivers.kubernetes]` alongside `image_pull_policy`
in the Helm template.
* fix(gateway): address review feedback on TOML configuration
Resolves the P1 and P2 issues raised in PR #1317:
- Helm gateway ConfigMap moves `grpc_endpoint` under
`[openshell.drivers.kubernetes]` so the default install no longer fails
the gateway's `deny_unknown_fields` schema check.
- `kubernetes_config_from_file` and `podman_config_from_file` only let
the gateway-wide CLI/env `grpc_endpoint` overwrite the driver-table
value when it was actually supplied, preserving file-only configs.
- Kubernetes driver default `image_pull_policy` is now empty (was Podman
vocabulary "missing"), so default deployments let the Kubernetes API
apply its own policy instead of being rejected.
- New `disable_tls` gateway field plumbs `.Values.server.disableTls`
through the TOML ConfigMap instead of relying on env vars dropped from
the StatefulSet.
- StatefulSet pod template now carries a `checksum/gateway-config`
annotation so `helm upgrade` rolls pods when the ConfigMap changes.
- Auxiliary listener resolution preserves the full `SocketAddr` from
`health_bind_address` / `metrics_bind_address`, so a loopback-pinned
health port is not silently relocated onto the public bind address.
- `ssh_session_ttl_secs` from the file is now applied to `Config` (it
was previously accepted by the loader but never read).
New regression coverage: cli-level merge tests for the new fields plus
helm-unittest assertions for the ConfigMap shape, checksum annotation,
and `disable_tls` rendering.
* docs(gateway): consolidate gateway TOML examples into docs reference
Replaces the per-driver example files under examples/gateway/ with a
single published reference page at docs/reference/gateway-config.mdx
covering source precedence, layout, the full example, and the four
per-driver examples (Kubernetes, Docker, Podman, microVM). Drew flagged
during PR #1317 review that the examples belong with the user-facing
docs rather than in a sibling examples/ directory.
The cross-references in architecture/gateway.md and RFC 0003 are updated
to point at the new docs page; the round-trip test in config_file.rs is
removed (schema coverage stays on the inline parses_full_example test
and per-field merge tests — doc-snippet drift belongs in a separate
docs-lint, not in a cross-tree Rust unit test).
* refactor(core): move DEFAULT_K8S_NAMESPACE into K8s driver
The constant is Kubernetes-specific (used only by KubernetesComputeConfig's
Default impl) and does not belong in openshell-core. Relocate it to the
driver crate that owns the K8s vocabulary; openshell-core retains only
truly cross-cutting defaults.
* refactor(core): move Podman bridge default into Podman driver
DEFAULT_NETWORK_NAME is Podman vocabulary, consumed only by the Podman
driver. Also drops the unused DEFAULT_IMAGE_PULL_POLICY constant.
* docs(auth): scrub remaining SSH handshake secret references
Sweeps the trailing mentions left after the rebase: the gateway
config-file module doc, the Helm gateway-config ConfigMap header,
the gateway-config.mdx env-only note, and the RPM systemd unit
comment for init-gateway-env.sh.
* docs(gateway): clarify OPENSHELL_GRPC_ENDPOINT applies to all drivers
The previous comment implied the callback endpoint was Kubernetes-only,
but the value is propagated to every compute driver (Kubernetes, Docker,
Podman, VM) and must be reachable from wherever the sandbox runs.
* refactor(gateway): move driver options into config (#1394)
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
* fix(e2e): regenerate gateway config via TOML for docker + podman harnesses
The gateway CLI flags moved into TOML config tables in 560550d2 (#1394),
which made every existing e2e/with-{docker,podman}-gateway.sh invocation
fail with "unexpected argument '--sandbox-namespace'" (and a long tail
of similar driver-specific options) before the gateway could even bind.
Replace the obsolete CLI flags with a synthesized
`[openshell.drivers.<driver>]` table written to `${STATE_DIR}/gateway.toml`
and passed via the new `--config` flag. Only the gateway-wide flags
that survived 560550d2 (bind-address, port, drivers, db-url, tls-*,
disable-tls, log-level, health-port) stay on the command line.
Both scripts get a small `toml_string` helper to properly TOML-quote
the values (the previous `%q` printf format produced bash-escape, not
TOML-escape). The Docker harness also corrects two field names that
diverged from the driver schema: `docker_network_name` →
`network_name`, and the supervisor binary/image plumbing now reads
through to `supervisor_bin` / `supervisor_image` in the same table.
The Podman harness drops `--ssh-gateway-port` (deleted in 560550d2 —
gRPC + SSH are multiplexed on the same port now) and substitutes
`network_name` + `gateway_port` for the obsolete `--sandbox-namespace`
(which the Podman driver never had as a typed field).
* fix(core): swap bind-only 0.0.0.0 SSH gateway host for cluster URL host
CLI's resolve_ssh_gateway treated 0.0.0.0 as a loopback "keep as-is"
when the cluster URL was also loopback, so the SSH proxy connected to
0.0.0.0:port. The unspecified address is never a valid connect target
and is not present in any TLS cert SAN, which produced BadCertificate
TLS handshake failures during `openshell sandbox create -- ...` in
docker/podman e2e (e.g. bypass_detection).
Resolution: when the server returns 0.0.0.0 or :: as the gateway host
and both endpoints are loopback, fall back to the cluster URL's host
(which the CLI is already using to reach the gateway, so it must
resolve and match the cert).
* fix(e2e): repair podman harness on macOS
Podman 5.x with the applehv/libkrun provider no longer creates the legacy
~/.local/share/containers/podman/machine/podman.sock symlink, and
`podman system service` is a Linux-only subcommand — the macOS client
delegates the API service to the VM. Both assumptions in the harness
were stale, so the script tried to start a temporary service that podman
rejected with "unknown flag: --time".
- Discover the macOS socket via `podman machine inspect` instead of the
hardcoded path.
- On Darwin, fail fast with a "start podman machine" message rather than
attempting the Linux-only `podman system service` fallback.
- Write socket_path into [openshell.drivers.podman] so the in-process
driver picks up the discovered socket; the driver reads TOML only
after the config refactor (560550d2), so OPENSHELL_PODMAN_SOCKET alone
was no longer enough.
* fix(server): use clone_from for TLS client CA assignment
clippy 1.95.0 rejects assigning the result of `Clone::clone()` to an
existing variable under `-D warnings` (`assigning_clones`). Switch to
`clone_from(&...)` to satisfy the lint and avoid the redundant
allocation.
---------
Signed-off-by: Drew Newberry <anewberry@nvidia.com>
Co-authored-by: Drew Newberry <anewberry@nvidia.com>
301 lines
25 KiB
Plaintext
301 lines
25 KiB
Plaintext
---
|
|
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
title: "OpenShell Security Best Practices for Controls, Risks, and Configuration Guidance"
|
|
sidebar-title: "Security Best Practices"
|
|
slug: "security/best-practices"
|
|
description: "A guide to every configurable security control in OpenShell: defaults, what you can change, and the risks of each choice."
|
|
keywords: "Generative AI, Cybersecurity, Security, Policy, Sandbox, Landlock, Seccomp"
|
|
position: 1
|
|
---
|
|
|
|
OpenShell enforces sandbox security across four layers: network, filesystem, process, and inference.
|
|
This page documents every configurable control, its default, what it protects, and the risk of relaxing it.
|
|
|
|
For the full policy YAML schema, refer to the [Policy Schema](/reference/policy-schema).
|
|
For the architecture of each enforcement layer, refer to [How OpenShell Works](/about/how-it-works).
|
|
|
|
<Note>
|
|
If you use [NemoClaw](https://github.com/NVIDIA/NemoClaw) to run OpenClaw assistants, its [Security Best Practices](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) guide covers additional entrypoint-level controls, policy presets, provider trust tiers, and posture profiles specific to the NemoClaw blueprint.
|
|
|
|
</Note>
|
|
|
|
## Enforcement Layers
|
|
|
|
OpenShell applies security controls at two enforcement points.
|
|
OpenShell locks static controls at sandbox creation and requires destroying and recreating the sandbox to change them.
|
|
You can update dynamic controls on a running sandbox with `openshell policy update` or `openshell policy set`.
|
|
|
|
| Layer | What it protects | Enforcement point | Changeable at runtime |
|
|
| --- | --- | --- | --- |
|
|
| Network | Unauthorized outbound connections and data exfiltration. | CONNECT proxy + OPA policy engine | Yes. Use `openshell policy update`, `openshell policy set`, or operator approval in the TUI. |
|
|
| Filesystem | System binary tampering, credential theft, config manipulation. | Landlock LSM (kernel level) | No. Requires sandbox re-creation. |
|
|
| Process | Privilege escalation, fork bombs, dangerous syscalls. | Seccomp BPF + privilege drop (`setuid`/`setgid`) | No. Requires sandbox re-creation. |
|
|
| Inference | Credential exposure, unauthorized model access. | Proxy intercept of `inference.local` | Yes. Use `openshell inference set`. |
|
|
|
|
## Network Controls
|
|
|
|
The CONNECT proxy and OPA policy engine enforce all network controls at the gateway level.
|
|
|
|
### Deny-by-Default Egress
|
|
|
|
Every outbound connection from the sandbox goes through the CONNECT proxy.
|
|
The proxy evaluates each connection against the OPA policy engine.
|
|
If no `network_policies` entry matches the destination host, port, and calling binary, the proxy denies the connection.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | All egress denied. Only endpoints listed in `network_policies` can receive traffic. |
|
|
| What you can change | Add entries to `network_policies` in the policy YAML. Apply statically at creation (`--policy`) or dynamically (`openshell policy update` for incremental changes, `openshell policy set` for full replacement). |
|
|
| Risk if relaxed | Each allowed endpoint is a potential data exfiltration path. The agent can send workspace content, credentials, or conversation history to any reachable host. |
|
|
| Recommendation | Add only endpoints the agent needs for its task. Start with a minimal policy and use denied-request logs (`openshell logs <name> --source sandbox`) to identify missing endpoints. |
|
|
|
|
### Network Namespace Isolation
|
|
|
|
The sandbox runs in a dedicated Linux network namespace with a veth pair.
|
|
All traffic routes through the host-side veth IP (`10.200.0.1`) where the proxy listens.
|
|
Even if a process ignores proxy environment variables, it can only reach the proxy.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | Always active. The sandbox cannot bypass the proxy at the network level. |
|
|
| What you can change | This is not a user-facing knob. OpenShell always enforces it in proxy mode. |
|
|
| Risk if bypassed | Without network namespace isolation, a process could connect directly to the internet, bypassing all policy enforcement. |
|
|
| Recommendation | No action needed. OpenShell enforces this automatically. |
|
|
|
|
### User Namespace Isolation
|
|
|
|
Kubernetes user namespaces (`hostUsers: false`) map container UID 0 to an unprivileged host UID.
|
|
Capabilities like `CAP_SYS_ADMIN` become namespaced. They grant power over container-local resources only, not the host.
|
|
This provides defense-in-depth: even if a container escape vulnerability exists, the attacker lands as an unprivileged host user.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | Disabled. Set `server.enableUserNamespaces: true` in Helm values or `enable_user_namespaces = true` in the gateway config to enable cluster-wide. |
|
|
| What you can change | Enable cluster-wide through Helm or gateway config. Override per-sandbox through the `user_namespaces` field on `SandboxTemplate` in the API. |
|
|
| Prerequisites | Kubernetes 1.33+ with user namespace support available (beta through 1.35, GA in 1.36+), a container runtime that supports user namespaces (containerd 2.0+, CRI-O 1.25+), and Linux 5.12+ for ID-mapped mounts. |
|
|
| Risk if enabled with GPU | NVIDIA device plugin compatibility with user namespaces is unverified. OpenShell logs a warning when both GPU and user namespaces are active on the same sandbox. |
|
|
| Recommendation | Enable on non-GPU clusters running Kubernetes with user namespace support available (1.33+ beta, 1.36+ GA) for stronger host isolation. Test GPU workloads separately before enabling on GPU clusters. |
|
|
|
|
### Binary Identity Binding
|
|
|
|
The proxy identifies which binary initiated each connection by reading `/proc/<pid>/exe` (the kernel-trusted executable path).
|
|
It walks the process tree for ancestor binaries and parses `/proc/<pid>/cmdline` for script interpreters.
|
|
The proxy SHA256-hashes each binary on first use (trust-on-first-use). If someone replaces a binary mid-session, the hash mismatch triggers an immediate deny.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | Every `network_policies` entry requires a `binaries` list. Only listed binaries can reach the associated endpoints. Binary paths support glob patterns (`*` for one path component, `**` for recursive). |
|
|
| What you can change | Add binaries to an endpoint entry. Use glob patterns for directory-scoped access (for example, `/sandbox/.vscode-server/**`). |
|
|
| Risk if relaxed | Broad glob patterns (like `/**`) allow any binary to reach the endpoint, defeating the purpose of binary-scoped enforcement. |
|
|
| Recommendation | Scope binaries to the specific executables that need each endpoint. Use narrow globs when the exact path varies (for example, across Python virtual environments). |
|
|
|
|
### L4-Only vs L7 Inspection
|
|
|
|
The `protocol` field on an endpoint controls whether the proxy inspects individual HTTP requests inside the tunnel.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | Endpoints without a `protocol` field use L4-only enforcement: the proxy checks host, port, and binary, then relays the TCP stream without inspecting payloads. |
|
|
| What you can change | Add `protocol: rest` to enable per-request HTTP method/path inspection, `protocol: websocket` to inspect RFC 6455 upgrade handshakes and client text messages, or `protocol: graphql` to inspect GraphQL-over-HTTP operation type, operation name, and root fields. WebSocket endpoints can also use GraphQL operation rules for GraphQL-over-WebSocket messages. Pair inspected protocols with `rules` or access presets (`full`, `read-only`, `read-write`). REST endpoints that need credential placeholders in supported text request bodies can set `request_body_credential_rewrite: true`. |
|
|
| Risk if relaxed | L4-only endpoints allow the agent to send any data through the tunnel after the initial connection is permitted. The proxy cannot see HTTP methods, paths, or GraphQL operations. Adding `access: full` with L7 inspection enables observability but permits all inspected actions. |
|
|
| Recommendation | Use `protocol: rest` with specific `rules` for APIs where intent is encoded in method and path. Add `request_body_credential_rewrite: true` only for REST APIs that require OpenShell-managed credentials in UTF-8 JSON, form, or text request bodies. Use `protocol: graphql` for GraphQL-over-HTTP APIs where destructive operations are body-encoded. Use `protocol: websocket` for RFC 6455 endpoints, with explicit `GET` and `WEBSOCKET_TEXT` rules for raw text protocols or explicit GraphQL operation rules for GraphQL-over-WebSocket. Prefer `access: read-only` or explicit allowlists, and deny hash-only persisted queries unless you maintain a trusted registry. Omit `protocol` for non-HTTP protocols. For WebSocket endpoints that must carry placeholder credentials in client text frames, add `websocket_credential_rewrite: true`. |
|
|
|
|
### Enforcement Mode (`audit` vs `enforce`)
|
|
|
|
When L7 inspection is active, the `enforcement` field controls whether the proxy blocks or logs rule violations.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | `audit`. The proxy logs violations but forwards traffic. |
|
|
| What you can change | Set `enforcement: enforce` to block requests that do not match any `rules` entry. Denied requests receive a `403 Forbidden` response with a JSON body describing the violation. |
|
|
| Risk if relaxed | `audit` mode provides visibility but does not prevent unauthorized actions. An agent can still perform write or delete operations on an API even if the rules would deny them. |
|
|
| Recommendation | Start with `audit` to understand traffic patterns and verify that rules are correct. Switch to `enforce` after you validate that the rules match the intended access pattern. |
|
|
|
|
### TLS Handling
|
|
|
|
The proxy auto-detects TLS on every tunnel by peeking the first bytes.
|
|
When a TLS ClientHello is detected, the proxy terminates TLS transparently using a per-sandbox ephemeral CA.
|
|
This enables credential injection and L7 inspection without explicit configuration.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | Auto-detect and terminate. OpenShell generates the sandbox CA at startup and injects it into the process trust stores (`NODE_EXTRA_CA_CERTS`, `SSL_CERT_FILE`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, `GIT_SSL_CAINFO`). |
|
|
| What you can change | Set `tls: skip` on an endpoint to disable TLS detection and termination for that endpoint. Use this for client-certificate mTLS to upstream or non-standard binary protocols. |
|
|
| Risk if relaxed | `tls: skip` disables credential injection and L7 inspection for that endpoint. The proxy relays encrypted traffic without seeing the contents. |
|
|
| Recommendation | Use auto-detect (the default) for most endpoints. Use `tls: skip` only when the upstream requires the client's own TLS certificate (mTLS) or uses a non-HTTP protocol. |
|
|
|
|
### SSRF Protection
|
|
|
|
After OPA policy allows a connection, the proxy resolves DNS and rejects connections where the resolved IP is internal (loopback, link-local, or RFC 1918 private).
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | The proxy blocks all private IPs. Loopback (`127.0.0.0/8`), link-local (`169.254.0.0/16`), and unspecified (`0.0.0.0`) addresses are always blocked and cannot be overridden with `allowed_ips`. |
|
|
| What you can change | Add `allowed_ips` (CIDR notation) to an endpoint to permit connections to specific private IP ranges. On Linux, the proxy consults the sandbox's `/etc/hosts` before DNS, so Kubernetes `hostAliases` can make LAN-only hostnames resolvable. Policies with `allowed_ips` entries that overlap loopback, link-local, or unspecified addresses fail to load with a clear validation error. |
|
|
| Risk if relaxed | Without SSRF protection, a misconfigured policy could allow the agent to reach cloud metadata services (`169.254.169.254`), internal databases, or other infrastructure endpoints through DNS rebinding. |
|
|
| Recommendation | Use `allowed_ips` only for known internal services. Scope the CIDR as narrowly as possible (for example, `10.0.5.20/32` for a single host). Loopback, link-local, and unspecified addresses are always blocked regardless of `allowed_ips`. `hostAliases` change resolution, not authorization: `host: searxng.local` with `/etc/hosts` mapping to `192.168.1.105` still needs `allowed_ips: ["192.168.1.105/32"]`. The policy advisor does not propose rules for always-blocked destinations. |
|
|
|
|
### Operator Approval
|
|
|
|
When the agent requests an endpoint not in the policy, OpenShell blocks it and surfaces the request in the TUI for operator review.
|
|
The system merges approved endpoints into the sandbox's policy as a new durable revision.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | Enabled. The proxy blocks unlisted endpoints and requires approval. |
|
|
| What you can change | Approved endpoints persist across sandbox restarts within the same sandbox instance. They reset when the sandbox is destroyed and recreated. |
|
|
| Risk if relaxed | Approving an endpoint permanently widens the running sandbox's policy. Review each request before approving. |
|
|
| Recommendation | Use operator approval for exploratory work. For recurring endpoints, add them to the policy YAML with appropriate binary and path restrictions. To reset all approved endpoints, destroy and recreate the sandbox. |
|
|
|
|
## Filesystem Controls
|
|
|
|
Landlock LSM restricts which paths the sandbox process can read or write at the kernel level.
|
|
|
|
### Landlock LSM
|
|
|
|
Landlock enforces filesystem access at the kernel level.
|
|
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.
|
|
|
|
| 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. |
|
|
|
|
### Read-Only vs Read-Write Paths
|
|
|
|
The policy separates filesystem paths into read-only and read-write groups.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | System paths (`/usr`, `/lib`, `/etc`, `/var/log`) are read-only. Working paths (`/sandbox`, `/tmp`) are read-write. `/app` is conditionally included if it exists. |
|
|
| What you can change | Add or remove paths in `filesystem_policy.read_only` and `filesystem_policy.read_write`. |
|
|
| Risk if relaxed | Making system paths writable lets the agent replace binaries, modify TLS trust stores, or change DNS resolution. Validation rejects broad read-write paths (like `/`). |
|
|
| Recommendation | Keep system paths read-only. If the agent needs additional writable space, add a specific subdirectory. |
|
|
|
|
### Path Validation
|
|
|
|
OpenShell validates policies before they take effect.
|
|
|
|
| Constraint | Behavior |
|
|
|---|---|
|
|
| Paths must be absolute (start with `/`). | Rejected with `INVALID_ARGUMENT`. |
|
|
| Paths must not contain `..` traversal. | Rejected with `INVALID_ARGUMENT`. |
|
|
| Read-write paths must not be overly broad (for example, `/` alone). | Rejected with `INVALID_ARGUMENT`. |
|
|
| Each path must not exceed 4096 characters. | Rejected with `INVALID_ARGUMENT`. |
|
|
| Combined `read_only` + `read_write` paths must not exceed 256. | Rejected with `INVALID_ARGUMENT`. |
|
|
|
|
## Process Controls
|
|
|
|
The sandbox supervisor drops privileges, applies seccomp filters, and enforces process-level restrictions during startup.
|
|
|
|
### Privilege Drop
|
|
|
|
The sandbox process runs as a non-root user after explicit privilege dropping.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | `run_as_user: sandbox`, `run_as_group: sandbox`. The supervisor calls `setuid()`/`setgid()` with post-condition verification, disables core dumps with `RLIMIT_CORE=0`, and on Linux sets `PR_SET_DUMPABLE=0`. |
|
|
| What you can change | Set `run_as_user` and `run_as_group` in the `process` section. Validation rejects root (`root` or `0`). |
|
|
| Risk if relaxed | Running as a higher-privilege user increases the impact of container escape vulnerabilities. |
|
|
| Recommendation | Keep the `sandbox` user. Do not attempt to set root. |
|
|
|
|
### Seccomp Filters
|
|
|
|
OpenShell applies seccomp in two phases. A narrow supervisor-startup prelude runs before CLI parsing and async runtime initialization, then the child process receives the broader runtime seccomp filter after privilege drop.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Startup prelude | After privileged bootstrap helpers complete, the supervisor sets `PR_SET_NO_NEW_PRIVS` and synchronizes a seccomp filter across all runtime threads that blocks `mount`, the new mount API syscalls, `pivot_root`, `umount2`, `bpf`, `perf_event_open`, `userfaultfd`, module-loading syscalls, and kexec. This closes the long-lived privileged remount and kernel-surface window while leaving required setup syscalls such as `setns` available. |
|
|
| Socket domains | The filter allows `AF_INET` and `AF_INET6` (for proxy communication) and blocks `AF_NETLINK`, `AF_PACKET`, `AF_BLUETOOTH`, and `AF_VSOCK` with `EPERM`. |
|
|
| Runtime unconditional syscall blocks | `memfd_create`, `ptrace`, `bpf`, `process_vm_readv`, `process_vm_writev`, `pidfd_open`, `pidfd_getfd`, `pidfd_send_signal`, `io_uring_setup`, `mount`, `fsopen`, `fsconfig`, `fsmount`, `fspick`, `move_mount`, `open_tree`, `setns`, `umount2`, `pivot_root`, `userfaultfd`, `perf_event_open`. |
|
|
| Conditional syscall blocks | `execveat` with `AT_EMPTY_PATH`, `unshare` and `clone` with `CLONE_NEWUSER`, and `seccomp(SECCOMP_SET_MODE_FILTER)` are denied with `EPERM`. |
|
|
| What you can change | This is not a user-facing knob. OpenShell enforces it automatically. |
|
|
| Risk if relaxed | The blocked syscalls support container escape (`mount`, `pivot_root`, `move_mount`, namespace creation), cross-process observation (`ptrace`, `process_vm_readv`, `pidfd_*`), raw kernel bypass (`bpf`, `io_uring_setup`, `perf_event_open`), and filter evasion (`seccomp`, `userfaultfd`). |
|
|
| Recommendation | No action needed. OpenShell enforces this automatically. |
|
|
|
|
### Enforcement Application Order
|
|
|
|
The sandbox supervisor applies enforcement in a specific order during process startup.
|
|
This ordering is intentional: named network-namespace setup still relies on privileged helpers, and privilege dropping still needs `/etc/group` and `/etc/passwd`, which Landlock subsequently restricts.
|
|
|
|
1. Privileged supervisor bootstrap helpers, including network-namespace setup and optional `iptables` probes.
|
|
2. Supervisor startup prelude seccomp (`PR_SET_NO_NEW_PRIVS` plus the early syscall denylist) synchronized across runtime threads.
|
|
3. Network namespace entry (`setns`) in child `pre_exec`.
|
|
4. Privilege drop (`initgroups` + `setgid` + `setuid`).
|
|
5. Core-dump hardening (`RLIMIT_CORE=0`, plus `PR_SET_DUMPABLE=0` on Linux).
|
|
6. Landlock filesystem restrictions.
|
|
7. Runtime seccomp socket domain and syscall filters.
|
|
|
|
## Inference Controls
|
|
|
|
OpenShell routes all inference traffic through the gateway to isolate provider credentials from the sandbox.
|
|
|
|
### Routed Inference through `inference.local`
|
|
|
|
The proxy intercepts HTTPS CONNECT requests to `inference.local` and routes matching inference API requests through the sandbox-local router.
|
|
The agent never receives the provider API key.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | Always active. The proxy handles `inference.local` before OPA policy evaluation. The gateway injects credentials on the host side. |
|
|
| Keep-alive isolation | If a sandbox reuses a keep-alive connection that previously carried a routed inference request for a subsequent non-inference request, the proxy denies the non-inference request with `connection not allowed by policy` and closes the connection. This prevents agents from reusing an inference-authorized connection for other destinations. |
|
|
| What you can change | Configure inference routes with `openshell inference set`. |
|
|
| Risk if bypassed | If an inference provider's host is added directly to `network_policies`, the agent could reach it with a stolen or hardcoded key, bypassing credential isolation. |
|
|
| Recommendation | Do not add inference provider hosts to `network_policies`. Use OpenShell inference routing instead. |
|
|
|
|
## Gateway Security
|
|
|
|
The gateway secures communication between the CLI, sandbox workloads, and external clients with mutual TLS and token-based authentication.
|
|
|
|
### mTLS
|
|
|
|
Communication between the CLI, sandbox workloads, and the gateway is secured by mutual TLS.
|
|
For Helm deployments, provide a CA and certificate bundle through the Kubernetes secrets consumed by the chart.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | mTLS required for gateway APIs and sandbox callbacks. TLS-enabled loopback gateways also accept plaintext HTTP for sandbox service hostnames by default. |
|
|
| What you can change | Enable dual-auth mode (`allow_unauthenticated=true`) for Cloudflare Tunnel deployments, disable TLS entirely for trusted reverse-proxy setups, or disable loopback service HTTP with `--enable-loopback-service-http=false`. |
|
|
| Risk if relaxed | Dual-auth mode accepts clients without certificates and defers authentication to the HTTP layer (Cloudflare JWT). Disabling TLS removes transport-level authentication entirely. Loopback service HTTP is local-only and rejects cross-origin browser requests, but any local process can still reach exposed service URLs directly. |
|
|
| Recommendation | Use mTLS (the default) unless deploying behind Cloudflare or a trusted reverse proxy. |
|
|
|
|
### SSH Tunnel Authentication
|
|
|
|
SSH connections to sandboxes travel through the gateway over the sandbox's existing mTLS session. On top of the gateway's transport authentication (mTLS by default), each SSH connect call also carries a short-lived session token scoped to a specific sandbox. The sandbox never exposes an SSH port on the network. Its SSH daemon listens on a local Unix socket that only the sandbox's own supervisor process can reach.
|
|
|
|
| Aspect | Detail |
|
|
|---|---|
|
|
| Default | Session tokens expire after 24 hours. Concurrent connections are limited to 10 per token and 20 per sandbox. |
|
|
| What you can change | Configure `ssh_session_ttl_secs`. Set to 0 for no expiry. |
|
|
| Risk if relaxed | Longer TTLs or no expiry increase the window for stolen token reuse. Higher connection limits increase the blast radius of a compromised token. |
|
|
| Recommendation | Keep the 24-hour default. Monitor connection counts through the TUI. |
|
|
|
|
## Common Mistakes
|
|
|
|
The following patterns weaken security without providing meaningful benefit.
|
|
|
|
| Mistake | Why it matters | What to do instead |
|
|
|---------|---------------|-------------------|
|
|
| Omitting an inspected protocol on REST or WebSocket API endpoints | Without `protocol: rest` or `protocol: websocket`, the proxy uses L4-only enforcement. It allows the TCP stream through after checking host, port, and binary, but cannot inspect individual HTTP requests or WebSocket text messages. | Add `protocol: rest` or `protocol: websocket` with specific `rules` to enable method and path control. |
|
|
| Using `access: full` when finer rules would suffice | `access: full` with `protocol: rest` or `protocol: websocket` enables inspection but allows all methods and paths for that protocol. | Use `access: read-only` or explicit `rules` to restrict what the agent can do at the L7 level. |
|
|
| Adding endpoints permanently when operator approval would suffice | Adding endpoints to the policy YAML makes them permanently reachable across all instances. | Use operator approval. Approved endpoints persist within the sandbox instance but reset on re-creation. |
|
|
| Using broad binary globs | A glob like `/**` allows any binary to reach the endpoint, defeating binary-scoped enforcement. | Scope globs to specific directories (for example, `/sandbox/.vscode-server/**`). |
|
|
| Skipping TLS termination on HTTPS APIs | Setting `tls: skip` disables credential injection and L7 inspection. | Use the default auto-detect behavior unless the upstream requires client-certificate mTLS. |
|
|
| Setting `enforcement: enforce` before auditing | Jumping to `enforce` without first running in `audit` mode risks breaking the agent's workflow. | Start with `audit`, review the logs, and switch to `enforce` after you validate the rules. |
|
|
|
|
## Related Topics
|
|
|
|
- [Policies](/sandboxes/policies) for applying and iterating on sandbox policies.
|
|
- [Policy Schema](/reference/policy-schema) for the full field-by-field YAML reference.
|
|
- [Default Policy](/reference/default-policy) for the built-in default policy breakdown.
|
|
- [Gateway Auth](/reference/gateway-auth) for gateway authentication details.
|
|
- [How OpenShell Works](/about/how-it-works) for the system architecture.
|
|
- NemoClaw [Security Best Practices](https://docs.nvidia.com/nemoclaw/latest/security/best-practices.html) for entrypoint-level controls (capability drops, PATH hardening, build toolchain removal), policy presets, provider trust tiers, and posture profiles.
|