Gateway Configuration File

View as Markdown

The OpenShell gateway reads its configuration from a TOML file when --config or OPENSHELL_GATEWAY_CONFIG is set. When neither is set, the gateway reads $XDG_CONFIG_HOME/openshell/gateway.toml if that file exists. If no config file exists, the gateway starts from built-in defaults. Gateway process flags and gateway OPENSHELL_* environment variables override the file. Compute driver settings live in the driver TOML tables. See RFC 0003 for the full schema.

Source Precedence

Gateway CLI flag > gateway OPENSHELL_* env var > TOML file > built-in default

database_url is env-only. The loader rejects it when it appears in the file. When OPENSHELL_DB_URL is unset, the gateway stores its SQLite database under $XDG_STATE_HOME/openshell/gateway/openshell.db.

Package-Managed Locations

Package-managed gateways do not require a TOML file. Create one at the package’s optional config location when you need to override built-in defaults. Set OPENSHELL_GATEWAY_CONFIG in the launch environment to use a different file.

PackageOptional Gateway TOML location
Homebrew$XDG_CONFIG_HOME/openshell/gateway.toml when it exists, otherwise the Homebrew prefix config such as /opt/homebrew/var/openshell/gateway.toml.
Debian/Ubuntu$XDG_CONFIG_HOME/openshell/gateway.toml, usually ~/.config/openshell/gateway.toml for the systemd user service.
Fedora/RHEL RPM$XDG_CONFIG_HOME/openshell/gateway.toml, usually ~/.config/openshell/gateway.toml for the systemd user service.
Snap$SNAP_COMMON/gateway.toml, usually /var/snap/openshell/common/gateway.toml.

The Fedora/RHEL RPM template leaves [openshell.gateway].bind_address unset. The gateway therefore uses its built-in 127.0.0.1:17670 primary listener. The Podman driver negotiates separate, restricted listeners for sandbox callbacks, so the primary listener does not need a wildcard address. Set bind_address explicitly only when clients must reach the primary multiplexed API through another interface.

The Homebrew formula creates its prefix config without setting bind_address, so the gateway uses its built-in 127.0.0.1:17670 primary listener. Docker Desktop and Podman Machine reuse that listener for sandbox callbacks. A user config takes precedence. Upgrades preserve user-edited configs and migrate only an unchanged prefix config generated with the affected IPv6-loopback default.

Layout

The file is rooted at [openshell]. Gateway-wide settings live under [openshell.gateway]. Each compute driver owns its own [openshell.drivers.<name>] table. Credential drivers own [openshell.credential_drivers.<name>] tables. Shared compute-driver keys set at gateway scope are inherited into compute driver tables when not overridden.

1[openshell]
2version = 1
3
4[openshell.gateway]
5# ... gateway-wide settings ...
6
7[openshell.gateway.tls]
8# ... gateway listener TLS ...
9
10[openshell.gateway.oidc]
11# ... JWT bearer auth ...
12
13[openshell.drivers.kubernetes]
14# ... driver-specific settings ...
15
16[openshell.credential_drivers.kubernetes-secrets]
17# ... credential-driver-specific settings ...

Full Example

A complete gateway configuration covering every section. Trim to the fields you need.

1# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2# SPDX-License-Identifier: Apache-2.0
3
4[openshell]
5version = 1
6
7[openshell.gateway]
8bind_address = "0.0.0.0:8080"
9health_bind_address = "0.0.0.0:8081"
10metrics_bind_address = "0.0.0.0:9090"
11
12log_level = "info"
13
14# When empty, the gateway auto-detects Kubernetes, then Podman, then Docker.
15# VM is never auto-detected and requires an explicit entry here.
16compute_drivers = ["kubernetes"]
17
18# Optional external provider credential storage backend. Omit this key to use
19# the gateway's default encrypted database credential storage.
20credential_drivers = ["kubernetes-secrets"]
21
22sandbox_namespace = "openshell"
23ssh_session_ttl_secs = 3600
24
25# Reject invalid policy generations securely by default. Set
26# "retain_last_valid" only when availability takes priority.
27policy_validation_failure_mode = "fail_closed"
28
29# Subject Alternative Names baked into the gateway server certificate.
30# Wildcard DNS SANs (e.g. "*.dev.openshell.localhost") also enable sandbox
31# service URLs under that domain.
32server_sans = ["openshell", "*.dev.openshell.localhost"]
33# Allow plaintext HTTP routing for loopback sandbox service URLs.
34enable_loopback_service_http = true
35
36# Set true only for local plaintext gateways or trusted TLS termination.
37disable_tls = false
38
39# Shared driver defaults. These inherit into [openshell.drivers.<name>] tables
40# when the driver-specific table does not override them.
41default_image = "ghcr.io/nvidia/openshell/sandbox:latest"
42# Defaults to the gateway version; override to pin a specific build.
43# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
44client_tls_secret_name = "openshell-client-tls"
45service_account_name = "openshell-sandbox"
46host_gateway_ip = "10.0.0.1"
47enable_user_namespaces = false
48sa_token_ttl_secs = 3600
49guest_tls_ca = "/etc/openshell/certs/ca.pem"
50guest_tls_cert = "/etc/openshell/certs/client.pem"
51guest_tls_key = "/etc/openshell/certs/client-key.pem"
52
53# Optional gRPC rate limit. Both values must be positive to enable the limit.
54# Set either value to 0, or omit both, to disable rate limiting.
55grpc_rate_limit_requests = 120
56grpc_rate_limit_window_seconds = 60
57
58# Optional exact provider-profile source composition. When omitted, the
59# gateway uses builtin + user.
60provider_profile_sources = [
61 { type = "builtin" },
62 { type = "user" },
63]
64
65# Operator-run supervisor middleware. The gRPC endpoint must be reachable from
66# both the gateway and sandbox supervisors.
67[[openshell.supervisor.middleware]]
68name = "local-content-guard"
69grpc_endpoint = "https://host.openshell.internal:50051"
70tls_ca_cert_path = "/etc/openshell/certs/content-guard-ca.pem"
71audience = "urn:openshell:middleware:local-content-guard"
72max_payload_bytes = 262144
73timeout = "500ms"
74
75# Gateway listener TLS (distinct from the per-driver guest_tls_*).
76[openshell.gateway.tls]
77cert_path = "/etc/openshell/certs/gateway.pem"
78key_path = "/etc/openshell/certs/gateway-key.pem"
79client_ca_path = "/etc/openshell/certs/client-ca.pem"
80require_client_auth = false
81# Optional: SNI-based dual certificate for external (e.g. ACME) TLS.
82# external_cert_path = "/etc/openshell/certs/external.pem"
83# external_key_path = "/etc/openshell/certs/external-key.pem"
84# external_server_names = ["gateway.example.com"]
85
86[openshell.gateway.gateway_jwt]
87signing_key_path = "/etc/openshell/jwt/signing.pem"
88public_key_path = "/etc/openshell/jwt/public.pem"
89kid_path = "/etc/openshell/jwt/kid"
90gateway_id = "openshell"
91# Omit or set to 0 only for local single-player Docker, Podman, or VM gateways.
92ttl_secs = 3600
93
94[openshell.gateway.auth]
95allow_unauthenticated_users = false
96
97[openshell.gateway.mtls_auth]
98enabled = false
99
100# OTLP export. Omit this table entirely to disable it.
101[openshell.gateway.otlp]
102endpoint = "http://otel-collector.observability.svc:4317"
103service_name = "openshell-gateway"
104
105[openshell.gateway.oidc]
106issuer = "https://idp.example.com/realms/openshell"
107audience = "openshell-cli"
108jwks_ttl_secs = 3600
109roles_claim = "realm_access.roles"
110admin_role = "openshell-admin"
111user_role = "openshell-user"
112scopes_claim = ""
113
114[[openshell.gateway.interceptors]]
115name = "quota"
116grpc_endpoint = "unix:///run/openshell/interceptors/quota.sock"
117audience = "urn:openshell:interceptor:quota"
118order = 10
119failure_policy = "fail_closed"
120binding_policy = "allowlist"
121timeout = "500ms"
122max_response_bytes = 1048576
123max_patches = 32
124
125[[openshell.gateway.interceptors.bindings]]
126rpc = "openshell.v1.OpenShell/CreateSandbox"
127phases = ["modify_operation", "validate"]
128failure_policy = "fail_closed"
129
130[[openshell.gateway.interceptors.bindings]]
131rpc = "openshell.v1.OpenShell/UpdateConfig"
132phases = ["validate"]
133
134[openshell.credential_drivers.kubernetes-secrets]
135namespace = "openshell"
136allow_reference_namespace = false

Local Docker, Podman, and VM gateways can also set [openshell.gateway.mtls_auth] enabled = true to authenticate CLI callers from verified client certificates. Kubernetes deployments must leave this unset and use OIDC or a trusted access proxy; the Helm chart does not render this table.

[openshell.gateway.tls] supports optional SNI-based dual-certificate mode for deployments that need separate internal and external server certificates. Set external_cert_path and external_key_path to point at the external (e.g. ACME/publicly-trusted) certificate and key. List the hostnames that should be served with the external certificate in external_server_names. Connections whose TLS SNI hostname matches one of those names receive the external certificate; all other connections (including those with no SNI) receive the primary internal certificate from cert_path/key_path. Both fields must be set together — providing only one is a configuration error. On Kubernetes with the Helm chart, the external certificate is managed automatically when certManager.serverIssuerRef.name is set; the chart populates these fields from the cert-manager-issued external server certificate.

[openshell.gateway] policy_validation_failure_mode controls what sandbox supervisors do when a complete candidate policy fails runtime validation. The default, fail_closed, deactivates the previous network policy, closes relays pinned to it, and denies new egress until a valid generation loads. retain_last_valid leaves the previous valid generation active. Both modes reject the candidate atomically; startup always fails closed when no previous valid generation exists. Gateway mutation paths that can preflight a known effective scope reject invalid candidates before persistence and leave the active policy unchanged regardless of this setting. Changing the value requires restarting the gateway so it can reload gateway.toml and distribute the new posture to sandbox supervisors.

[openshell.gateway.gateway_jwt] ttl_secs controls gateway-minted sandbox JWT lifetime. When omitted, it defaults to 0: the token exp claim and expires_at_ms response field become 0, and the sandbox JWT does not expire. Use that default only for local single-player Docker, Podman, or VM gateways. Kubernetes and other shared deployments should set a positive TTL; Helm renders 3600 seconds by default, and the gateway logs a warning when a Kubernetes gateway uses 0.

[openshell.gateway.auth] allow_unauthenticated_users = true is an unsafe local-development and trusted-proxy escape hatch. It accepts user-facing CLI/API calls without OIDC or mTLS credentials while sandbox supervisors still authenticate with gateway-minted sandbox JWTs. Leave it false for shared and production gateways.

OTLP Export

[openshell.gateway.otlp] enables OpenTelemetry export over OTLP/gRPC. Omit the table to disable export; there is no separate enabled flag.

The gateway already uses the Rust tracing framework for structured logs sent to stdout and the sandbox log stream. Enabling this section adds an OpenTelemetry layer to the same tracing subscriber. It exports span trees to an OTLP collector without exporting, replacing, or redirecting the existing log events.

