Skip to content

Commit 2428ed1

Browse files
committed
feat(proxy): add discovery-driven application TLS passthrough
Route application TLS directly to discovered supervisor listeners, with bounded caches and connections, expiry and withdrawal, health endpoints, container/release packaging, and a standalone Helm chart. Support one workspace, a finite selected set, or all active workspaces through exclusive CLI and Helm settings. Validate complete snapshot scope before atomic cache replacement and retain credential reload on reconnect. Closes #4954 Refs #4668, #4669 Signed-off-by: Taylor Mutch <taylormutch@gmail.com>
1 parent ae5eb28 commit 2428ed1

27 files changed

Lines changed: 1481 additions & 11 deletions

File tree

‎.github/workflows/build-binaries.yml‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ jobs:
114114
binary: openshell-sandbox
115115
triple: aarch64-unknown-linux-musl
116116
runner: linux-arm64-cpu8
117+
- package: openshell-sandbox-proxy
118+
binary: openshell-sandbox-proxy
119+
triple: x86_64-unknown-linux-gnu
120+
runner: linux-amd64-cpu8
121+
- package: openshell-sandbox-proxy
122+
binary: openshell-sandbox-proxy
123+
triple: aarch64-unknown-linux-gnu
124+
runner: linux-arm64-cpu8
117125
- package: openshell-supervisor
118126
binary: openshell-supervisor
119127
triple: x86_64-unknown-linux-gnu

‎.github/workflows/build-images.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,12 @@ jobs:
6464
runs-on: linux-amd64-cpu8
6565
timeout-minutes: 20
6666
steps: *build_steps
67+
68+
sandbox-proxy:
69+
env:
70+
COMPONENT: sandbox-proxy
71+
BINARY: openshell-sandbox-proxy
72+
TARGET_SUFFIX: unknown-linux-gnu
73+
runs-on: linux-amd64-cpu8
74+
timeout-minutes: 20
75+
steps: *build_steps

‎.github/workflows/package-release-binaries.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ jobs:
5454
- artifact: openshell-supervisor-aarch64-unknown-linux-gnu
5555
binary: openshell-supervisor
5656
package: supervisor-binary-linux-arm64
57+
- artifact: openshell-sandbox-proxy-x86_64-unknown-linux-gnu
58+
binary: openshell-sandbox-proxy
59+
package: sandbox-proxy-binary-linux-amd64
60+
- artifact: openshell-sandbox-proxy-aarch64-unknown-linux-gnu
61+
binary: openshell-sandbox-proxy
62+
package: sandbox-proxy-binary-linux-arm64
5763
- artifact: openshell-driver-vm-x86_64-unknown-linux-gnu
5864
binary: openshell-driver-vm
5965
package: driver-vm-linux-amd64

‎.github/workflows/release-dev.yml‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ jobs:
183183
run: |
184184
set -euo pipefail
185185
REGISTRY="ghcr.io/nvidia/openshell"
186-
for component in gateway sandbox supervisor; do
186+
for component in gateway sandbox supervisor sandbox-proxy; do
187187
echo "Tagging ${REGISTRY}/${component}:${{ github.sha }} as dev..."
188188
docker buildx imagetools create \
189189
--prefer-index=false \
@@ -318,6 +318,13 @@ jobs:
318318
path: release/
319319
merge-multiple: true
320320

