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: 7 additions & 1 deletion .agents/skills/helm-dev-environment/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ The Skaffold flow builds distinct `gateway`, `sandbox`, and `supervisor` images
and deploys the OpenShell Helm chart. The Kubernetes driver creates a
capability-free workload Pod and a directly managed capability-free supervisor
Pod. One namespace-wide NetworkPolicy denies direct egress from every OpenShell
workload Pod. The
workload Pod. New sandbox generations use `runtime-initiated` control: the sandbox
runtime dials its supervisor Pod, and the driver adds a per-sandbox
`os-control-<id>` NetworkPolicy for that path only. Existing running generations
keep their connection until replaced; stop/start and gateway recovery that
recreates both Pods upgrade legacy generations automatically. Use compatible
gateway, supervisor, and sandbox runtime images together; there is no legacy
rollout flag. The
`pkiInitJob` hook (a pre-install Job that runs `openshell-gateway generate-certs`)
generates gateway and CLI TLS secrets on first install. Supervisor Pods project
only `ca.crt` and authenticate gateway RPCs with sandbox bearer tokens. User
Expand Down
19 changes: 19 additions & 0 deletions crates/openshell-core/src/isolation_registration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ pub enum SandboxTransport {
guest_cid: u32,
port: u32,
},
/// The supervisor accepts a connection from its sandbox runtime. TLS and
/// gRPC roles stay unchanged.
RuntimeInitiatedTcp {
authority: String,
listen_address: SocketAddr,
},
}

