Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/build-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ jobs:
binary: openshell-sandbox
triple: aarch64-unknown-linux-musl
runner: linux-arm64-cpu8
- package: openshell-sandbox-proxy
binary: openshell-sandbox-proxy
triple: x86_64-unknown-linux-gnu
runner: linux-amd64-cpu8
- package: openshell-sandbox-proxy
binary: openshell-sandbox-proxy
triple: aarch64-unknown-linux-gnu
runner: linux-arm64-cpu8
- package: openshell-supervisor
binary: openshell-supervisor
triple: x86_64-unknown-linux-gnu
Expand Down
9 changes: 9 additions & 0 deletions .github/workflows/build-images.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,12 @@ jobs:
runs-on: linux-amd64-cpu8
timeout-minutes: 20
steps: *build_steps

sandbox-proxy:
env:
COMPONENT: sandbox-proxy
BINARY: openshell-sandbox-proxy
TARGET_SUFFIX: unknown-linux-gnu
runs-on: linux-amd64-cpu8
timeout-minutes: 20
steps: *build_steps
6 changes: 6 additions & 0 deletions .github/workflows/package-release-binaries.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ jobs:
- artifact: openshell-supervisor-aarch64-unknown-linux-gnu
binary: openshell-supervisor
package: supervisor-binary-linux-arm64
- artifact: openshell-sandbox-proxy-x86_64-unknown-linux-gnu
binary: openshell-sandbox-proxy
package: sandbox-proxy-binary-linux-amd64
- artifact: openshell-sandbox-proxy-aarch64-unknown-linux-gnu
binary: openshell-sandbox-proxy
package: sandbox-proxy-binary-linux-arm64
- artifact: openshell-driver-vm-x86_64-unknown-linux-gnu
binary: openshell-driver-vm
package: driver-vm-linux-amd64
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/release-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ jobs:
run: |
set -euo pipefail
REGISTRY="ghcr.io/nvidia/openshell"
for component in gateway sandbox supervisor; do
for component in gateway sandbox supervisor sandbox-proxy; do
echo "Tagging ${REGISTRY}/${component}:${{ github.sha }} as dev..."
docker buildx imagetools create \
--prefer-index=false \
Expand Down Expand Up @@ -321,6 +321,13 @@ jobs:
path: release/
merge-multiple: true

- name: Download sandbox proxy binary artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: sandbox-proxy-binary-*
path: release/
merge-multiple: true

- name: Download sandbox binary artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand Down Expand Up @@ -441,6 +448,10 @@ jobs:
openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz \
openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz > openshell-supervisor-checksums-sha256.txt
cat openshell-supervisor-checksums-sha256.txt
sha256sum -- \
openshell-sandbox-proxy-x86_64-unknown-linux-gnu.tar.gz \
openshell-sandbox-proxy-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-proxy-checksums-sha256.txt
cat openshell-sandbox-proxy-checksums-sha256.txt
- name: Generate Homebrew formula
run: |
Expand Down Expand Up @@ -547,7 +558,9 @@ jobs:
release/openshell-sandbox-x86_64-unknown-linux-musl.tar.gz
release/openshell-sandbox-aarch64-unknown-linux-musl.tar.gz
release/openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz
release/openshell-sandbox-proxy-x86_64-unknown-linux-gnu.tar.gz
release/openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz
release/openshell-sandbox-proxy-aarch64-unknown-linux-gnu.tar.gz
release/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz
release/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz
release/openshell-driver-vm-aarch64-apple-darwin.tar.gz
Expand All @@ -560,6 +573,7 @@ jobs:
release/openshell-gateway-checksums-sha256.txt
release/openshell-sandbox-checksums-sha256.txt
release/openshell-supervisor-checksums-sha256.txt
release/openshell-sandbox-proxy-checksums-sha256.txt
release/openshell-prover-checksums-sha256.txt
release-helm:
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,13 @@ jobs:
path: release/
merge-multiple: true

- name: Download sandbox proxy binary artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: sandbox-proxy-binary-*
path: release/
merge-multiple: true