321+
- name: Download sandbox proxy binary artifacts
322+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
323+
with:
324+
pattern: sandbox-proxy-binary-*
325+
path: release/
326+
merge-multiple: true
327+
321328
- name: Download sandbox binary artifacts
322329
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
323330
with:
@@ -438,6 +445,10 @@ jobs:
438445
openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz \
439446
openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz > openshell-supervisor-checksums-sha256.txt
440447
cat openshell-supervisor-checksums-sha256.txt
448+
sha256sum -- \
449+
openshell-sandbox-proxy-x86_64-unknown-linux-gnu.tar.gz \
450+
openshell-sandbox-proxy-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-proxy-checksums-sha256.txt
451+
cat openshell-sandbox-proxy-checksums-sha256.txt
441452
442453
- name: Generate Homebrew formula
443454
run: |
@@ -544,7 +555,9 @@ jobs:
544555
release/openshell-sandbox-x86_64-unknown-linux-musl.tar.gz
545556
release/openshell-sandbox-aarch64-unknown-linux-musl.tar.gz
546557
release/openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz
558+
release/openshell-sandbox-proxy-x86_64-unknown-linux-gnu.tar.gz
547559
release/openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz
560+
release/openshell-sandbox-proxy-aarch64-unknown-linux-gnu.tar.gz
548561
release/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz
549562
release/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz
550563
release/openshell-driver-vm-aarch64-apple-darwin.tar.gz
@@ -557,6 +570,7 @@ jobs:
557570
release/openshell-gateway-checksums-sha256.txt
558571
release/openshell-sandbox-checksums-sha256.txt
559572
release/openshell-supervisor-checksums-sha256.txt
573+
release/openshell-sandbox-proxy-checksums-sha256.txt
560574
release/openshell-prover-checksums-sha256.txt
561575
562576
release-helm:

‎.github/workflows/release-publish.yml‎

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,13 @@ jobs:
9595
path: release/
9696
merge-multiple: true
9797

98+
- name: Download sandbox proxy binary artifacts
99+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
100+
with:
101+
pattern: sandbox-proxy-binary-*
102+
path: release/
103+
merge-multiple: true
104+
98105
- name: Download sandbox binary artifacts
99106
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
100107
with:
@@ -190,6 +197,10 @@ jobs:
190197
openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz \
191198
openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz > openshell-supervisor-checksums-sha256.txt
192199
cat openshell-supervisor-checksums-sha256.txt
200+
sha256sum -- \
201+
openshell-sandbox-proxy-x86_64-unknown-linux-gnu.tar.gz \
202+
openshell-sandbox-proxy-aarch64-unknown-linux-gnu.tar.gz > openshell-sandbox-proxy-checksums-sha256.txt
203+
cat openshell-sandbox-proxy-checksums-sha256.txt
193204
194205
- name: Generate Homebrew formula
195206
run: |
@@ -254,7 +265,7 @@ jobs:
254265
VERSION: ${{ inputs.semver }}
255266
run: |
256267
set -euo pipefail
257-
for component in gateway sandbox supervisor; do
268+
for component in gateway sandbox supervisor sandbox-proxy; do
258269
image="ghcr.io/nvidia/openshell/${component}:${VERSION}"
259270
# Never replace a published stable image; a rerun keeps the original.
260271
# Only a missing manifest means unpublished; any other lookup error
@@ -300,7 +311,7 @@ jobs:
300311
run: |
301312
set -euo pipefail
302313
REGISTRY="ghcr.io/nvidia/openshell"
303-
for component in gateway sandbox supervisor; do
314+
for component in gateway sandbox supervisor sandbox-proxy; do
304315
if [[ "${IS_PRERELEASE}" == "true" ]]; then
305316
echo "Tagging ${REGISTRY}/${component}:${SOURCE_TAG} as ${VERSION}..."
306317
docker buildx imagetools create \
@@ -432,7 +443,9 @@ jobs:
432443
release/openshell-sandbox-x86_64-unknown-linux-musl.tar.gz
433444
release/openshell-sandbox-aarch64-unknown-linux-musl.tar.gz
434445
release/openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz
446+
release/openshell-sandbox-proxy-x86_64-unknown-linux-gnu.tar.gz
435447
release/openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz
448+
release/openshell-sandbox-proxy-aarch64-unknown-linux-gnu.tar.gz
436449
release/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz
437450
release/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz
438451
release/openshell-driver-vm-aarch64-apple-darwin.tar.gz
@@ -445,6 +458,7 @@ jobs:
445458
release/openshell-gateway-checksums-sha256.txt
446459
release/openshell-sandbox-checksums-sha256.txt
447460
release/openshell-supervisor-checksums-sha256.txt
461+
release/openshell-sandbox-proxy-checksums-sha256.txt
448462
release/openshell-prover-checksums-sha256.txt
449463
450464
- name: Upload amd64 Debian prerelease package

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ Do not rely on this file for a full inventory. The detailed public and contribut
2525
| `crates/openshell-conformance/` | CLI conformance library | Reusable driver-agnostic scenarios and command runner |
2626
| `crates/openshell-server/` | Gateway server | Control-plane API, sandbox lifecycle, auth boundary |
2727
| `crates/openshell-sandbox/` | Sandbox runtime | Capability-free workload launcher, process identity, and seccomp-mediated I/O |
28+
| `crates/openshell-sandbox-proxy/` | Optional application proxy | Discovery-driven TLS passthrough directly to supervisor app listeners |
2829
| `crates/openshell-supervisor/` | Supervisor runtime | Gateway session, policy evaluation, credentials, and upstream networking |
2930
| `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 |
3031
| `crates/openshell-binary-identity/` | Binary identity | Shared trusted procfs executable identity resolution for isolation backends |

‎CONTRIBUTING.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,8 @@ still miss across worktrees.
344344

345345
## Container Image Base Overrides
346346

347+
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`.
348+
347349
The gateway and supervisor Dockerfiles accept `GATEWAY_BASE_IMAGE` and
348350
`SUPERVISOR_BASE_IMAGE` build arguments. Omitting them keeps the pinned
349351
distroless defaults. To build with your own bases, pass the argument through

