Skip to content

Commit ec5655e

Browse files
committed
feat(sandbox): add Platform network mode for restricted K8s platforms
Add NetworkMode::Platform that enables the OpenShell supervisor to run without any elevated capabilities on Kubernetes platforms enforcing the restricted Pod Security Standard (e.g. OpenShift restricted-v2 SCC). Platform Mode keeps Landlock filesystem isolation, seccomp syscall filtering, OPA policy evaluation, credential injection, and L7 inspection via a loopback CONNECT proxy. It replaces the network namespace (which requires CAP_SYS_ADMIN + CAP_NET_ADMIN) with Kubernetes NetworkPolicy for L3/L4 egress control. Changes: - proto: add NetworkEnforcementMode enum to SandboxPolicy (field 6) and DriverSandboxSpec (field 12), backward-compatible - sandbox: add Platform variant to NetworkMode, wire TryFrom conversion - sandbox: skip netns, bind proxy to loopback (127.0.0.1:3128) - sandbox: allow AF_INET sockets in seccomp for Platform mode - driver-k8s: zero capabilities (drop ALL), typed enum comparison - server: propagate network_enforcement to DriverSandboxSpec Ref: NVIDIA#899 Signed-off-by: Ladislav Smola <lsmola@redhat.com>
1 parent 69d9e06 commit ec5655e

10 files changed

Lines changed: 220 additions & 119 deletions

File tree

