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
1 change: 0 additions & 1 deletion .agents/skills/debug-openshell-cluster/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,6 @@ Check required Helm deployment secrets:

```bash
kubectl -n openshell get secret \
openshell-ssh-handshake \
openshell-server-tls \
openshell-server-client-ca \
openshell-client-tls
Expand Down
4 changes: 0 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,10 +152,6 @@ cargo build -p openshell-prover --features bundled-z3
# One-time trust
mise trust

# Podman and Kubernetes drivers require an SSH handshake secret.
# Set any value for local development:
export OPENSHELL_SSH_HANDSHAKE_SECRET=dev-secret

# Run a standalone gateway for local development
mise run gateway
```
Expand Down
31 changes: 0 additions & 31 deletions crates/openshell-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,6 @@ pub const DEFAULT_SERVER_PORT: u16 = 8080;
/// Default container stop timeout in seconds (SIGTERM → SIGKILL).
pub const DEFAULT_STOP_TIMEOUT_SECS: u32 = 10;

/// Default allowed clock skew for SSH handshake validation, in seconds.
pub const DEFAULT_SSH_HANDSHAKE_SKEW_SECS: u64 = 300;

/// Default Podman bridge network name.
pub const DEFAULT_NETWORK_NAME: &str = "openshell";

Expand Down Expand Up @@ -272,14 +269,6 @@ pub struct Config {
#[serde(default = "default_sandbox_ssh_socket_path")]
pub sandbox_ssh_socket_path: String,

/// Shared secret for gateway-to-sandbox SSH handshake.
#[serde(default)]
pub ssh_handshake_secret: String,

/// Allowed clock skew for SSH handshake validation, in seconds.
#[serde(default = "default_ssh_handshake_skew_secs")]
pub ssh_handshake_skew_secs: u64,

/// TTL for SSH session tokens, in seconds. 0 disables expiry.
#[serde(default = "default_ssh_session_ttl_secs")]
pub ssh_session_ttl_secs: u64,
Expand Down Expand Up @@ -429,8 +418,6 @@ impl Config {
ssh_gateway_port: default_ssh_gateway_port(),
sandbox_ssh_port: default_sandbox_ssh_port(),
sandbox_ssh_socket_path: default_sandbox_ssh_socket_path(),
ssh_handshake_secret: String::new(),
ssh_handshake_skew_secs: default_ssh_handshake_skew_secs(),
ssh_session_ttl_secs: default_ssh_session_ttl_secs(),
client_tls_secret_name: String::new(),
host_gateway_ip: String::new(),
Expand Down Expand Up @@ -544,20 +531,6 @@ impl Config {
self
}

/// Create a new configuration with the SSH handshake secret.
#[must_use]
pub fn with_ssh_handshake_secret(mut self, secret: impl Into<String>) -> Self {
self.ssh_handshake_secret = secret.into();
self
}

/// Create a new configuration with SSH handshake skew allowance.
#[must_use]
pub const fn with_ssh_handshake_skew_secs(mut self, secs: u64) -> Self {
self.ssh_handshake_skew_secs = secs;
self
}

/// Create a new configuration with the SSH session TTL.
#[must_use]
pub const fn with_ssh_session_ttl_secs(mut self, secs: u64) -> Self {
Expand Down Expand Up @@ -707,10 +680,6 @@ const fn default_sandbox_ssh_port() -> u16 {
DEFAULT_SSH_PORT
}

const fn default_ssh_handshake_skew_secs() -> u64 {
DEFAULT_SSH_HANDSHAKE_SKEW_SECS
}

const fn default_ssh_session_ttl_secs() -> u64 {
86400 // 24 hours
}
Expand Down
6 changes: 0 additions & 6 deletions crates/openshell-core/src/sandbox_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,6 @@ pub const SANDBOX_ID: &str = "OPENSHELL_SANDBOX_ID";
/// Filesystem path to the UNIX socket used for the in-sandbox SSH server.
pub const SSH_SOCKET_PATH: &str = "OPENSHELL_SSH_SOCKET_PATH";

/// Shared secret used for HMAC-based SSH handshake authentication.
pub const SSH_HANDSHAKE_SECRET: &str = "OPENSHELL_SSH_HANDSHAKE_SECRET";

/// Allowed clock-skew tolerance in seconds for the SSH handshake.
pub const SSH_HANDSHAKE_SKEW_SECS: &str = "OPENSHELL_SSH_HANDSHAKE_SKEW_SECS";

/// Log level for the sandbox supervisor (e.g. `"debug"`, `"info"`, `"warn"`).
pub const LOG_LEVEL: &str = "OPENSHELL_LOG_LEVEL";

Expand Down
8 changes: 0 additions & 8 deletions crates/openshell-driver-docker/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,14 +395,6 @@ fn build_environment_sets_docker_tls_paths() {
assert!(env.contains(&"TEMPLATE_ENV=template".to_string()));
assert!(env.contains(&"SPEC_ENV=spec".to_string()));
assert!(env.contains(&"OPENSHELL_SANDBOX_COMMAND=sleep infinity".to_string()));
assert!(
!env.iter()
.any(|entry| entry.starts_with("OPENSHELL_SSH_HANDSHAKE_SECRET="))
);
assert!(
!env.iter()
.any(|entry| entry.starts_with("OPENSHELL_SSH_HANDSHAKE_SKEW_SECS="))
);
}

#[test]
Expand Down
2 changes: 0 additions & 2 deletions crates/openshell-driver-kubernetes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,6 @@ pub struct KubernetesComputeConfig {
pub supervisor_sideload_method: SupervisorSideloadMethod,
pub grpc_endpoint: String,
pub ssh_socket_path: String,
pub ssh_handshake_secret: String,
pub ssh_handshake_skew_secs: u64,
pub client_tls_secret_name: String,
pub host_gateway_ip: String,
pub enable_user_namespaces: bool,
Expand Down
59 changes: 0 additions & 59 deletions crates/openshell-driver-kubernetes/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,6 @@ impl KubernetesComputeDriver {
&self.config.ssh_socket_path
}

pub const fn ssh_handshake_skew_secs(&self) -> u64 {
self.config.ssh_handshake_skew_secs
}

fn watch_api(&self) -> Api<DynamicObject> {
let gvk = GroupVersionKind::gvk(SANDBOX_GROUP, SANDBOX_VERSION, SANDBOX_KIND);
let resource = ApiResource::from_gvk(&gvk);
Expand Down Expand Up @@ -296,10 +292,6 @@ impl KubernetesComputeDriver {
}
}

fn ssh_handshake_secret(&self) -> &str {
&self.config.ssh_handshake_secret
}

pub async fn create_sandbox(&self, sandbox: &Sandbox) -> Result<(), KubernetesDriverError> {
let name = sandbox.name.as_str();
info!(
Expand Down Expand Up @@ -328,8 +320,6 @@ impl KubernetesComputeDriver {
sandbox_name: &sandbox.name,
grpc_endpoint: &self.config.grpc_endpoint,
ssh_socket_path: self.ssh_socket_path(),
ssh_handshake_secret: self.ssh_handshake_secret(),
ssh_handshake_skew_secs: self.ssh_handshake_skew_secs(),
client_tls_secret_name: &self.config.client_tls_secret_name,
host_gateway_ip: &self.config.host_gateway_ip,
enable_user_namespaces: self.config.enable_user_namespaces,
Expand Down Expand Up @@ -984,8 +974,6 @@ struct SandboxPodParams<'a> {
sandbox_name: &'a str,
grpc_endpoint: &'a str,
ssh_socket_path: &'a str,
ssh_handshake_secret: &'a str,
ssh_handshake_skew_secs: u64,
client_tls_secret_name: &'a str,
host_gateway_ip: &'a str,
enable_user_namespaces: bool,
Expand Down Expand Up @@ -1146,8 +1134,6 @@ fn sandbox_template_to_k8s(
params.sandbox_name,
params.grpc_endpoint,
params.ssh_socket_path,
params.ssh_handshake_secret,
params.ssh_handshake_skew_secs,
!params.client_tls_secret_name.is_empty(),
);

Expand Down Expand Up @@ -1310,8 +1296,6 @@ fn build_env_list(
sandbox_name: &str,
grpc_endpoint: &str,
ssh_socket_path: &str,
ssh_handshake_secret: &str,
ssh_handshake_skew_secs: u64,
tls_enabled: bool,
) -> Vec<serde_json::Value> {
let mut env = existing_env.cloned().unwrap_or_default();
Expand All @@ -1323,8 +1307,6 @@ fn build_env_list(
sandbox_name,
grpc_endpoint,
ssh_socket_path,
ssh_handshake_secret,
ssh_handshake_skew_secs,
tls_enabled,
);
env
Expand All @@ -1341,15 +1323,12 @@ fn apply_env_map(

// Required env vars are passed individually for clarity at call sites; grouping into a struct
// would not improve readability for this internal helper.
#[allow(clippy::too_many_arguments)]
fn apply_required_env(
env: &mut Vec<serde_json::Value>,
sandbox_id: &str,
sandbox_name: &str,
grpc_endpoint: &str,
ssh_socket_path: &str,
ssh_handshake_secret: &str,
ssh_handshake_skew_secs: u64,
tls_enabled: bool,
) {
upsert_env(env, openshell_core::sandbox_env::SANDBOX_ID, sandbox_id);
Expand All @@ -1367,16 +1346,6 @@ fn apply_required_env(
ssh_socket_path,
);
}
upsert_env(
env,
openshell_core::sandbox_env::SSH_HANDSHAKE_SECRET,
ssh_handshake_secret,
);
upsert_env(
env,
openshell_core::sandbox_env::SSH_HANDSHAKE_SKEW_SECS,
&ssh_handshake_skew_secs.to_string(),
);
// TLS cert paths for sandbox-to-server mTLS. Only set when TLS is enabled
// and the client TLS secret is mounted into the sandbox pod.
if tls_enabled {
Expand Down Expand Up @@ -1534,32 +1503,6 @@ mod tests {
use super::*;
use prost_types::{Struct, Value, value::Kind};

#[test]
fn apply_required_env_always_injects_ssh_handshake_secret() {
let mut env = Vec::new();
apply_required_env(
&mut env,
"sandbox-1",
"my-sandbox",
"https://endpoint:8080",
"0.0.0.0:2222",
"my-secret-value",
300,
true,
);

let secret_entry = env
.iter()
.find(|e| {
e.get("name").and_then(|v| v.as_str()) == Some("OPENSHELL_SSH_HANDSHAKE_SECRET")
})
.expect("OPENSHELL_SSH_HANDSHAKE_SECRET must be present in env");
assert_eq!(
secret_entry.get("value").and_then(|v| v.as_str()),
Some("my-secret-value")
);
}

#[test]
fn supervisor_sideload_injects_run_as_user_zero() {
let mut pod_template = serde_json::json!({
Expand Down Expand Up @@ -1790,8 +1733,6 @@ mod tests {
"my-sandbox",
"https://endpoint:8080",
"0.0.0.0:2222",
"secret",
300,
true, // tls_enabled
);

Expand Down
8 changes: 0 additions & 8 deletions crates/openshell-driver-kubernetes/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,6 @@ struct Args {
)]
sandbox_ssh_socket_path: String,

#[arg(long, env = "OPENSHELL_SSH_HANDSHAKE_SECRET")]
ssh_handshake_secret: String,

#[arg(long, env = "OPENSHELL_SSH_HANDSHAKE_SKEW_SECS", default_value_t = 300)]
ssh_handshake_skew_secs: u64,

#[arg(long, env = "OPENSHELL_CLIENT_TLS_SECRET_NAME")]
client_tls_secret_name: Option<String>,

Expand Down Expand Up @@ -96,8 +90,6 @@ async fn main() -> Result<()> {
supervisor_sideload_method: args.supervisor_sideload_method,
grpc_endpoint: args.grpc_endpoint.unwrap_or_default(),
ssh_socket_path: args.sandbox_ssh_socket_path,
ssh_handshake_secret: args.ssh_handshake_secret,
ssh_handshake_skew_secs: args.ssh_handshake_skew_secs,
client_tls_secret_name: args.client_tls_secret_name.unwrap_or_default(),
host_gateway_ip: args.host_gateway_ip.unwrap_or_default(),
enable_user_namespaces: args.enable_user_namespaces,
Expand Down
2 changes: 1 addition & 1 deletion crates/openshell-driver-podman/NETWORKING.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ bind-mounted into sandbox containers by the Podman driver.
| DNS | Kubernetes CoreDNS | Podman bridge DNS through aardvark-dns when DNS is enabled. |
| Network policy | Kubernetes network policy for pod ingress plus supervisor policy | iptables inside inner sandbox netns plus supervisor policy. |
| Supervisor delivery | Kubernetes driver managed pod image or template | OCI image volume mount. |
| Secrets | Kubernetes Secret volume and env vars | Podman `secret_env` for handshake secret, plus mounted TLS files. |
| Secrets | Kubernetes Secret volume and env vars | Mounted TLS client materials from a Podman secret. |

Both drivers use the same reverse gRPC relay for SSH transport. The most
important Podman-specific difference is network reachability: in rootless
Expand Down
30 changes: 10 additions & 20 deletions crates/openshell-driver-podman/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,12 +197,13 @@ The standalone `openshell-driver-podman` binary sets the same struct field from

## Credential Injection

The SSH handshake secret is injected via Podman's `secret_env` API rather than
a plaintext environment variable.
Sandboxes authenticate to the gateway via mTLS using client materials bind-
mounted into the container from a Podman secret. No shared per-request secret
is injected as an environment variable.

| Credential | Mechanism | Visible in `inspect`? | Visible in `/proc/<pid>/environ`? |
|---|---|---|---|
| SSH handshake secret | Podman `secret_env`, created via secrets API and referenced by name | No | Yes, supervisor only, scrubbed from children |
| mTLS client cert/key | Bind-mounted file paths (`OPENSHELL_TLS_*` env vars point at them) | Yes (paths only) | Yes (paths only) |
| Sandbox identity | Plaintext env var | Yes | Yes |
| gRPC endpoint | Plaintext env var, override-protected | Yes | Yes |
| Supervisor relay socket path | Plaintext env var, override-protected | Yes | Yes |
Expand All @@ -215,13 +216,9 @@ via sandbox templates:
- `OPENSHELL_SANDBOX_ID`
- `OPENSHELL_ENDPOINT`
- `OPENSHELL_SSH_SOCKET_PATH`
- `OPENSHELL_SSH_HANDSHAKE_SKEW_SECS`
- `OPENSHELL_CONTAINER_IMAGE`
- `OPENSHELL_SANDBOX_COMMAND`

The `PodmanComputeConfig::Debug` implementation redacts the handshake secret as
`[REDACTED]`.

## Sandbox Lifecycle

### Creation Flow
Expand All @@ -239,26 +236,23 @@ sequenceDiagram
D->>P: pull_image(supervisor, "missing")
D->>P: pull_image(sandbox_image, policy)

D->>P: create_secret(handshake)
Note over D: On failure below, rollback secret

D->>P: create_volume(workspace)
Note over D: On failure below, rollback volume + secret
Note over D: On failure below, rollback volume

D->>P: create_container(spec)
alt Conflict (409)
D->>P: remove_volume + remove_secret
D->>P: remove_volume
D-->>GW: AlreadyExists
end
Note over D: On failure below, rollback container + volume + secret
Note over D: On failure below, rollback container + volume

D->>P: start_container
D-->>GW: Ok
```

Each step rolls back previously-created resources on failure. The Conflict path
cleans up the volume and secret because they are keyed by the new sandbox's ID,
not the conflicting container's ID.
cleans up the volume because it is keyed by the new sandbox's ID, not the
conflicting container's ID.

### Readiness and Health

Expand All @@ -281,11 +275,9 @@ the socket without the old marker or published-port signal.
4. Force-remove the container.
5. Remove workspace volume derived from the request `sandbox_id`, warning on
failure and continuing.
6. Remove handshake secret derived from the request `sandbox_id`, warning on
failure and continuing.

If the container is already gone during inspect or remove, the driver still
performs idempotent volume and secret cleanup using the request `sandbox_id` and
performs idempotent volume cleanup using the request `sandbox_id` and
returns `Ok(false)` for the container-delete result. This prevents leaked
Podman resources after out-of-band container removal or label drift.

Expand All @@ -300,8 +292,6 @@ Podman resources after out-of-band container removal or label drift.
| `OPENSHELL_GATEWAY_PORT` | `--gateway-port` | `8080` | Gateway port used for endpoint auto-detection by the standalone binary. |
| `OPENSHELL_NETWORK_NAME` | `--network-name` | `openshell` | Podman bridge network name. |
| `OPENSHELL_SANDBOX_SSH_PORT` | `--sandbox-ssh-port` | `2222` | SSH compatibility port inside the container. |
| `OPENSHELL_SSH_HANDSHAKE_SECRET` | `--ssh-handshake-secret` | Required standalone, gateway-generated in-process | Shared secret for the NSSH1 handshake. |
| `OPENSHELL_SSH_HANDSHAKE_SKEW_SECS` | `--ssh-handshake-skew-secs` | `300` | Allowed timestamp skew for SSH handshake validation. |
| `OPENSHELL_SANDBOX_SSH_SOCKET_PATH` | `--sandbox-ssh-socket-path` | `/run/openshell/ssh.sock` | Standalone driver only: supervisor Unix socket path in `PodmanComputeConfig`. In-gateway Podman uses server `config.sandbox_ssh_socket_path`. |
| `OPENSHELL_STOP_TIMEOUT` | `--stop-timeout` | `10` | Container stop timeout in seconds. |
| `OPENSHELL_SUPERVISOR_IMAGE` | `--supervisor-image` | `openshell/supervisor:latest` through the gateway, required standalone | OCI image containing the supervisor binary. |
Expand Down
Loading
Loading