Skip to content
Merged
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
3 changes: 2 additions & 1 deletion architecture/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ attachments; and asks compute runtimes to create or delete sandbox workloads.
- Authenticate clients and sandbox supervisor sessions.
- Serve gRPC APIs for sandbox lifecycle, provider management, policy updates,
settings, logs, watch streams, and relay forwarding.
- Serve HTTP endpoints for health, WebSocket tunnels, and edge-auth flows.
- Serve HTTP endpoints for health and edge-auth flows, plus the opt-in
WebSocket tunnel for edge-proxy deployments.
- Persist domain objects in SQLite or Postgres.
- Resolve endpoint-bound provider environments for sandbox supervisors.
- Coordinate supervisor relay sessions for connect, exec, file sync, and
Expand Down
12 changes: 12 additions & 0 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,10 @@ pub struct Config {
/// Gateway user authentication behavior.
pub auth: GatewayAuthConfig,

/// Allow the WebSocket tunnel used by authenticated edge proxies.
/// Disabled for local gateways by default.
pub enable_websocket_tunnel: bool,

/// Disabled-by-default gateway interceptor service configs.
pub gateway_interceptors: Vec<GatewayInterceptorConfig>,

Expand Down Expand Up @@ -850,6 +854,7 @@ impl Config {
tls,
oidc: None,
auth: GatewayAuthConfig::default(),
enable_websocket_tunnel: false,
gateway_interceptors: Vec::new(),
provider_profile_sources: vec![GatewayProviderProfileSourceConfig::User],
mtls_auth: MtlsAuthConfig::default(),
Expand Down Expand Up @@ -1022,6 +1027,13 @@ impl Config {
self.service_routing.enable_loopback_service_http = enabled;
self
}

/// Enable the WebSocket tunnel for an authenticated edge proxy.
#[must_use]
pub const fn with_websocket_tunnel(mut self, enabled: bool) -> Self {
self.enable_websocket_tunnel = enabled;
self
}
}

impl Default for ServiceRoutingConfig {
Expand Down
41 changes: 40 additions & 1 deletion crates/openshell-server/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,15 @@ struct RunArgs {
action = ArgAction::Set
)]
enable_loopback_service_http: bool,

/// Enable the WebSocket tunnel for an authenticated edge proxy.
#[arg(
long,
env = "OPENSHELL_ENABLE_WEBSOCKET_TUNNEL",
default_value_t = false,
action = ArgAction::Set
)]
enable_websocket_tunnel: bool,
}