crates/openshell-driver-kubernetes/src/driver.rs

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,10 @@ impl KubernetesComputeDriver {
330330
enable_user_namespaces: self.config.enable_user_namespaces,
331331
workspace_default_storage_size: &self.config.workspace_default_storage_size,
332332
sa_token_ttl_secs: self.config.effective_sa_token_ttl_secs(),
333+
is_platform_mode: sandbox
334+
.spec
335+
.as_ref()
336+
.is_some_and(|s| s.network_enforcement == 1),
333337
};
334338
obj.data = sandbox_to_k8s_spec(sandbox.spec.as_ref(), &params);
335339
let api = self.api();
@@ -823,6 +827,7 @@ fn apply_supervisor_sideload(
823827
supervisor_image: &str,
824828
supervisor_image_pull_policy: &str,
825829
method: SupervisorSideloadMethod,
830+
is_platform_mode: bool,
826831
) {
827832
let Some(spec) = pod_template.get_mut("spec").and_then(|v| v.as_object_mut()) else {
828833
return;
@@ -882,16 +887,16 @@ fn apply_supervisor_sideload(
882887
serde_json::json!([format!("{}/openshell-sandbox", SUPERVISOR_MOUNT_PATH)]),
883888
);
884889

885-
// Force the supervisor to run as root (UID 0). Sandbox images may set
886-
// a non-root USER directive (e.g. `USER sandbox`), but the supervisor
887-
// needs root to create network namespaces, set up the proxy, and
888-
// configure Landlock/seccomp. The supervisor itself drops privileges
889-
// for child processes via the policy's `run_as_user`/`run_as_group`.
890-
let security_context = container
891-
.entry("securityContext")
892-
.or_insert_with(|| serde_json::json!({}));
893-
if let Some(sc) = security_context.as_object_mut() {
894-
sc.insert("runAsUser".to_string(), serde_json::json!(0));
890+
// In namespace mode, force root (UID 0) so the supervisor can create
891+
// network namespaces and drop privileges for child processes.
892+
// In platform mode, keep the image's default non-root user.
893+
if !is_platform_mode {
894+
let security_context = container
895+
.entry("securityContext")
896+
.or_insert_with(|| serde_json::json!({}));
897+
if let Some(sc) = security_context.as_object_mut() {
898+
sc.insert("runAsUser".to_string(), serde_json::json!(0));
899+
}
895900
}
896901

897902
// Add volume mount
@@ -1044,6 +1049,10 @@ struct SandboxPodParams<'a> {
10441049
/// Lifetime (seconds) of the projected `ServiceAccount` token used
10451050
/// for the bootstrap `IssueSandboxToken` exchange.
10461051
sa_token_ttl_secs: i64,
1052+
/// Platform network enforcement mode (Issue #899). When true, sandbox
1053+
/// pods are emitted without elevated capabilities, compatible with
1054+
/// restricted-v2 SCC and restricted Pod Security Standard.
1055+
is_platform_mode: bool,
10471056
}
10481057

10491058
impl Default for SandboxPodParams<'_> {
@@ -1065,6 +1074,7 @@ impl Default for SandboxPodParams<'_> {
10651074
enable_user_namespaces: false,
10661075
workspace_default_storage_size: DEFAULT_WORKSPACE_STORAGE_SIZE,
10671076
sa_token_ttl_secs: 3600,
1077+
is_platform_mode: false,
10681078
}
10691079
}
10701080
}
@@ -1265,22 +1275,32 @@ fn sandbox_template_to_k8s(
12651275

12661276
container.insert("env".to_string(), serde_json::Value::Array(env));
12671277

1268-
let mut capabilities: Vec<&str> = vec!["SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE", "SYSLOG"];
1269-
if use_user_namespaces {
1270-
// In a user namespace the bounding set is reset. SETUID/SETGID are
1271-
// needed for the supervisor to drop privileges to the sandbox user.
1272-
// DAC_READ_SEARCH is needed for cross-UID /proc/<pid>/fd/ access
1273-
// for process identity resolution in network policy enforcement.
1274-
capabilities.extend(["SETUID", "SETGID", "DAC_READ_SEARCH"]);
1278+
if params.is_platform_mode {
1279+
// Platform mode: zero elevated capabilities. Compatible with
1280+
// restricted-v2 SCC and restricted Pod Security Standard.
1281+
container.insert(
1282+
"securityContext".to_string(),
1283+
serde_json::json!({
1284+
"allowPrivilegeEscalation": false,
1285+
"capabilities": {
1286+
"drop": ["ALL"]
1287+
}
1288+
}),
1289+
);
1290+
} else {
1291+
let mut capabilities: Vec<&str> = vec!["SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE", "SYSLOG"];
1292+
if use_user_namespaces {
1293+
capabilities.extend(["SETUID", "SETGID", "DAC_READ_SEARCH"]);
1294+
}
1295+
container.insert(
1296+
"securityContext".to_string(),
1297+
serde_json::json!({
1298+
"capabilities": {
1299+
"add": capabilities
1300+
}
1301+
}),
1302+
);
12751303
}
1276-
container.insert(
1277-
"securityContext".to_string(),
1278-
serde_json::json!({
1279-
"capabilities": {
1280-
"add": capabilities
1281-
}
1282-
}),
1283-
);
12841304

12851305
// Mount client TLS secret for mTLS to the server, plus the projected
12861306
// ServiceAccount token used to bootstrap the sandbox's gateway JWT
@@ -1363,6 +1383,7 @@ fn sandbox_template_to_k8s(
13631383
params.supervisor_image,
13641384
params.supervisor_image_pull_policy,
13651385
params.supervisor_sideload_method,
1386+
params.is_platform_mode,
13661387
);
13671388

13681389
// Inject workspace persistence (init container + PVC volume mount) so
@@ -1750,6 +1771,7 @@ mod tests {
17501771
"custom-image:latest",
17511772
"IfNotPresent",
17521773
SupervisorSideloadMethod::InitContainer,
1774+
false,
17531775
);
17541776

17551777
let sc = &pod_template["spec"]["containers"][0]["securityContext"];
@@ -1779,6 +1801,7 @@ mod tests {
17791801
"supervisor-image:latest",
17801802
"IfNotPresent",
17811803
SupervisorSideloadMethod::InitContainer,
1804+
false,
17821805
);
17831806

17841807
let sc = &pod_template["spec"]["containers"][0]["securityContext"];
@@ -1804,6 +1827,7 @@ mod tests {
18041827
"supervisor-image:latest",
18051828
"IfNotPresent",
18061829
SupervisorSideloadMethod::InitContainer,
1830+
false,
18071831
);
18081832

18091833
// Volume should be an emptyDir
@@ -1878,6 +1902,7 @@ mod tests {
18781902
"supervisor-image:latest",
18791903
"IfNotPresent",
18801904
SupervisorSideloadMethod::ImageVolume,
1905+
false,
18811906
);
18821907

18831908
let volumes = pod_template["spec"]["volumes"]
@@ -1932,6 +1957,7 @@ mod tests {
19321957
"supervisor-image:latest",
19331958
"",
19341959
SupervisorSideloadMethod::ImageVolume,
1960+
false,
19351961
);
19361962

19371963
let volume = &pod_template["spec"]["volumes"][0];

crates/openshell-policy/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,7 @@ fn to_proto(raw: PolicyFile) -> SandboxPolicy {
378378
run_as_group: p.run_as_group,
379379
}),
380380
network_policies,
381+
network_enforcement: 0,
381382
}
382383
}
383384

@@ -649,6 +650,7 @@ pub fn restrictive_default_policy() -> SandboxPolicy {
649650
run_as_group: "sandbox".into(),
650651
}),
651652
network_policies: HashMap::new(),
653+
network_enforcement: 0, // NAMESPACE (default)
652654
}
653655
}
654656

@@ -1262,6 +1264,7 @@ network_policies:
12621264
filesystem: None,
12631265
landlock: None,
12641266
network_policies: HashMap::new(),
1267+
network_enforcement: 0,
12651268
};
12661269
assert!(validate_sandbox_policy(&policy).is_ok());
12671270
}

0 commit comments

Comments
 (0)