- name: Download sandbox binary artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
Expand Down Expand Up @@ -198,6 +205,10 @@ jobs:
openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz \
openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz > openshell-supervisor-checksums-sha256.txt
cat openshell-supervisor-checksums-sha256.txt
sha256sum -- \
openshell-sandbox-proxy-x86_64-unknown-linux-gnu.tar.gz \
openshell-sandbox-proxy-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-proxy-checksums-sha256.txt
cat openshell-sandbox-proxy-checksums-sha256.txt
- name: Generate Homebrew formula
run: |
Expand Down Expand Up @@ -262,7 +273,7 @@ jobs:
VERSION: ${{ inputs.semver }}
run: |
set -euo pipefail
for component in gateway sandbox supervisor; do
for component in gateway sandbox supervisor sandbox-proxy; do
image="ghcr.io/nvidia/openshell/${component}:${VERSION}"
# Archives are build artifacts from this run, not externally signed images.
nix shell --inputs-from . nixpkgs#skopeo -c \
Expand Down Expand Up @@ -296,7 +307,7 @@ jobs:
run: |
set -euo pipefail
REGISTRY="ghcr.io/nvidia/openshell"
for component in gateway sandbox supervisor; do
for component in gateway sandbox supervisor sandbox-proxy; do
if [[ "${IS_PRERELEASE}" == "true" ]]; then
echo "Tagging ${REGISTRY}/${component}:${SOURCE_TAG} as ${VERSION}..."
docker buildx imagetools create \
Expand Down Expand Up @@ -429,7 +440,9 @@ jobs:
release/openshell-sandbox-x86_64-unknown-linux-musl.tar.gz
release/openshell-sandbox-aarch64-unknown-linux-musl.tar.gz
release/openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz
release/openshell-sandbox-proxy-x86_64-unknown-linux-gnu.tar.gz
release/openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz
release/openshell-sandbox-proxy-aarch64-unknown-linux-gnu.tar.gz
release/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz
release/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz
release/openshell-driver-vm-aarch64-apple-darwin.tar.gz
Expand All @@ -442,6 +455,7 @@ jobs:
release/openshell-gateway-checksums-sha256.txt
release/openshell-sandbox-checksums-sha256.txt
release/openshell-supervisor-checksums-sha256.txt
release/openshell-sandbox-proxy-checksums-sha256.txt
release/openshell-prover-checksums-sha256.txt
- name: Upload amd64 Debian prerelease package
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Do not rely on this file for a full inventory. The detailed public and contribut
| `crates/openshell-conformance/` | CLI conformance library | Reusable driver-agnostic scenarios and command runner |
| `crates/openshell-server/` | Gateway server | Control-plane API, sandbox lifecycle, auth boundary |
| `crates/openshell-sandbox/` | Sandbox runtime | Capability-free workload launcher, process identity, and seccomp-mediated I/O |
| `crates/openshell-sandbox-proxy/` | Optional application proxy | Discovery-driven TLS passthrough directly to supervisor app listeners |
| `crates/openshell-supervisor/` | Supervisor runtime | Gateway session, policy evaluation, credentials, and upstream networking |
| `crates/openshell-supervisor-control/` | Shared supervisor control | Gateway-owned assignment controller, launch registrations, recovery coordination, and host observations for a supervisor shared by many sandboxes |
| `crates/openshell-binary-identity/` | Binary identity | Shared trusted procfs executable identity resolution for isolation backends |
Expand Down
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,8 @@ still miss across worktrees.

## Container Image Base Overrides

The optional application proxy image can be built with `mise run docker:build:sandbox-proxy`. Its standalone deployment chart is in `deploy/helm/openshell-sandbox-proxy`.

The gateway and supervisor Dockerfiles accept `GATEWAY_BASE_IMAGE` and
`SUPERVISOR_BASE_IMAGE` build arguments. Omitting them keeps the pinned
distroless defaults. To build with your own bases, pass the argument through
Expand Down
17 changes: 17 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions crates/openshell-sandbox-proxy/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[package]
name = "openshell-sandbox-proxy"
description = "Optional discovery-driven TLS passthrough proxy for sandbox applications"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true

[dependencies]
openshell-core = { path = "../openshell-core" }
openshell-sdk = { path = "../openshell-sdk" }
axum = { workspace = true }
clap = { workspace = true }
rustls = { workspace = true }
tokio = { workspace = true }
tonic = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }

[dev-dependencies]
tokio = { workspace = true, features = ["test-util"] }
openshell-crypto = { workspace = true }

[lints]
workspace = true
94 changes: 94 additions & 0 deletions crates/openshell-sandbox-proxy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Sandbox application proxy

