@@ -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
10491058impl 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 ] ;
0 commit comments