1[openshell.gateway.otlp]
2endpoint = "http://otel-collector.observability.svc:4317"
3service_name = "openshell-gateway"

endpoint is required and must be a valid URI. If it is malformed, the gateway logs the configuration error and continues with export disabled. It does not connect at startup: an unreachable collector produces export failures, never a failure to serve.

The transport is OTLP over gRPC only. HTTP/protobuf and HTTP/JSON are not supported, and OTEL_EXPORTER_OTLP_PROTOCOL has no effect. Point endpoint at a collector’s gRPC receiver, conventionally port 4317, not the HTTP receiver on 4318. A URI alone cannot distinguish the two, so an HTTP endpoint is accepted at startup and then fails on export.

The OpenTelemetry SDK logs export failures after startup. Spans in a failed batch are dropped rather than retried.

service_name sets the gateway’s service.name resource attribute and defaults to openshell-gateway. The gateway also reports service.version.

Only OpenTelemetry traces are exported. Inbound gRPC and HTTP requests produce server spans named for the RPC or HTTP method. Store and compute-driver operations appear as child spans. Internal reconciliation, credential-refresh, and driver-watch loops create operation roots for their store work because no inbound request supplies a parent. The gateway continues valid W3C traceparent context and starts a new trace when none is supplied. Request spans carry method, path, and the request_id that also appears in gateway logs. Health endpoint spans use DEBUG level and are not exported by the default INFO filter.

The gateway forwards the OTLP configuration to managed external drivers. Each driver exports under its own service name.

Tuning

This table decides whether and where to export. How the SDK exports is controlled by the standard OpenTelemetry environment variables, which the gateway reads through the SDK rather than mirroring as TOML keys:

VariableEffect
OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARGSampling strategy and ratio. Defaults to parentbased_always_on.
OTEL_BSP_SCHEDULE_DELAY, OTEL_BSP_MAX_QUEUE_SIZE, OTEL_BSP_MAX_EXPORT_BATCH_SIZE, OTEL_BSP_EXPORT_TIMEOUTBatch span processor tuning.
OTEL_RESOURCE_ATTRIBUTESAdditional resource attributes, such as deployment.environment=prod.
OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT, OTEL_SPAN_EVENT_COUNT_LIMIT, OTEL_SPAN_LINK_COUNT_LIMITPer-span limits.
OTEL_EXPORTER_OTLP_HEADERS, OTEL_EXPORTER_OTLP_COMPRESSION, OTEL_EXPORTER_OTLP_TIMEOUTExporter transport tuning.
OTEL_EXPORTER_OTLP_PROTOCOLNo effect. The gateway is built with the gRPC exporter only.

To sample 10% of traces:

$OTEL_TRACES_SAMPLER=parentbased_traceidratio OTEL_TRACES_SAMPLER_ARG=0.1

OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_TRACES_ENDPOINT are deliberately ignored. Enablement has one source, so an environment variable cannot silently turn export on or redirect it. OTEL_RESOURCE_ATTRIBUTES does apply and adds attributes, but a service_name set here wins over OTEL_SERVICE_NAME.

The gateway flushes buffered spans during shutdown, so spans from in-flight requests survive a SIGTERM.

Supervisor Middleware Services

Register operator-run supervisor middleware services with one or more [[openshell.supervisor.middleware]] entries. Registration is static and operator-owned; changing it requires restarting the gateway.

1[[openshell.supervisor.middleware]]
2name = "local-content-guard"
3grpc_endpoint = "https://host.openshell.internal:50051"
4tls_ca_cert_path = "/etc/openshell/certs/content-guard-ca.pem"
5audience = "urn:openshell:middleware:local-content-guard"
6max_payload_bytes = 262144
7timeout = "500ms"

Each service implements the supervisor middleware gRPC contract and exposes bindings through Describe. Policies reference the operator-owned registration name, attaching the complete middleware and all of its bindings. Bindings are identified by operation and phase. A manifest may expose at most one binding for each operation and phase pair. V1 supports HttpRequest/pre_credentials and WebSocketMessage/pre_credentials, so a service can inspect HTTP, WebSocket, or both. Registration names must be unique, and operator-run registrations cannot claim the reserved openshell/ namespace. The service-reported manifest name is diagnostic metadata and does not need to match the registration name.

The gateway connects to every registered service and validates Describe before it starts. The service must therefore be running before the gateway. Policy creation and full policy updates call ValidateConfig; an unavailable service or invalid middleware configuration rejects the policy before persistence.

max_payload_bytes is the shared operator limit for inspectable logical payloads across every binding exposed by the service. It caps HTTP request and replacement bodies as well as complete WebSocket text messages and replacements. The value must be greater than zero, no larger than each binding’s advertised max_payload_bytes capability, and no larger than the 4 MiB platform maximum. OpenShell rejects oversized values instead of silently clamping them. Binary WebSocket messages are not exposed to V1 middleware, so this field does not limit binary pass-through. Middleware gRPC servers should allow messages of at least 4 MiB plus 293 KiB so a maximum-size payload and its protobuf envelope fit on the transport.

timeout is the operator-configured service-wide RPC timeout. It accepts the same compact duration syntax as gateway interceptors: an integer followed by ms or s, such as 500ms or 2s. Values must be between 10ms and 30s, inclusive. Omit the field to use the 500 ms platform default. A binding may advertise a shorter timeout in the Describe manifest, but it cannot extend the operator-configured deadline; OpenShell uses the smaller value. OpenShell validates both levels before accepting the service. The operator-configured service timeout applies to Describe and ValidateConfig. The effective binding timeout applies only to EvaluateHttpRequest, WebSocket preflight, and each WebSocket message. An accepted WebSocket stream has no connection-wide RPC deadline.