impl SandboxTransport {
Expand All @@ -45,6 +51,10 @@ impl SandboxTransport {
.iter()
.all(|address| address.port() != 0 && !address.ip().is_unspecified())
}
Self::RuntimeInitiatedTcp {
authority,
listen_address,
} => !authority.is_empty() && listen_address.port() != 0,
Self::Vsock { guest_cid, port } => {
*guest_cid >= 3 && *guest_cid != u32::MAX && *port != 0 && *port != u32::MAX
}
Expand Down Expand Up @@ -82,6 +92,12 @@ pub fn validate_registrations(registrations: &[BackendRegistration]) -> Result<(
let mut names = BTreeSet::new();
for registration in registrations {
validate_backend_name(&registration.name)?;
if matches!(
registration.endpoint,
SandboxTransport::RuntimeInitiatedTcp { .. }
) {
return Err("runtime-initiated control is not a registered backend endpoint");
}
if !names.insert(&registration.name) {
return Err("duplicate isolation backend registration");
}
Expand All @@ -101,6 +117,9 @@ impl TryFrom<&BackendRegistration> for crate::proto::compute::v1::IsolationBacke
validate_backend_name(&registration.name)?;
registration.endpoint.validate()?;
let endpoint = match &registration.endpoint {
SandboxTransport::RuntimeInitiatedTcp { .. } => {
return Err("runtime-initiated control is not a registered backend endpoint");
}
SandboxTransport::Unix { socket_path } => Endpoint::UnixSocketPath(
socket_path
.to_str()
Expand Down
55 changes: 54 additions & 1 deletion crates/openshell-driver-kubernetes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,67 @@ them. The cluster CNI must enforce ingress and egress `NetworkPolicy` for every
sandbox namespace. Kubernetes accepts policy objects without confirming
enforcement, so operators must verify CNI support before running sandboxes.

### Control connection direction

Every new runtime generation uses `runtime-initiated` control. The Sandbox
annotation `openshell.ai/sandbox-runtime-control-connection` records the current
generation's mode. A missing annotation identifies a legacy,
`supervisor-initiated` generation, which keeps working until replaced. Stop/start
upgrades it automatically: with both old Pods removed, the driver changes the
Service selector in place, preserves its ClusterIP, and records the new mode
before releasing the replacement workload. Gateway recovery that recreates both
Pods also upgrades the generation. Reconciliation never migrates a running
generation. An unknown annotation value fails closed; there is no rollout flag.

Only the dial direction differs. In both modes the sandbox runtime presents the
generation-pinned TLS server identity and serves gRPC, and the supervisor
verifies that identity, acts as the gRPC client, and sends the Sandbox Protocol
JWT. The per-sandbox Service `os-boundary-<id>` keeps one stable ClusterIP for
the sandbox's lifetime; it selects the paired workload Pod for
`supervisor-initiated` and the paired supervisor Pod for `runtime-initiated`.

For `runtime-initiated`:

- The bootstrap Secret gives the runtime the Service ClusterIP and port. The
runtime retries with capped backoff until the supervisor listens, so neither
Pod waits on the other during bootstrap. The Service sets
`publishNotReadyAddresses` because the supervisor becomes Ready only after
the runtime attaches through it. The workload still cannot reach the control
port.
- A per-sandbox `NetworkPolicy` `os-control-<id>` selects only this sandbox's
two Pods and admits one flow, from its workload Pod to its supervisor Pod on
`boundary_port` over TCP, as workload egress and supervisor ingress.
Supervisors may already be ingress-isolated by the managed SSH ingress policy;
when that policy is disabled, this one isolates them. Exec, file sync, and
logs need no other supervisor ingress because the supervisor opens those
connections. The CNI must still allow kubelet readiness probes. The
namespace-wide policies are unchanged.
- The policy's UID and resourceVersion are bound into the boundary resource
claims and outer-fence evidence. Reconciliation suspends the workload if the
policy disappears, diverges, or changes generation, or if the boundary
Service no longer selects the role the recorded mode requires.
- The Service and control policy are owned by the Sandbox resource and are
deleted only with that sandbox.

Deploy a gateway, supervisor image, and sandbox runtime image that all support
`runtime-initiated` before using it. Older binaries reject the new descriptor
shapes and fail closed.

Rollback is unsupported for `runtime-initiated` sandboxes. Already running Pods
keep their established connection, but older binaries cannot start or restart
these sandboxes: an older driver ignores the recorded mode while the boundary
Service selects the supervisor, and older supervisors and runtimes reject the
new descriptor variants. Delete them before rolling back, or roll forward.

Each sandbox generation uses two immutable bootstrap Secrets. A trusted init
container stages the sandbox bootstrap into memory, and the sandbox removes it
before starting untrusted code. The other Secret is mounted only by the
supervisor; when `proxy_ca_bundle` is configured it also carries the operator's
corporate proxy CA bundle, which the gateway reads from its own filesystem so
the anchor stays in the gateway's trust domain rather than the sandbox
namespace. The TLS channel binds the namespace, Sandbox CR, workload Pod,
supervisor Pod, and shared network-policy identities. Stop deletes the workload
supervisor Pod, and shared network-policy identities, plus the per-sandbox
control policy for `runtime-initiated` sandboxes. Stop deletes the workload
and supervisor Pods. Start rotates both Secrets and creates a new supervisor
Pod before releasing a new workload Pod. It also renders the trusted init
container again from gateway config, so a sandbox created by an older gateway
Expand Down
43 changes: 42 additions & 1 deletion crates/openshell-driver-kubernetes/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,52 @@ pub const DEFAULT_SANDBOX_SERVICE_ACCOUNT_NAME: &str = "default";
/// Default storage size for the workspace PVC.
pub const DEFAULT_WORKSPACE_STORAGE_SIZE: &str = "2Gi";

/// Which side opens the Sandbox Protocol TCP connection.
///
/// Internal state for the current runtime generation. A missing Sandbox
/// annotation identifies a legacy generation; every new generation uses
/// `RuntimeInitiated`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum KubernetesControlConnection {
/// The sandbox runtime dials its supervisor's per-sandbox Service.
#[default]
RuntimeInitiated,
/// The supervisor dials the sandbox runtime's listener (original mode).
SupervisorInitiated,
}

impl KubernetesControlConnection {
/// Stable spelling used in configuration and Sandbox annotations.
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::RuntimeInitiated => "runtime-initiated",
Self::SupervisorInitiated => "supervisor-initiated",
}
}
}

impl FromStr for KubernetesControlConnection {
type Err = String;

fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"runtime-initiated" => Ok(Self::RuntimeInitiated),
"supervisor-initiated" => Ok(Self::SupervisorInitiated),
other => Err(format!(
"invalid sandbox control connection '{other}'; expected runtime-initiated or supervisor-initiated"
)),
}
}
}

/// Driver-owned requirements for the Kubernetes sandbox runtime.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct KubernetesSandboxRuntimeConfig {
/// TCP port exposed by the workload boundary to its paired control pod.
/// TCP port of the Sandbox Protocol listener: the workload boundary for
/// supervisor-initiated sandboxes, the supervisor for runtime-initiated ones.
pub boundary_port: u16,
}

Expand Down
Loading
Loading