Accept dynamic primary group in workload audit

This commit is contained in:
Meta Vens 2026-09-28 15:19:52 +02:00
commit 865190611f
2 changed files with 67 additions and 13 deletions

View file

@ -135,6 +135,13 @@ deterministically lowers the constrained launch contract through the configured
workload driver; repository-owned raw unit or container-runtime text is not an
input.
For systemd `DynamicUser` workloads, systemd's reported primary user and group
names are allocated runtime identities. When a binding declares no fixed state
group, Idunn validates that the process primary UID and GID match instead of
requiring an empty `Group` property. A declared state group remains an exact
name and numeric GID contract because it grants the process its bounded write
lease.
## Operator binding
An operator binding is static root-admitted configuration, not observed state.

View file

@ -2407,6 +2407,18 @@ pub struct SystemdTransientWorkloadDriver {
pub credential_root: PathBuf,
}
fn systemd_group_matches_launch_contract(
expected_group: Option<(&str, u32)>,
observed_group: &str,
process_uid: u32,
process_gid: u32,
) -> bool {
match expected_group {
Some((group, group_id)) => observed_group == group && process_gid == group_id,
None => process_uid == process_gid,
}
}
impl Default for SystemdTransientWorkloadDriver {
fn default() -> Self {
Self {
@ -3342,13 +3354,15 @@ impl SystemdTransientWorkloadDriver {
//
// `systemd_user` is deliberately absent: systemd reports the identity it
// allocated for a DynamicUser unit, so it is never empty for a running
// workload. See the same note in `observe_unit`.
// workload. The same applies to `Group` when no fixed state group is
// bound. In that case the numeric primary GID must match the allocated
// UID; a fixed state group still requires both its exact name and GID.
let expected_load_credential = if regular_credential_names.is_empty() {
""
} else {
"[unprintable]"
};
let launch_contract: [(bool, &str); 28] = [
let launch_contract: [(bool, &str); 27] = [
(
observation.unit_description == expected_description,
"unit description",
@ -3358,10 +3372,12 @@ impl SystemdTransientWorkloadDriver {
(observation.kill_mode == "mixed", "kill mode"),
(observation.dynamic_user, "dynamic user"),
(
match expected_group {
Some(group) => observation.systemd_group == group,
None => observation.systemd_group.is_empty(),
},
systemd_group_matches_launch_contract(
expected_group.zip(expected_group_id),
&observation.systemd_group,
observation.process_uids[0],
observation.process_gids[0],
),
"systemd group",
),
(
@ -3430,13 +3446,6 @@ impl SystemdTransientWorkloadDriver {
observed_credential_names == regular_credential_names,
"credential names",
),
(
match expected_group_id {
Some(group_id) => observation.process_gids[0] == group_id,
None => observation.process_gids[0] == observation.process_uids[0],
},
"process group id",
),
];
for (holds, clause) in launch_contract {
ensure!(
@ -8199,6 +8208,44 @@ mod tests {
}
}
#[test]
fn dynamic_primary_group_is_checked_by_numeric_identity() {
assert!(systemd_group_matches_launch_contract(
None,
"_du12345",
61_000,
61_000
));
assert!(!systemd_group_matches_launch_contract(
None,
"_du12345",
61_000,
61_001
));
}
#[test]
fn fixed_state_group_keeps_its_name_and_gid_contract() {
assert!(systemd_group_matches_launch_contract(
Some(("streampixels-service-state", 61_234)),
"streampixels-service-state",
61_000,
61_234
));
assert!(!systemd_group_matches_launch_contract(
Some(("streampixels-service-state", 61_234)),
"_du12345",
61_000,
61_234
));
assert!(!systemd_group_matches_launch_contract(
Some(("streampixels-service-state", 61_234)),
"streampixels-service-state",
61_000,
61_235
));
}
#[cfg(unix)]
#[test]
fn the_published_projection_is_readable_by_a_workload() {