The service grpc_endpoint supports plaintext http:// and TLS https://. HTTPS uses the platform trust store unless tls_ca_cert_path names a certificate-only PEM bundle. OpenShell rejects bundles containing private keys, loads the certificates at gateway startup, and distributes only public certificates to sandbox supervisors; normal TLS hostname verification still applies. audience sets the exact audience for gateway-minted service tokens and defaults to urn:openshell:extension:middleware:<name>. After authenticated Describe succeeds, OpenShell treats a non-empty manifest expected_audience as a consistency assertion and refuses to start when it differs from the configured audience. A strict verifier may reject an incorrect audience before returning the manifest.

When gateway_jwt is configured, OpenShell attaches short-lived bearer credentials to gateway and supervisor calls and requires https://. A middleware endpoint must be reachable from sandbox supervisors, so Unix sockets are not an option here. Set allow_insecure_transport = true on a registration to keep a plaintext http:// endpoint: OpenShell then attaches no credential, supervisors do not request one, and the gateway logs a warning naming the registration at every startup. mTLS client authentication, health checks, and runtime registration are not currently supported. The endpoint must be reachable from both the gateway and sandbox supervisors; use host.openshell.internal or another shared address that can be resolved in both places.

See Supervisor Middleware for selection, failure, payload-limit, and operational guidance.

Gateway Interceptors

[[openshell.gateway.interceptors]] configures gateway-side interceptor services. The gateway calls each service’s Describe RPC at startup, validates its declared OpenShell RPC bindings against the compiled service descriptor, and applies matching phases from a central gRPC middleware path. Interceptors can target only methods in the gateway’s built-in allowlist of unary mutation RPCs. New RPCs are non-interceptable until they are deliberately added to that allowlist; adding one does not require handler-specific interceptor code. Request bodies are exposed as protobuf JSON objects. Fields marked secret in the protobuf schema are recursively omitted from requests and post-commit responses. Interceptors cannot patch an omitted field or a containing object.

HTTPS interceptor endpoints use the platform trust store by default. Set tls_ca_cert_path to a PEM certificate bundle for a private CA; normal TLS hostname verification still applies. audience sets the exact audience for gateway-minted service tokens and defaults to urn:openshell:extension:interceptor:<name>. After authenticated Describe succeeds, the gateway treats a non-empty manifest expected_audience as a consistency assertion and refuses to start when it differs from the configured audience. A strict verifier may reject an incorrect audience before returning the manifest. When gateway_jwt is configured, network interceptors must use HTTPS and receive short-lived gateway-caller bearer credentials; local Unix sockets are also supported. Set allow_insecure_transport = true to keep a plaintext http:// interceptor endpoint with no credential attached and a startup warning.

Extension Token Verification Endpoints

When gateway_jwt is configured the gateway publishes two unauthenticated documents that extension services use to verify OpenShell callers:

PathContents
/.well-known/jwks.jsonSingle-key JWKS holding the Ed25519 public key and its kid.
/.well-known/openid-configurationOIDC-shaped discovery metadata: the exact expected iss, an absolute jwks_uri, and EdDSA as the only supported signing algorithm.

The discovery document is OIDC-shaped rather than OIDC-compliant: issuer is the gateway identity (openshell-gateway:<gateway_id>), not the URL the document is served from. Verifiers compare iss against that value and must not infer trust from the document’s location; the serving TLS connection is what authenticates the gateway. Both endpoints return 404 when gateway_jwt is not configured.

binding_policy controls how the manifest and operator binding configuration combine:

  • dynamic enables valid manifest bindings and treats configured entries as optional narrowing overrides. This is the compatibility default. The gateway logs a startup warning because the interceptor controls its non-secret RPC authority.
  • allowlist enables only configured RPC selectors and phases. The gateway ignores and logs extra manifest declarations, but fails startup when the manifest omits a configured RPC or phase.
  • exact requires the configured and manifest RPC selectors and phases to match exactly.

Bindings under allowlist and exact require rpc or service plus method, and a nonempty phases list. They match by RPC rather than manifest binding ID. Duplicate selectors, id, and disabled are invalid in these modes; omit a binding to disable it. Binding failure policy comes from the binding, then the configured service, then defaults to fail_closed. Manifest failure policies do not override strict-mode operator configuration.

Bindings that include post_commit must resolve to failure_policy = "fail_open"; the gateway rejects fail-closed post-commit configuration at startup. Post-commit evaluation is observational. If response observation or interceptor evaluation fails after a handler succeeds, the gateway logs and counts the failure but returns the committed response unchanged.

provider_profile_sources selects the exact ordered provider-profile source set. Omit it to use the built-in and user-managed sources. Use { type = "builtin" }, { type = "user" }, and { type = "interceptor", name = "<configured-interceptor-name>" } entries to compose a catalog. An interceptor entry must reference a configured service whose manifest advertises provider_profiles = true. Selecting only an interceptor makes that catalog authoritative; include local sources explicitly to compose them. Empty or duplicate source lists, unknown interceptor names, and duplicate normalized profile IDs fail closed. Source order controls collection and diagnostics, not override precedence.

For an authoritative interceptor catalog, select only that interceptor:

1[openshell.gateway]
2provider_profile_sources = [
3 { type = "interceptor", name = "provider-governance" },
4]

The gateway validates snapshot structure and provider-profile semantics. It treats a configured interceptor as the source trust boundary and does not verify signature, hash, or key annotations in profile payloads.