pub fn command() -> Command {
Expand Down Expand Up @@ -508,7 +517,8 @@ fn prepare_server_config_with_drivers(
.unwrap_or_default(),
)
.with_server_sans(args.server_sans.clone())
.with_loopback_service_http(args.enable_loopback_service_http);
.with_loopback_service_http(args.enable_loopback_service_http)
.with_websocket_tunnel(args.enable_websocket_tunnel);
if let Some(sources) = file
.as_ref()
.and_then(|file| file.openshell.gateway.provider_profile_sources.clone())
Expand Down Expand Up @@ -1089,6 +1099,11 @@ fn merge_file_into_args(args: &mut RunArgs, file: &GatewayFileSection, matches:
{
args.enable_loopback_service_http = enabled;
}
if let Some(enabled) = file.enable_websocket_tunnel
&& arg_defaulted(matches, "enable_websocket_tunnel")
{
args.enable_websocket_tunnel = enabled;
}
if let Some(mtls_auth) = &file.mtls_auth
&& arg_defaulted(matches, "enable_mtls_auth")
{
Expand Down Expand Up @@ -1440,6 +1455,30 @@ mod tests {
assert!(cli.run.enable_loopback_service_http);
}

#[test]
fn websocket_tunnel_is_disabled_by_default_and_can_be_enabled_from_file() {
let _lock = ENV_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let _guard = EnvVarGuard::remove("OPENSHELL_ENABLE_WEBSOCKET_TUNNEL");
let (mut args, matches) =
parse_with_args(&["openshell-gateway", "--db-url", "sqlite::memory:"]);
assert!(!args.enable_websocket_tunnel);

let file = config_file_from_toml("[openshell.gateway]\nenable_websocket_tunnel = true\n");
merge_file_into_args(&mut args, &file.openshell.gateway, &matches);
assert!(args.enable_websocket_tunnel);

let (mut args, matches) = parse_with_args(&[
"openshell-gateway",
"--db-url",
"sqlite::memory:",
"--enable-websocket-tunnel=false",
]);
merge_file_into_args(&mut args, &file.openshell.gateway, &matches);
assert!(!args.enable_websocket_tunnel, "CLI flag must override file");
}

#[test]
fn command_disables_loopback_service_http_with_false_value() {
let _lock = ENV_LOCK
Expand Down
3 changes: 3 additions & 0 deletions crates/openshell-server/src/config_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,9 @@ pub struct GatewayFileSection {
/// Enable plaintext HTTP routing for loopback sandbox service URLs.
#[serde(default)]
pub enable_loopback_service_http: Option<bool>,
/// Enable the WebSocket tunnel for an authenticated edge proxy.
#[serde(default)]
pub enable_websocket_tunnel: Option<bool>,

// ── Sandbox client TLS ───────────────────────────────────────────────
#[serde(default)]
Expand Down
14 changes: 8 additions & 6 deletions crates/openshell-server/src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,12 +179,14 @@ async fn render_metrics(State(handle): State<PrometheusHandle>) -> impl IntoResp

/// Create the HTTP router served on the multiplexed gateway port.
pub fn http_router(state: Arc<crate::ServerState>) -> Router {
crate::ws_tunnel::router(state.clone())
.merge(crate::auth::router(state.clone()))
.layer(middleware::from_fn_with_state(
state,
sandbox_service_routing_first,
))
let mut router = crate::auth::router(state.clone());
if state.config.enable_websocket_tunnel {
router = router.merge(crate::ws_tunnel::router(state.clone()));
}
router.layer(middleware::from_fn_with_state(
state,
sandbox_service_routing_first,
))
}

/// Create the plaintext loopback-only router for browser service endpoints.
Expand Down
24 changes: 24 additions & 0 deletions crates/openshell-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,9 @@ mod tests {
use tokio::sync::watch;

use crate::tls_test_utils::generate_test_certs_with_ca;
use axum::body::Body;
use http::{Request, StatusCode};
use tower::ServiceExt;

fn tls_enabled_config() -> Config {
Config::new(Some(openshell_core::TlsConfig {
Expand Down Expand Up @@ -2174,6 +2177,27 @@ mod tests {
))
}

#[tokio::test]
async fn websocket_tunnel_is_mounted_only_when_enabled() {
let state = test_state("127.0.0.1:17670".parse().unwrap(), true).await;
let response = super::http_router(state.clone())
.oneshot(Request::get("/_ws_tunnel").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);

let mut enabled = state;
Arc::get_mut(&mut enabled)
.unwrap()
.config
.enable_websocket_tunnel = true;
let response = super::http_router(enabled)
.oneshot(Request::get("/_ws_tunnel").body(Body::empty()).unwrap())
.await
.unwrap();
assert_ne!(response.status(), StatusCode::NOT_FOUND);
}

async fn start_tls_gateway_listener(
bind_addr: &str,
enable_loopback_service_http: bool,
Expand Down
1 change: 1 addition & 0 deletions deploy/helm/openshell/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ discovery endpoint or its TLS CA.
| server.drivers.kubernetes.workspaceMode | string | `"shared"` | How workspaces map to Kubernetes namespaces. "shared" (default): all sandboxes in a single namespace. "managed": auto-creates per-workspace namespaces. "operator": uses pre-provisioned namespaces. |
| server.enableLoopbackServiceHttp | bool | `true` | Enable plaintext HTTP routing for loopback sandbox service URLs on TLS-enabled gateways. |
| server.enableUserNamespaces | bool | `false` | Enable Kubernetes user namespace isolation (hostUsers: false) for sandbox pods. Requires Kubernetes 1.33+ with user namespace support available (beta through 1.35, GA in 1.36+), plus a supporting container runtime and Linux 5.12+. When enabled, container UID 0 maps to an unprivileged host UID and capabilities become namespaced. |
| server.enableWebsocketTunnel | bool | `false` | Enable the WebSocket tunnel used by CLI/SDK clients behind an authenticated edge proxy. Leave disabled for direct gateway installs. |
| server.externalDbSecret | string | `""` | Name of a pre-existing Opaque Secret containing a PostgreSQL connection URI (key: uri). When set, the gateway reads OPENSHELL_DB_URL from this Secret instead of using dbUrl. The Secret must contain a `uri` key, e.g. postgresql://user:pass@host:5432/dbname. |
| server.grpcEndpoint | string | `""` | gRPC endpoint sandboxes call back into the gateway. Leave empty to derive it from the chart fullname, release namespace, service port, and disableTls flag, for example https://openshell.openshell.svc.cluster.local:8080. Override only when sandboxes must reach the gateway via a different hostname (e.g. an external ingress or a host alias). |
| server.grpcRateLimit.requests | int | `0` | Maximum gRPC requests allowed per window. Must be positive (alongside windowSeconds) to enable rate limiting; 0 (default) disables it. |
Expand Down
1 change: 1 addition & 0 deletions deploy/helm/openshell/templates/gateway-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ data:
disable_tls = true
{{- end }}
enable_loopback_service_http = {{ .Values.server.enableLoopbackServiceHttp }}
enable_websocket_tunnel = {{ .Values.server.enableWebsocketTunnel }}
{{- $sans := list -}}
{{- if and .Values.certManager.enabled .Values.certManager.serverDnsNames }}
{{- $sans = .Values.certManager.serverDnsNames }}
Expand Down
16 changes: 16 additions & 0 deletions deploy/helm/openshell/tests/gateway_config_test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ release:
namespace: my-namespace

tests:
- it: disables the WebSocket tunnel unless explicitly enabled for an edge proxy
template: templates/gateway-config.yaml
asserts:
- matchRegex:
path: data["gateway.toml"]
pattern: '(?m)^enable_websocket_tunnel\s*=\s*false$'

- it: enables the WebSocket tunnel when requested
template: templates/gateway-config.yaml
set:
server.enableWebsocketTunnel: true
asserts:
- matchRegex:
path: data["gateway.toml"]
pattern: '(?m)^enable_websocket_tunnel\s*=\s*true$'

- it: defaults to label admission with caller driver config disabled
template: templates/gateway-config.yaml
asserts:
Expand Down
3 changes: 3 additions & 0 deletions deploy/helm/openshell/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ server:
# -- Enable plaintext HTTP routing for loopback sandbox service URLs on
# TLS-enabled gateways.
enableLoopbackServiceHttp: true
# -- Enable the WebSocket tunnel used by CLI/SDK clients behind an
# authenticated edge proxy. Leave disabled for direct gateway installs.
enableWebsocketTunnel: false
# -- Posture when a candidate sandbox policy fails validation. `fail_closed`
# deactivates the previous policy; `retain_last_valid` keeps it active.
policyValidationFailureMode: fail_closed
Expand Down
2 changes: 1 addition & 1 deletion docs/how-it-works/gateways/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ a workspace. For membership commands, refer to

### Edge JWT (cloud gateways)

For gateways behind a reverse proxy that handles authentication (e.g. Cloudflare Access), the CLI uses a browser-based login flow and routes traffic through a WebSocket tunnel.
For gateways behind a reverse proxy that handles authentication (e.g. Cloudflare Access), the CLI uses a browser-based login flow and routes traffic through a WebSocket tunnel. The tunnel is disabled by default; enable it with `enable_websocket_tunnel = true` under `[openshell.gateway]` (Helm: `server.enableWebsocketTunnel=true`).

**Registration flow** (`openshell gateway add https://gateway.example.com`):

Expand Down
2 changes: 2 additions & 0 deletions docs/how-it-works/gateways/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ policy_validation_failure_mode = "fail_closed"
server_sans = ["openshell", "*.dev.openshell.localhost"]
# Allow plaintext HTTP routing for loopback sandbox service URLs.
enable_loopback_service_http = true
# Serve the WebSocket tunnel for CLI clients behind an authenticating edge proxy.
enable_websocket_tunnel = false

# Set true only for local plaintext gateways or trusted TLS termination.
disable_tls = false
Expand Down
2 changes: 2 additions & 0 deletions e2e/with-docker-gateway.sh
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,8 @@ GATEWAY_ARGS=(
--compute-driver docker
--tls-cert "${PKI_DIR}/server/tls.crt"
--tls-key "${PKI_DIR}/server/tls.key"
# edge_tunnel_e2e exercises the opt-in WebSocket tunnel.
--enable-websocket-tunnel true
--db-url "sqlite:${STATE_DIR}/gateway.db?mode=rwc"
)

Expand Down
2 changes: 2 additions & 0 deletions e2e/with-podman-gateway.sh
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,8 @@ GATEWAY_ARGS=(
--health-port "${HEALTH_PORT}"
--tls-cert "${PKI_DIR}/server/tls.crt"
--tls-key "${PKI_DIR}/server/tls.key"
# edge_tunnel_e2e exercises the opt-in WebSocket tunnel.
--enable-websocket-tunnel true
--db-url "sqlite:${STATE_DIR}/gateway.db?mode=rwc"
--log-level info
)
Expand Down
4 changes: 3 additions & 1 deletion skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,9 @@ Gateway configuration requires `[openshell] version = 2`, a singular
`--drivers` selectors rather than silently migrating them. One valid, nonempty
`OPENSHELL_DRIVERS` value remains a deprecated environment-only alias when the
canonical selector is absent; the gateway selects that driver with a warning.
Empty, invalid, comma-delimited, or conflicting values fail startup. Homebrew
Empty, invalid, comma-delimited, or conflicting values fail startup. The
WebSocket tunnel for edge-proxy CLI access is off unless
`enable_websocket_tunnel = true` (`server.enableWebsocketTunnel` in Helm). Homebrew
and RPM package startup migrates only exact package-generated v1 defaults. If
an upgraded package still reports an unsupported version, inspect the active prefix or `~/.config/openshell/gateway.toml`; an edited v1 file must
follow the published schema-v2 migration steps and must not be overwritten.
Expand Down
Loading