Skip to content

Commit fe3380a

Browse files
committed
fix(podman): support keep-id runtime groups
Signed-off-by: Evan Lezar <elezar@nvidia.com> refactor(podman): generalize keep-id group handling Signed-off-by: Evan Lezar <elezar@nvidia.com>
1 parent 31d2a25 commit fe3380a

5 files changed

Lines changed: 84 additions & 28 deletions

File tree

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,9 @@ impl PodmanComputeDriver {
978978
&workload_id,
979979
&uuid::Uuid::new_v4().to_string(),
980980
&identity,
981+
crate::isolation::userns_preserves_host_groups(
982+
self.config.userns.as_deref(),
983+
),
981984
child_env,
982985
&launch_authentication,
983986
)?;
@@ -1319,6 +1322,7 @@ impl PodmanComputeDriver {
13191322
&container_id,
13201323
generation.as_str(),
13211324
&restart_metadata.workload_identity,
1325+
crate::isolation::userns_preserves_host_groups(self.config.userns.as_deref()),
13221326
restart_metadata.child_env,
13231327
&launch_authentication,
13241328
)?;

crates/openshell-driver-podman/src/isolation.rs

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use openshell_core::proto::compute::v1::DriverSandbox;
1313
use openshell_isolation_interface::contract::{
1414
OuterFenceGuarantee, OuterFenceGuarantees, ResolvedWorkloadIdentity,
1515
};
16+
use openshell_sandbox_backend::ALLOW_EXTRA_SUPPLEMENTARY_GROUPS_RESOURCE_CLAIM;
1617
use openshell_sandbox_backend::boundary_protocol::{
1718
BoundaryConfig, BoundaryListener, GatewayVerificationKey, SandboxRuntimeDescriptor,
1819
SandboxTlsClientConfig, SandboxTlsServerConfig, SandboxTransport,
@@ -70,6 +71,12 @@ pub fn channel_volume_name(id: &str) -> String {
7071
format!("openshell-channel-{id}")
7172
}
7273

74+
/// `keep-id` may retain the gateway user's supplementary groups in the
75+
/// container. Other user-namespace modes, including `auto`, do not.
76+
pub fn userns_preserves_host_groups(userns: Option<&str>) -> bool {
77+
userns.is_some_and(|mode| mode.split(':').next() == Some("keep-id"))
78+
}
79+
7380
fn invalid(error: impl std::fmt::Display) -> ComputeDriverError {
7481
ComputeDriverError::Precondition(error.to_string())
7582
}
@@ -195,19 +202,26 @@ pub fn bootstrap_archives(
195202
container_id: &str,
196203
generation: &str,
197204
identity: &ResolvedWorkloadIdentity,
205+
allow_extra_supplementary_groups: bool,
198206
child_env: HashMap<String, String>,
199207
launch_authentication: &openshell_core::jwt::SandboxLaunchAuthentication,
200208
) -> Result<BootstrapArchives, ComputeDriverError> {
201209
launch_authentication.validate().map_err(invalid)?;
202210
let session_id = launch_authentication.supervisor.session_id;
203211
let tls = generate_sandbox_tls_material(session_id).map_err(invalid)?;
204-
let resource_claims = BTreeMap::from([
212+
let mut resource_claims = BTreeMap::from([
205213
("podman.container_id".into(), container_id.into()),
206214
(
207215
"podman.image_identity".into(),
208216
identity.resource_digest.clone(),
209217
),
210218
]);
219+
if allow_extra_supplementary_groups {
220+
resource_claims.insert(
221+
ALLOW_EXTRA_SUPPLEMENTARY_GROUPS_RESOURCE_CLAIM.into(),
222+
"true".into(),
223+
);
224+
}
211225
let runtime_generation = launch_authentication
212226
.supervisor
213227
.runtime_generation
@@ -496,6 +510,7 @@ mod tests {
496510
"container",
497511
"generation-1",
498512
&identity,
513+
false,
499514
child_env.clone(),
500515
&authentication,
501516
)
@@ -541,6 +556,11 @@ mod tests {
541556
.outer_fence
542557
.validate(&runtime_descriptor.generation)
543558
.unwrap();
559+
assert!(
560+
!config
561+
.resource_claims
562+
.contains_key(ALLOW_EXTRA_SUPPLEMENTARY_GROUPS_RESOURCE_CLAIM)
563+
);
544564
let restart_metadata: RestartMetadata = serde_json::from_slice(
545565
supervisor
546566
.get(&PathBuf::from(
@@ -558,4 +578,15 @@ mod tests {
558578
.any(|window| window == b"PRIVATE KEY")
559579
);
560580
}
581+
582+
#[test]
583+
fn keep_id_is_the_only_userns_mode_that_preserves_host_groups() {
584+
assert!(userns_preserves_host_groups(Some("keep-id")));
585+
assert!(userns_preserves_host_groups(Some(
586+
"keep-id:uid=1000,gid=1000"
587+
)));
588+
assert!(!userns_preserves_host_groups(Some("auto")));
589+
assert!(!userns_preserves_host_groups(Some("private")));
590+
assert!(!userns_preserves_host_groups(None));
591+
}
561592
}

crates/openshell-sandbox-backend/src/lib.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ pub const BACKEND_NAME: &str = "openshell-sandbox";
2121
/// Resource claim set by compute drivers when the workload requests GPU access.
2222
pub const GPU_RESOURCE_CLAIM: &str = "openshell.gpu";
2323

24+
/// Resource claim set when the runtime may retain supplementary groups in
25+
/// addition to the image-derived workload identity.
26+
pub const ALLOW_EXTRA_SUPPLEMENTARY_GROUPS_RESOURCE_CLAIM: &str =
27+
"openshell.identity.allow_extra_supplementary_groups";
28+
2429
/// Memory-backed parent used for supervisor CA material.
2530
pub const SUPERVISOR_CA_RUNTIME_ROOT: &str = "/run/openshell-supervisor-ca";
2631

crates/openshell-sandbox/src/boundary_server.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ mod linux {
3939
BoundaryConfirmation, BoundaryExec, BoundaryLoopbackConnector, BoundaryProcess,
4040
BoundaryTerminal, ExecSession, LoopbackTarget, ResolvedWorkloadIdentity,
4141
};
42-
use openshell_sandbox_backend::GPU_RESOURCE_CLAIM;
4342
use openshell_sandbox_backend::mediation::{
4443
self, DnsQueryWire, MediationFrame, MediationFrameKind,
4544
};
@@ -53,6 +52,9 @@ mod linux {
5352
SandboxConnectionId, SandboxConnectionRegistry, SandboxProtocolAuthenticator,
5453
SandboxProtocolPrincipal,
5554
};
55+
use openshell_sandbox_backend::{
56+
ALLOW_EXTRA_SUPPLEMENTARY_GROUPS_RESOURCE_CLAIM, GPU_RESOURCE_CLAIM,
57+
};
5658
use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _};
5759
use tokio_stream::wrappers::ReceiverStream;
5860

@@ -389,6 +391,10 @@ mod linux {
389391
.resource_claims
390392
.get(GPU_RESOURCE_CLAIM)
391393
.is_some_and(|value| value == "true")
394+
|| config
395+
.resource_claims
396+
.get(ALLOW_EXTRA_SUPPLEMENTARY_GROUPS_RESOURCE_CLAIM)
397+
.is_some_and(|value| value == "true")
392398
}
393399

394400
fn supplementary_groups_match(actual: &[u32], expected: &[u32], allow_extra: bool) -> bool {
@@ -3834,6 +3840,33 @@ mod linux {
38343840
assert!(!supplementary_groups_match(&[44, 992], &[1001], true));
38353841
}
38363842

3843+
#[test]
3844+
fn generic_identity_claim_allows_runtime_supplementary_groups() {
3845+
let config = BoundaryConfig {
3846+
boundary_id: "sandbox-1".to_string(),
3847+
generation: "generation-1".to_string(),
3848+
session_id: test_session_id(),
3849+
session_rotation: openshell_core::jwt::SessionRotation::new(1)
3850+
.expect("session rotation"),
3851+
auth_epoch: CredentialEpoch::new(1).expect("auth epoch"),
3852+
gateway_id: "test-gateway".to_string(),
3853+
verification_keys: vec![],
3854+
listener: BoundaryListenerConfig::Vsock {
3855+
control_port: 5500,
3856+
tls: placeholder_server_tls(),
3857+
},
3858+
resource_claims: std::collections::BTreeMap::from([(
3859+
ALLOW_EXTRA_SUPPLEMENTARY_GROUPS_RESOURCE_CLAIM.to_string(),
3860+
"true".to_string(),
3861+
)]),
3862+
resource_claim_files: std::collections::BTreeMap::new(),
3863+
workload_identity: test_workload_identity(),
3864+
outer_fence: test_outer_fence(),
3865+
child_env: std::collections::HashMap::new(),
3866+
};
3867+
assert!(allows_runtime_supplementary_groups(&config));
3868+
}
3869+
38373870
#[test]
38383871
fn control_connection_slots_bound_authenticated_sessions() {
38393872
let active = Arc::new(AtomicUsize::new(MAX_CONTROL_CONNECTIONS - 1));

tests/suites/drivers/podman/tests/default_userns.rs

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ const PODMAN_TEST_IMAGE_ENV: &str = "OPENSHELL_PODMAN_TEST_IMAGE";
1919
/// Verify that the gateway's user-namespace configuration matches Podman's
2020
/// direct behavior for the same profile.
2121
///
22-
/// The test creates a sandbox and compares its user-namespace mapping with the
23-
/// direct-Podman reference stored at
22+
/// The test runs a short-lived sandbox command and compares its user-namespace
23+
/// mapping with the direct-Podman reference stored at
2424
/// `OPENSHELL_TEST_INPUT_DIR/reference-uid-map`. The tmachine pre-test
2525
/// playbook creates that reference in the same gateway-user context. This deliberately
2626
/// avoids baking a particular Podman mapping into OpenShell's test contract.
@@ -59,38 +59,21 @@ async fn configured_userns_matches_podman_reference() {
5959
if let Some(image) = workload_image.as_deref() {
6060
create_args.extend(["--from", image]);
6161
}
62-
create_args.extend(["--detach", "--", "sleep", "infinity"]);
63-
let create = runner
64-
.step("userns/create")
65-
.description("sandbox from the configured test image is created")
66-
.with_timeout(SANDBOX_TIMEOUT)
67-
.run(&create_args)
68-
.await
69-
.map_err(|error| error.to_string())?;
70-
create.require_success()?;
71-
let exec = runner
62+
create_args.extend(["--no-tty", "--", "cat", "/proc/self/uid_map"]);
63+
let run = runner
7264
.step("userns/uid-map")
7365
.description("sandbox exposes its UID map")
7466
.with_timeout(SANDBOX_TIMEOUT)
75-
.run(&[
76-
"sandbox",
77-
"exec",
78-
"--name",
79-
&sandbox_name,
80-
"--no-tty",
81-
"--",
82-
"cat",
83-
"/proc/self/uid_map",
84-
])
67+
.run(&create_args)
8568
.await
8669
.map_err(|error| error.to_string())?;
87-
exec.require_success()?;
88-
let sandbox_uid_map = normalize_uid_map(exec.stdout()).ok_or_else(|| {
89-
exec.failure_diagnostic("sandbox returns a non-empty UID map")
70+
run.require_success()?;
71+
let sandbox_uid_map = normalize_uid_map(run.stdout()).ok_or_else(|| {
72+
run.failure_diagnostic("sandbox returns a non-empty UID map")
9073
})?;
9174
if sandbox_uid_map != expected_uid_map {
9275
return Err(format!(
93-
"sandbox UID map differs from direct Podman default:\nexpected:\n{expected_uid_map}\nactual:\n{sandbox_uid_map}"
76+
"sandbox UID map differs from the direct Podman reference:\nexpected:\n{expected_uid_map}\nactual:\n{sandbox_uid_map}"
9477
));
9578
}
9679
Ok(())

0 commit comments

Comments
 (0)