`openshell-sandbox-proxy` is an optional consumer of the gateway service discovery
API. It selects the exact discovered hostname from TLS SNI and forwards TLS bytes
to the owning supervisor's app listener. Application certificates and private
keys remain at supervisors. HTTP, WebSockets, application authentication, and
sandbox policy remain enforced by the existing listener and application.

```shell
openshell-sandbox-proxy \
--gateway-endpoint https://gateway.example.com \
--workspace default \
--gateway-ca /run/discovery/ca.crt \
--discovery-token-file /run/discovery/token \
--listen-address 0.0.0.0:8443
```

Repeat `--workspace` for a finite set (up to 64 distinct names), or use
`--all-workspaces` to include every active workspace, including future ones.
These modes are mutually exclusive. For example:

```shell
openshell-sandbox-proxy --gateway-endpoint https://gateway.example.com \
--workspace team-a --workspace team-b \
--discovery-client-cert /run/discovery/tls.crt \
--discovery-client-key /run/discovery/tls.key --gateway-ca /run/discovery/ca.crt
```

Use a discovery-only `OU=discovery` certificate with an administrator-managed
fingerprint grant, or an existing identity authorized for the selection. Client mTLS can
use `--discovery-client-cert` and `--discovery-client-key`. Credentials and CA
files reload on reconnect. The gateway must authorize every selected workspace;
a finite grant cannot request platform scope. Unauthorized selections fail as a
whole. All-workspaces output is bounded by the gateway's 64-workspace scan and
shared route/byte limits. The proxy does not require operator credentials. Plaintext
credential-free loopback development requires `--allow-insecure-gateway`.

A snapshot initializes the cache and bounded watches replace it. Reconnects
relist; transient errors retain routes only until their published expiry.
Authorization/configuration/data-integrity failures clear the cache. Monotonic
local deadlines are derived from absolute expiries and capped at fifteen seconds;
operators must synchronize gateway and proxy clocks. Snapshot revisions are
opaque and installation versions identify replacement independently of renewal.

Only exact, current routes can dial a backend. A route withdrawal, installation
replacement, or expiry closes affected TCP streams; renewal preserves them.
There is no gateway payload fallback, wildcard route fallback, or caller-selected
backend. Limits are 4,096 cached routes, 1,024 concurrent connections, 64 per
hostname, a 64 KiB ClientHello, and five-second hello/connect/setup deadlines.
Each relay uses Tokio's bounded bidirectional copy buffers. ClientHello parsing
is dependency-owned rustls protocol parsing, not a first-party cryptographic
operation. Authenticated discovery transport uses the shared SDK/crypto facade.

The health listener defaults to port 9091: `/healthz` is process liveness,
`/readyz` reflects discovery availability, and `/metrics` exposes connection
counts. During transient discovery failures readiness fails while existing
unexpired connections can continue. Expiry still closes streams independently
of the discovery connection. Multiple replicas keep independent caches and need
no Kubernetes ownership or leader election.

Build with `mise run docker:build:sandbox-proxy`. The standalone Helm chart at
`deploy/helm/openshell-sandbox-proxy` mounts only discovery identity/trust Secrets;
it needs no Kubernetes API permissions. Configure NetworkPolicy to allow the
proxy to reach the gateway API and discovered supervisor app endpoints, and
restrict supervisor ingress where edge authentication is required. The proxy
never authenticates application users using its discovery identity.

For Gateway API TLS passthrough, attach a TLSRoute to a Gateway listener in
`Passthrough` mode and target the proxy Service on port 443. Application TLS
terminates at the supervisor. An HTTPRoute requires a separate edge termination
and backend-TLS contract. A controller can instead consume the same discovery
API and reconcile exact-host routes directly to supervisor endpoints; it does
not need this proxy.

## Verification

```shell
cargo test -p openshell-sandbox-proxy
cargo clippy -p openshell-sandbox-proxy --all-targets -- -D warnings
helm lint deploy/helm/openshell-sandbox-proxy \
--set gatewayEndpoint=https://gateway.example.com --set workspace=default
```

Kubernetes discovery resolves dedicated supervisors and acknowledged shared-pool
owners. During HA takeover the proxy withdraws the old installation and connects
to the new owner after it advertises an accepted installation. Existing streams
may close; clients reconnect without restarting the sandbox. Hashed multi-gateway hostnames require
the common listener/discovery hostname integration; the proxy consumes returned
hostnames and never reconstructs them.
Loading
Loading