Skip to content
Open
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
116 changes: 104 additions & 12 deletions crates/openshell-server/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -659,12 +659,34 @@ fn prepare_server_config_with_drivers(
})
}

/// Reject an implicit development gateway before starting any listeners or services.
fn validate_user_auth(config: &openshell_core::Config) -> Result<()> {
if !config.auth.allow_unauthenticated_users
&& !config.mtls_auth.enabled
&& !config.mtls_auth.operator_enabled
&& config.oidc.is_none()
&& config.gateway_jwt.is_none()
{
return Err(miette::miette!(
"gateway has no user authentication configured\n\n\
No OIDC issuer, mTLS user auth, operator auth, or gateway_jwt is set, so every \
caller would be granted platform-admin access.\n\n\
Configure [openshell.gateway.oidc], [openshell.gateway.mtls_auth], or \
[openshell.gateway.gateway_jwt], or explicitly opt in for trusted local development:\n\n\
[openshell.gateway.auth]\n\
allow_unauthenticated_users = true"
));
}
Ok(())
}

async fn run_from_args(
mut args: RunArgs,
matches: ArgMatches,
compute_drivers: ComputeDriverRegistry,
) -> Result<()> {
let prepared = prepare_server_config_with_drivers(&mut args, &matches, &compute_drivers)?;
validate_user_auth(&prepared.config)?;

// Initialize OCSF identity before tracing can emit gateway events.
let gateway_identity = crate::gateway_ocsf::GatewayIdentity {
Expand Down Expand Up @@ -758,18 +780,6 @@ async fn run_from_args(
);
}

if !prepared.config.auth.allow_unauthenticated_users
&& !prepared.config.mtls_auth.enabled
&& !prepared.config.mtls_auth.operator_enabled
&& !has_oidc
&& prepared.config.gateway_jwt.is_none()
{
warn!(
"Neither mTLS user auth nor OIDC nor sandbox JWT auth is configured — \
the gateway has no authentication mechanism"
);
}

info!(bind = %prepared.config.bind_address, "Starting OpenShell server");

let result = Box::pin(run_server(prepared, tracing_log_bus, compute_drivers)).await;
Expand Down Expand Up @@ -1420,6 +1430,88 @@ mod tests {
use std::net::{IpAddr, Ipv4Addr};
use std::sync::atomic::{AtomicUsize, Ordering};

#[test]
fn user_auth_requires_a_mechanism_or_explicit_opt_in() {
// Cover each mechanism on its own and in combination, with and without
// the development opt-in. Only the entirely unconfigured case fails.
for mechanisms in 0..32 {
let mut config = openshell_core::Config::new(None);
config.auth.allow_unauthenticated_users = mechanisms & 1 != 0;
config.mtls_auth.enabled = mechanisms & 2 != 0;
config.mtls_auth.operator_enabled = mechanisms & 4 != 0;
if mechanisms & 8 != 0 {
config.oidc = Some(
toml::from_str("issuer = 'https://idp.example.com'\naudience = 'openshell'")
.unwrap(),
);
}
if mechanisms & 16 != 0 {
config.gateway_jwt = Some(toml::from_str(
"signing_key_path = 'key.pem'\npublic_key_path = 'public.pem'\nkid_path = 'kid'"
).unwrap());
}
assert_eq!(
super::validate_user_auth(&config).is_ok(),
mechanisms != 0,
"authentication combination {mechanisms}"
);
}
}

#[test]
fn startup_rejects_missing_user_auth_before_starting_services() {
for auth in [
"",
"[openshell.gateway.auth]\nallow_unauthenticated_users = false\n",
] {
let home = tempfile::tempdir().unwrap();
let config_path = home.path().join("gateway.toml");
std::fs::write(&config_path, format!("[openshell]\nversion = 2\n{auth}")).unwrap();
Environment::new()
.set("XDG_CONFIG_HOME", home.path().to_str().unwrap())
.set("XDG_STATE_HOME", home.path().to_str().unwrap())
.set("OPENSHELL_LOCAL_TLS_DIR", home.path().join("tls"))
.remove("OPENSHELL_GATEWAY_CONFIG")
.remove("OPENSHELL_DRIVERS")
.run(|| {
let (args, matches) = parse_with_args(&[
"openshell-gateway",
"--config",
config_path.to_str().unwrap(),
"--disable-tls",
"--db-url",
"sqlite::memory:",
"--compute-driver",
"test",
]);
// The factory panics if startup reaches driver construction.
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
let error = runtime
.block_on(super::run_from_args(
args,
matches,
test_registry("test", true),
))
.unwrap_err()
.to_string();
for expected in [
"gateway has no user authentication configured",
"OIDC",
"mTLS",
"operator auth",
"gateway_jwt",
"[openshell.gateway.auth]",
"allow_unauthenticated_users = true",
] {
assert!(error.contains(expected), "missing {expected:?} in {error}");
}
});
}
}

static REGISTRY_DETECTION_CALLS: AtomicUsize = AtomicUsize::new(0);
static REJECTING_VALIDATION_CALLS: AtomicUsize = AtomicUsize::new(0);

Expand Down
7 changes: 7 additions & 0 deletions docs/how-it-works/gateways/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,13 @@ The gateway rejects partial or invalid signing bundles at startup, including pri

External compute drivers declare this requirement by adding `openshell.compute.launch-authentication` to their extension metadata's `required_capabilities`. This requires a gateway that understands the launch-signing preflight and supplies `SandboxLaunchAuthentication` on creation. Drivers that use another launch mechanism can omit the requirement. The separate `supports_sandbox_authentication` capability describes the `AuthenticateSandbox` RPC and does not require launch signing.

The gateway refuses to start before binding any ports when no OIDC issuer, mTLS user authentication, operator authentication, or `gateway_jwt` is configured and `allow_unauthenticated_users` is unset or false. Configure an authentication mechanism, or explicitly opt in for trusted local development:

```toml
[openshell.gateway.auth]
allow_unauthenticated_users = true
```

`[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.

## Database Pool
Expand Down
1 change: 1 addition & 0 deletions skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ openshell --gateway-endpoint <url> status
Common findings:

- `No active gateway`: register one with `openshell gateway add <endpoint>`.
- `gateway has no user authentication configured`: configure OIDC, mTLS user or operator authentication, or gateway JWTs using the published gateway configuration reference. For trusted local development, explicitly set `[openshell.gateway.auth] allow_unauthenticated_users = true`; the gateway otherwise refuses to bind any ports.
- Connection refused: gateway process is not running, service exposure is wrong, or a port-forward/proxy is not active.
- TLS/certificate errors: the endpoint scheme or trust chain is wrong, a CLI mTLS bundle does not match the gateway CA, a supervisor is missing the gateway CA, or TLS termination does not match the gateway listener. Workloads and supervisors should not contain a user TLS client certificate or private key.
- Fresh snap installations select `gateway-mode=user`, but the user gateway is globally disabled by default. `install.sh` enables `snap.openshell.user-gateway.service` only for its target user; direct snap installations must select one user and run `systemctl --user enable --now snap.openshell.user-gateway.service` from that user's session. Do not enable it for multiple concurrently logged-in users: every instance binds to `127.0.0.1:17670`, so later instances fail with an address-in-use error. Older snap revisions could leave the unit globally enabled; remove that state with `sudo systemctl disable --global snap.openshell.user-gateway.service`, then disable and stop the service from every other affected user's session. The global command does not stop running user services. Setting `gateway-mode=user` disables the system gateway without changing any user's service. To change the selected user, disable and stop the current user's unit before enabling the new user's unit. Upgraded installations retain their system-owned state through `openshell.system-gateway`. Existing HTTPS registrations continue to work after the service rename. Follow the published snap installation steps to enroll a new user, replace an old HTTP registration, or change modes safely through the `disable` maintenance state. If `gateway-mode=disable`, no local snap gateway is expected to run.
Expand Down
Loading