‎Cargo.lock‎

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[package]
5+
name = "openshell-sandbox-proxy"
6+
description = "Optional discovery-driven TLS passthrough proxy for sandbox applications"
7+
version.workspace = true
8+
edition.workspace = true
9+
rust-version.workspace = true
10+
license.workspace = true
11+
repository.workspace = true
12+
13+
[dependencies]
14+
openshell-core = { path = "../openshell-core" }
15+
openshell-sdk = { path = "../openshell-sdk" }
16+
axum = { workspace = true }
17+
clap = { workspace = true }
18+
rustls = { workspace = true }
19+
tokio = { workspace = true }
20+
tonic = { workspace = true }
21+
tracing = { workspace = true }
22+
tracing-subscriber = { workspace = true }
23+
url = { workspace = true }
24+
25+
[dev-dependencies]
26+
tokio = { workspace = true, features = ["test-util"] }
27+
openshell-crypto = { workspace = true }
28+
29+
[lints]
30+
workspace = true
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<!--
2+
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
SPDX-License-Identifier: Apache-2.0
4+
-->
5+
6+
# Sandbox application proxy
7+
8+
`openshell-sandbox-proxy` is an optional consumer of the gateway service discovery
9+
API. It selects the exact discovered hostname from TLS SNI and forwards TLS bytes
10+
to the owning supervisor's app listener. Application certificates and private
11+
keys remain at supervisors. HTTP, WebSockets, application authentication, and
12+
sandbox policy remain enforced by the existing listener and application.
13+
14+
```shell
15+
openshell-sandbox-proxy \
16+
--gateway-endpoint https://gateway.example.com \
17+
--workspace default \
18+
--gateway-ca /run/discovery/ca.crt \
19+
--discovery-token-file /run/discovery/token \
20+
--listen-address 0.0.0.0:8443
21+
```
22+
23+
Repeat `--workspace` for a finite set (up to 64 distinct names), or use
24+
`--all-workspaces` to include every active workspace, including future ones.
25+
These modes are mutually exclusive. For example:
26+
27+
```shell
28+
openshell-sandbox-proxy --gateway-endpoint https://gateway.example.com \
29+
--workspace team-a --workspace team-b \
30+
--discovery-client-cert /run/discovery/tls.crt \
31+
--discovery-client-key /run/discovery/tls.key --gateway-ca /run/discovery/ca.crt
32+
```
33+
34+
Use a discovery-only `OU=discovery` certificate with an administrator-managed
35+
fingerprint grant, or an existing identity authorized for the selection. Client mTLS can
36+
use `--discovery-client-cert` and `--discovery-client-key`. Credentials and CA
37+
files reload on reconnect. The gateway must authorize every selected workspace;
38+
a finite grant cannot request platform scope. Unauthorized selections fail as a
39+
whole. All-workspaces output is bounded by the gateway's 64-workspace scan and
40+
shared route/byte limits. The proxy does not require operator credentials. Plaintext
41+
credential-free loopback development requires `--allow-insecure-gateway`.
42+
43+
A snapshot initializes the cache and bounded watches replace it. Reconnects
44+
relist; transient errors retain routes only until their published expiry.
45+
Authorization/configuration/data-integrity failures clear the cache. Monotonic
46+
local deadlines are derived from absolute expiries and capped at fifteen seconds;
47+
operators must synchronize gateway and proxy clocks. Snapshot revisions are
48+
opaque and installation versions identify replacement independently of renewal.
49+
50+
Only exact, current routes can dial a backend. A route withdrawal, installation
51+
replacement, or expiry closes affected TCP streams; renewal preserves them.
52+
There is no gateway payload fallback, wildcard route fallback, or caller-selected
53+
backend. Limits are 4,096 cached routes, 1,024 concurrent connections, 64 per
54+
hostname, a 64 KiB ClientHello, and five-second hello/connect/setup deadlines.
55+
Each relay uses Tokio's bounded bidirectional copy buffers. ClientHello parsing
56+
is dependency-owned rustls protocol parsing, not a first-party cryptographic
57+
operation. Authenticated discovery transport uses the shared SDK/crypto facade.
58+
59+
The health listener defaults to port 9091: `/healthz` is process liveness,
60+
`/readyz` reflects discovery availability, and `/metrics` exposes connection
61+
counts. During transient discovery failures readiness fails while existing
62+
unexpired connections can continue. Expiry still closes streams independently
63+
of the discovery connection. Multiple replicas keep independent caches and need
64+
no Kubernetes ownership or leader election.
65+
66+
Build with `mise run docker:build:sandbox-proxy`. The standalone Helm chart at
67+
`deploy/helm/openshell-sandbox-proxy` mounts only discovery identity/trust Secrets;
68+
it needs no Kubernetes API permissions. Configure NetworkPolicy to allow the
69+
proxy to reach the gateway API and discovered supervisor app endpoints, and
70+
restrict supervisor ingress where edge authentication is required. The proxy
71+
never authenticates application users using its discovery identity.
72+
73+
For Gateway API TLS passthrough, attach a TLSRoute to a Gateway listener in
74+
`Passthrough` mode and target the proxy Service on port 443. Application TLS
75+
terminates at the supervisor. An HTTPRoute requires a separate edge termination
76+
and backend-TLS contract. A controller can instead consume the same discovery
77+
API and reconcile exact-host routes directly to supervisor endpoints; it does
78+
not need this proxy.
79+
80+
## Verification
81+
82+
```shell
83+
cargo test -p openshell-sandbox-proxy
84+
cargo clippy -p openshell-sandbox-proxy --all-targets -- -D warnings
85+
helm lint deploy/helm/openshell-sandbox-proxy \
86+
--set gatewayEndpoint=https://gateway.example.com --set workspace=default
87+
```
88+
89+
Kubernetes discovery resolves dedicated supervisors and acknowledged shared-pool
90+
owners. During HA takeover the proxy withdraws the old installation and connects
91+
to the new owner after it advertises an accepted installation. Existing streams
92+
may close; clients reconnect without restarting the sandbox. Readable and hashed
93+
hostnames use the same discovery path. The proxy consumes returned hostnames
94+
and never reconstructs them or needs the gateway's hashing configuration.

0 commit comments

Comments
 (0)