failure_policy accepts fail_closed or fail_open. timeout accepts ms and s suffixes. In dynamic mode, binding overrides may select a manifest binding by id, rpc, or service plus method; they can disable a binding, narrow its phases, or override its failure policy.

image_pull_policy is intentionally not a shared gateway key. Kubernetes and Docker use Always, IfNotPresent, or Never. Podman uses always, missing, never, or newer. Set it inside the relevant driver table.

Credential Drivers

Set credential_drivers only when the gateway should store provider secrets in an external credential backend. Provider secrets include injectable credentials and gateway-only refresh material such as OAuth refresh tokens, client secrets, and service-account private keys. OpenShell supports at most one enabled credential driver at a time. When credential_drivers is omitted, the gateway uses its default encrypted database credential storage. credential_drivers = [] is invalid in the TOML file; omit the field for the default encrypted store, or select a backend such as kubernetes-secrets or vault.

Credential driver tables are backend-owned and live under [openshell.credential_drivers.<name>]. Built-in drivers default to in-tree transport, so they do not need a transport field. Use transport = "uds" with an absolute socket_path only for a remote gRPC driver over a Unix domain socket.

1[openshell.gateway.credential_storage]
2key_encryption_key_path = "/var/lib/openshell/credentials/key-encryption-key.bin"

For Kubernetes Secrets:

1[openshell.gateway]
2credential_drivers = ["kubernetes-secrets"]
3
4[openshell.credential_drivers.kubernetes-secrets]
5namespace = "openshell"

For Vault instead:

1[openshell.gateway]
2credential_drivers = ["vault"]
3
4[openshell.credential_drivers.vault]
5address = "http://vault.vault.svc.cluster.local:8200"
6mount = "secret"
7kv_version = "2"
8auth_method = "kubernetes"
9role = "openshell-gateway"
10service_account_token_path = "/var/run/secrets/kubernetes.io/serviceaccount/token"

For the default encrypted database store, OpenShell stores provider credentials as JSON envelopes encrypted with AES-256-GCM in the gateway database. Each credential gets a random data-encryption key; the gateway wraps that key with a local key-encryption key. By default, the key-encryption key is created at $XDG_STATE_HOME/openshell/gateway/credentials/key-encryption-key.bin with owner-only permissions. Use [openshell.gateway.credential_storage] key_encryption_key_env instead of key_encryption_key_path to load a base64-encoded 32-byte key-encryption key from an environment variable. Back up the database and key-encryption key together; losing either makes stored credentials unrecoverable. In Kubernetes, the Helm chart creates a retained Secret containing the shared key-encryption key, injects it as OPENSHELL_GATEWAY_CREDENTIAL_KEY_ENCRYPTION_KEY, and renders key_encryption_key_env for the gateway when no external credential driver is enabled. Multi-replica deployments need every replica to use the same database and key-encryption key; the chart default handles the key-encryption key side.

For GitOps and helm template workflows where lookup returns empty, the chart-generated KEK Secret gets a random value on every render, making credentials unrecoverable. Set server.credentialStorage.existingSecret to the name of a pre-provisioned Secret containing the key-encryption key under the key-encryption-key data key. When set, the chart skips KEK Secret generation and references the provided Secret directly.

1server:
2 credentialStorage:
3 existingSecret: my-preprovisioned-kek-secret

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.

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.

For remote credential drivers, set transport = "uds" with socket_path. Omit command, args, and startup_timeout_secs when another service manager prestarts the driver socket. Keep backend tokens out of TOML; point the driver at mounted token files or native identity mechanisms instead.

The built-in kubernetes-secrets and vault drivers can also run out of process over UDS. Set command to the standalone driver binary and pass driver-specific settings through args; the gateway appends --bind-socket <socket_path> when it launches the process.

1[openshell.gateway]
2credential_drivers = ["kubernetes-secrets"]
3
4[openshell.credential_drivers.kubernetes-secrets]
5transport = "uds"
6socket_path = "/run/openshell/credential-drivers/kubernetes-secrets.sock"
7command = "/usr/libexec/openshell/openshell-driver-kubernetes-secrets"
8args = ["--namespace", "openshell"]
1[openshell.gateway]
2credential_drivers = ["vault"]
3
4[openshell.credential_drivers.vault]
5transport = "uds"
6socket_path = "/run/openshell/credential-drivers/vault.sock"
7command = "/usr/libexec/openshell/openshell-driver-vault"
8args = [
9 "--address", "http://vault.vault.svc.cluster.local:8200",
10 "--auth-method", "kubernetes",
11 "--role", "openshell-gateway",
12]

Driver References

Each example is a complete TOML file for one compute driver. The examples repeat [openshell] and [openshell.gateway] so they stay copyable, and the driver tables list the accepted driver-specific keys. Driver-specific values override inherited gateway defaults. The gateway rejects unknown driver fields after inheritance is merged.

Kubernetes

The gateway runs as a Pod and creates sandbox Pods in another namespace. mTLS material for sandboxes is delivered through a Kubernetes Secret rather than host-side file paths.

