diff --git a/architecture/sandbox.md b/architecture/sandbox.md index fde042b6af..35f9993586 100644 --- a/architecture/sandbox.md +++ b/architecture/sandbox.md @@ -532,3 +532,11 @@ engine with a gateway policy revision. reports its normalized exit code before shutdown. The gateway persists the code on sandbox status, records `MainProcessExited`, and makes the sandbox terminal `Error`; runtime restart policies must not replace the process. + +### Kubernetes volume ownership + +The Kubernetes driver applies the sandbox group with `OnRootMismatch`. Initial +volume preparation remains enabled; repeated CSI republish does not recursively +replace application-managed groups after the volume root is prepared. This keeps +long-running workloads with distinct internal users usable without granting those +users one another's groups. diff --git a/crates/openshell-driver-kubernetes/README.md b/crates/openshell-driver-kubernetes/README.md index 6d6e26654c..a72c53e29b 100644 --- a/crates/openshell-driver-kubernetes/README.md +++ b/crates/openshell-driver-kubernetes/README.md @@ -253,3 +253,13 @@ openshell sandbox create \ }' \ -- claude ``` + +## Persistent volume ownership + +Sandbox pods pair `fsGroup` with `fsGroupChangePolicy: OnRootMismatch`. +Kubelet prepares a volume whose root lacks the sandbox group or required mode, +then leaves an initialized volume unchanged on subsequent mounts and CSI +republish. Workloads may therefore use distinct identities within their workspace +without periodic recursive group changes revoking access during execution. +Applications remain responsible for their internal file permissions; changing the +volume root's group or required mode can cause kubelet to prepare it again. diff --git a/crates/openshell-driver-kubernetes/src/driver.rs b/crates/openshell-driver-kubernetes/src/driver.rs index c63611aab5..5d733f2a32 100644 --- a/crates/openshell-driver-kubernetes/src/driver.rs +++ b/crates/openshell-driver-kubernetes/src/driver.rs @@ -3246,6 +3246,11 @@ fn apply_supervisor_sidecar_topology( .or_insert_with(|| serde_json::json!({})); if let Some(sc) = pod_security_context.as_object_mut() { sc.insert("fsGroup".to_string(), serde_json::json!(params.sandbox_gid)); + // CSI republish must not rewrite ownership chosen by a running workload. + sc.insert( + "fsGroupChangePolicy".to_string(), + serde_json::json!("OnRootMismatch"), + ); } spec.insert("shareProcessNamespace".to_string(), serde_json::json!(true)); @@ -3462,6 +3467,10 @@ fn apply_workspace_persistence( .or_insert_with(|| serde_json::json!({})); if let Some(pod_sc_obj) = pod_sc.as_object_mut() { pod_sc_obj.insert("fsGroup".to_string(), serde_json::json!(sandbox_gid)); + pod_sc_obj.insert( + "fsGroupChangePolicy".to_string(), + serde_json::json!("OnRootMismatch"), + ); } // 1. Add workspace volume mount to the agent container @@ -6520,6 +6529,10 @@ mod tests { assert_eq!(pod_template["spec"]["shareProcessNamespace"], true); assert_eq!(pod_template["spec"]["securityContext"]["fsGroup"], 1500); + assert_eq!( + pod_template["spec"]["securityContext"]["fsGroupChangePolicy"], + "OnRootMismatch" + ); let containers = pod_template["spec"]["containers"].as_array().unwrap(); assert_eq!(containers.len(), 2); @@ -7447,6 +7460,11 @@ mod tests { }); apply_workspace_persistence(&mut pod_template, "img:latest", "Always", 1000); + assert_eq!(pod_template["spec"]["securityContext"]["fsGroup"], 1000); + assert_eq!( + pod_template["spec"]["securityContext"]["fsGroupChangePolicy"], + "OnRootMismatch" + ); let cmd = pod_template["spec"]["initContainers"][0]["command"] .as_array()