1[openshell]
2version = 1
3
4[openshell.gateway]
5bind_address = "0.0.0.0:8080"
6health_bind_address = "0.0.0.0:8081"
7metrics_bind_address = "0.0.0.0:9090"
8log_level = "info"
9compute_drivers = ["kubernetes"]
10
11[openshell.gateway.tls]
12cert_path = "/etc/openshell-tls/server/tls.crt"
13key_path = "/etc/openshell-tls/server/tls.key"
14client_ca_path = "/etc/openshell-tls/client-ca/ca.crt"
15# When cert-manager serverIssuerRef is configured, these are populated by Helm:
16# external_cert_path = "/etc/openshell-tls/server-external/tls.crt"
17# external_key_path = "/etc/openshell-tls/server-external/tls.key"
18# external_server_names = ["gateway.example.com"]
19
20[openshell.drivers.kubernetes]
21# Workspace isolation mode. "shared" renders all sandboxes into a single
22# namespace. "managed" auto-creates a K8s namespace per workspace
23# (openshell-{gateway_id}-{workspace}). "operator" maps each workspace to a
24# pre-provisioned namespace discovered via label selector or drop-in file.
25workspace_mode = "shared"
26# Gateway identity used in managed-mode namespace naming. Defaults to the
27# gateway JWT gateway_id. Must be a DNS-1123 label.
28# gateway_id = "openshell"
29namespace = "agents"
30service_account_name = "openshell-sandbox"
31default_image = "ghcr.io/nvidia/openshell/sandbox:latest"
32image_pull_policy = "IfNotPresent"
33image_pull_secrets = ["regcred"]
34# Defaults to the gateway version; override to pin a specific build.
35# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
36supervisor_image_pull_policy = "IfNotPresent"
37
38# Use the image volume on Kubernetes >= 1.35 (GA in 1.36); switch to "init-container"
39# on older clusters or where the ImageVolume feature gate is off.
40supervisor_sideload_method = "image-volume"
41# "combined" runs the existing single supervisor container with full process,
42# filesystem, and network enforcement in the agent container. "sidecar" moves
43# pod-level network enforcement and gateway session handling into a network sidecar.
44topology = "combined"
45# Optional corporate HTTP forward proxy for policy-approved TLS egress. The
46# sandbox workload cannot select or override these settings. Only http:// proxy
47# endpoints and TLS CONNECT traffic are supported; plain HTTP egress remains
48# direct. `no_proxy` bypasses only the corporate proxy, never OpenShell policy.
49# https_proxy = "http://proxy.corp.example:8080"
50# no_proxy = ".svc,.svc.cluster.local,10.96.0.0/12,10.244.0.0/16"
51# Proxy credentials must be an existing Secret in the sandbox namespace. The
52# key contains a `user:pass` value and is mounted only in the network
53# supervisor container, never in workload environment or command arguments.
54# proxy_auth_secret_name = "corporate-proxy-auth"
55# proxy_auth_secret_key = "credentials"
56# The gateway validates the Secret name/key syntax and their configuration
57# relationship at startup; it does not read the Secret from the Kubernetes API.
58# Kubernetes resolves the Secret when the Sandbox Pod starts. A missing key or
59# Secret prevents that Pod from starting; unreadable or malformed `user:pass`
60# content is validated fail-closed by the supervisor at startup and never
61# falls back to direct egress.
62# Proxy credential Secrets require `topology = "sidecar"`. Combined topology
63# shares its credential mount with the workload and can make it readable by the
64# sandbox group through Kubernetes `fsGroup` volume permission handling.
65# Required with a credential Secret: Basic authentication to an http:// proxy
66# is cleartext on the connection to that proxy.
67# proxy_auth_allow_insecure = true
68# Last resort for hostname-filtering proxy ACLs. The proxy resolves the target,
69# so its ACL becomes part of the egress boundary for proxied connections.
70# proxy_connect_by_hostname = true
71grpc_endpoint = "https://openshell-gateway.agents.svc:8080"
72ssh_socket_path = "/run/openshell/ssh.sock"
73client_tls_secret_name = "openshell-client-tls"
74host_gateway_ip = "10.0.0.1"
75enable_user_namespaces = false
76app_armor_profile = "Unconfined"
77workspace_default_storage_size = "10Gi"
78# Kubernetes StorageClass for the workspace PVC. Empty (default) omits the
79# field, using the cluster's default StorageClass. Set this on clusters with no
80# default StorageClass, otherwise the workspace PVC stays Pending.
81# workspace_storage_class = "fast-ssd"
82# Kubernetes RuntimeClass applied to sandbox pods when the API request does
83# not specify one. Empty (default) = omit the field, using the cluster default.
84# default_runtime_class_name = "kata-containers"
85# Kubelet clamps projected tokens below 600 seconds. The driver caps values at 86400.
86sa_token_ttl_secs = 3600
87# Optional SPIFFE Workload API socket mounted into sandbox pods for dynamic
88# provider token grants. Use an absolute path under a dedicated directory;
89# shared roots such as /run, /var, /tmp, and /etc are rejected.
90# Supervisor-to-gateway auth still uses gateway JWTs.
91provider_spiffe_workload_api_socket_path = "/spiffe-workload-api/spire-agent.sock"
92# Explicit sandbox UID/GID for the supervisor container securityContext and
93# PVC init container. When unset, the driver auto-detects from OpenShift SCC
94# namespace annotations (openshift.io/sa.scc.uid-range) if present, falling
95# back to 1000 on non-OpenShift clusters. Any non-root Linux UID/GID is valid.
96# sandbox_uid = 1500
97# sandbox_gid = 1500
98# Operator-mode namespace discovery. At least one must be set when
99# workspace_mode = "operator". Both can be combined.
100# operator_namespace_label discovers namespaces matching a K8s label selector.
101# operator_namespace_label = "openshell.ai/workspace=true"
102# operator_namespace_file reads allowed namespaces from a JSON/YAML file
103# (hot-reloaded on change, e.g. via ConfigMap volume mount).
104# operator_namespace_file = "/etc/openshell/workspace-namespaces.json"
105
106[openshell.drivers.kubernetes.managed_ssh_ingress]
107enabled = true
108gateway_namespace = "openshell"
109gateway_pod_selector = { "app.kubernetes.io/name" = "openshell", "app.kubernetes.io/instance" = "openshell" }
110
111[openshell.drivers.kubernetes.sidecar]
112# UID used by relaxed long-running network sidecars. Strict process/binary-aware
113# sidecars run as UID 0 so Kubernetes grants the required /proc inspection
114# capabilities into the effective set. In sidecar topology the network init
115# container installs nftables rules that exempt the effective sidecar UID, so
116# this dedicated infrastructure UID must remain at least 1000 and must not
117# match the sandbox workload UID.
118proxy_uid = 1337
119# Keep process/binary-aware network policy enabled in sidecar topology. Set
120# false to run the sidecar as proxy_uid, drop the sidecar's extra /proc
121# inspection capabilities, and enforce endpoint/L7 policy without matching
122# policy.binaries.
123process_binary_aware_network_policy = true

In managed workspace mode, the Kubernetes driver copies each explicitly named image_pull_secrets Secret from namespace into the managed workspace namespace on sandbox creation. Shared and operator modes require the Secret to already exist in the sandbox namespace.

Docker

Sandboxes run as containers on a local bridge network. The supervisor binary is bind-mounted from the host (no in-cluster image pull required); guest mTLS material is supplied as host paths.

1[openshell]
2version = 1
3
4[openshell.gateway]
5bind_address = "127.0.0.1:17670"
6log_level = "info"
7compute_drivers = ["docker"]
8
9[openshell.drivers.docker]
10socket_path = "/var/run/docker.sock"
11default_image = "ghcr.io/nvidia/openshell/sandbox:latest"
12# Docker vocabulary: Always | IfNotPresent | Never. Empty behaves like IfNotPresent.
13image_pull_policy = "IfNotPresent"
14sandbox_namespace = "docker-dev"
15# Empty auto-detects https://host.openshell.internal:<gateway-port> when guest TLS is set.
16grpc_endpoint = "https://host.openshell.internal:17670"
17# Skip the image-pull-and-extract step by pointing at a locally built binary.
18supervisor_bin = "/usr/local/libexec/openshell/openshell-sandbox"
19# When supervisor_bin is omitted, Docker extracts /openshell-sandbox from this image.
20# Defaults to the gateway version; override to pin a specific build.
21# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
22guest_tls_ca = "/etc/openshell/certs/ca.pem"
23guest_tls_cert = "/etc/openshell/certs/client.pem"
24guest_tls_key = "/etc/openshell/certs/client-key.pem"
25network_name = "openshell-docker"
26host_gateway_ip = "172.17.0.1"
27ssh_socket_path = "/run/openshell/ssh.sock"
28# Unsafe operator override. Host bind mounts, including Docker local-driver
29# bind-backed volumes, expose gateway-host paths inside sandboxes and can
30# negate OpenShell isolation and filesystem controls.
31enable_bind_mounts = false
32# Set to 0 to leave Docker's runtime default unchanged.
33sandbox_pids_limit = 2048

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; guest mTLS material is supplied as host paths.

1[openshell]
2version = 1
3
4[openshell.gateway]
5bind_address = "127.0.0.1:17670"
6log_level = "info"
7compute_drivers = ["podman"]
8
9[openshell.drivers.podman]
10# Rootless socket path. For root Podman use /run/podman/podman.sock.
11# Omit to auto-detect: the driver probes for a responsive Podman socket and
12# fails to start if none respond.
13socket_path = "/run/user/1000/podman/podman.sock"
14default_image = "ghcr.io/nvidia/openshell/sandbox:latest"
15image_pull_policy = "missing" # always | missing | never | newer
16grpc_endpoint = "https://host.containers.internal:17670"
17# The gateway overwrites gateway_port from bind_address at runtime.
18gateway_port = 17670
19network_name = "openshell"
20# Omit for the platform default: empty on Linux, 192.168.127.254 on macOS Podman machine.
21# Set "" to force Podman's host-gateway resolver.
22# host_gateway_ip = "192.168.127.254"
23sandbox_ssh_socket_path = "/run/openshell/ssh.sock"
24stop_timeout_secs = 45
25# Defaults to the gateway version; override to pin a specific build.
26# supervisor_image = "ghcr.io/nvidia/openshell/supervisor:<version>"
27guest_tls_ca = "/etc/openshell/certs/ca.pem"
28guest_tls_cert = "/etc/openshell/certs/client.pem"
29guest_tls_key = "/etc/openshell/certs/client-key.pem"
30# Unsafe operator override. Host bind mounts, including Podman local-driver
31# bind-backed volumes, expose gateway-host paths inside sandboxes and can
32# negate OpenShell isolation and filesystem controls.
33enable_bind_mounts = false
34# Set to 0 to leave Podman's runtime default unchanged.
35sandbox_pids_limit = 2048
36# Health check interval in seconds. Lower values detect readiness faster
37# but increase process churn (each check spawns a conmon subprocess).
38# Set to 0 to disable health checks entirely. Default: 10.
39health_check_interval_secs = 10
40# User namespace mode for sandbox containers. Omit to use the default.
41# Supported modes: auto, host, keep-id, no-map, private.
42# userns = "auto"
43# Explicit UID/GID mappings for userns = "private". Each entry is
44# "container_id:host_id:size". Required when mode is "private"; rejected
45# for other modes. Rootless Podman uses intermediate IDs (0:0:1, 1:1:65535);
46# rootful Podman uses absolute host IDs (0:1000:1, 1:100000:65536).
47# uidmap = ["0:0:1", "1:1:65535"]
48# gidmap = ["0:0:1", "1:1:65535"]
49# Corporate forward proxy for sandbox egress. When set, the in-container
50# supervisor chains policy-approved TLS tunnels through this proxy with HTTP
51# CONNECT instead of dialing destinations directly. Plain-HTTP requests are
52# not proxied and always dial the destination directly. Only http:// proxy
53# URLs in explicit http://host:port form are supported: the scheme and port
54# are both required, and a URL carrying a path, query, or fragment is
55# rejected rather than silently truncated.
56# NO_PROXY entries (hostnames, domain suffixes, IPs, CIDRs, each with an
57# optional :port qualifier) are dialed directly. A port-qualified entry only
58# bypasses that destination port. IP and CIDR entries also match hostnames
59# through their validated DNS resolution; such a match dials directly only
60# the resolved addresses inside the entry. This is an operator-owned egress
61# boundary:
62# sandbox and template environment cannot override it, and the conventional
63# HTTPS_PROXY/HTTP_PROXY/NO_PROXY variables a sandbox sets do not affect it.
64#
65# The CONNECT request sent to the proxy targets a validated resolved IP,
66# not the hostname, so the proxy performs no DNS resolution of its own and
67# the tunnel stays bound to the address that passed the sandbox's SSRF and
68# allowed_ips validation. The hostname still travels inside the tunnel (TLS
69# SNI, application Host), so destination servers behave normally. In
70# split-horizon networks, point the gateway host at the corporate resolver
71# so internal names validate to their internal addresses. If the proxy's
72# ACLs filter on hostnames and reject IP CONNECT targets, set
73# proxy_connect_by_hostname = true as a last resort: the proxy then
74# resolves the name itself, so a name that resolves differently at the
75# proxy (split-horizon DNS, rebinding) can reach destinations the sandbox
76# policy never approved, and the proxy's own ACLs become the effective
77# egress control for proxied TLS.
78#
79# Configuration is fail-closed: an invalid proxy URL is rejected at gateway
80# startup, setting no_proxy, proxy_auth_file, or proxy_connect_by_hostname
81# without a proxy URL is rejected as well, and a set-but-invalid value
82# reaching a sandbox (for example an unreadable or malformed auth file) is
83# fatal to that sandbox's supervisor instead of silently falling back to
84# direct or unauthenticated egress.
85#
86# Credentials must NOT be embedded in the URL (an inline user:pass@ is
87# rejected at startup, since it would be stored here and exposed in container
88# metadata). Instead point proxy_auth_file at a file containing "user:pass";
89# the gateway delivers it to the supervisor through a root-only secret mount.
90# The credential must use the user:pass form (non-empty user, no control
91# characters); the same validation runs at sandbox-create time and in the
92# supervisor, so a credential accepted here is never rejected in-container.
93# Keep gateway.toml and the auth file owner-readable only (mode 0600).
94#
95# WARNING: the supervisor sends the credential as a Proxy-Authorization:
96# Basic header over the plain-TCP connection to the http:// proxy. Basic
97# auth is base64, not encryption: anyone on the network path between the
98# sandbox host and the proxy can recover the credential. Because of that,
99# proxy_auth_file requires the explicit acknowledgement
100# proxy_auth_allow_insecure = true; without it the configuration is
101# rejected at gateway startup. Only opt in when the path to the proxy is a
102# trusted network segment.
103# https_proxy = "http://proxy.corp.com:8080"
104# no_proxy = "*.svc.cluster.local,10.0.0.0/8"
105# proxy_auth_file = "/etc/openshell/secrets/proxy-auth"
106# proxy_auth_allow_insecure = true
107# Last resort for hostname-filtering proxy ACLs; see the warning above.
108# proxy_connect_by_hostname = true

MicroVM

Each sandbox runs inside its own libkrun microVM managed by the standalone openshell-driver-vm subprocess. Use this driver when you want stronger isolation than container namespaces alone.

1[openshell]
2version = 1
3
4[openshell.gateway]
5bind_address = "127.0.0.1:17670"
6log_level = "info"
7# VM is never auto-detected; an explicit entry here is required.
8compute_drivers = ["vm"]
9
10[openshell.drivers.vm]
11state_dir = "/var/lib/openshell/vm"
12# Where the gateway looks for the openshell-driver-vm subprocess binary.
13driver_dir = "/usr/local/libexec/openshell"
14default_image = "ghcr.io/nvidia/openshell/sandbox:latest"
15grpc_endpoint = "https://host.containers.internal:17670"
16# Empty falls back to default_image.
17bootstrap_image = "ghcr.io/nvidia/openshell/sandbox:latest"
18krun_log_level = 1
19vcpus = 2
20mem_mib = 2048
21overlay_disk_mib = 4096
22guest_tls_ca = "/var/lib/openshell/guest-tls/ca.pem"
23guest_tls_cert = "/var/lib/openshell/guest-tls/client.pem"
24guest_tls_key = "/var/lib/openshell/guest-tls/client-key.pem"
25# Resolved sandbox UID/GID for the rootfs /etc/passwd entry.
26# Defaults to 10001 when unset; matching GID is used if sandbox_gid is empty.
27# Any non-root Linux UID/GID is valid.
28# sandbox_uid = 20001

Extension Driver

Extension drivers run outside the gateway and expose the compute_driver.proto gRPC service on a Unix socket. Use a non-reserved driver name; built-in names such as vm, docker, podman, and kubernetes cannot be selected through unmanaged socket endpoints. The selected driver name is the key used for driver-owned sandbox config such as template.driver_config.<name>.

1[openshell]
2version = 1
3
4[openshell.gateway]
5bind_address = "127.0.0.1:17670"
6log_level = "info"
7compute_drivers = ["kyma"]
8
9[openshell.drivers.kyma]
10socket_path = "/run/openshell/kyma-compute-driver.sock"