run opted-in agents in a user namespace with a dedicated data owner
This commit is contained in:
parent
5ae0205e1d
commit
bfdf603d12
1 changed files with 828 additions and 21 deletions
|
|
@ -50,6 +50,9 @@ pub(crate) fn handle_write_nspawn_flags(
|
|||
);
|
||||
false
|
||||
});
|
||||
// After the salvage, so the mount points land in the root the start will
|
||||
// snapshot.
|
||||
let userns = crate::userns::activate(container, agent, binds)?;
|
||||
write_nspawn_flags(
|
||||
container,
|
||||
binds,
|
||||
|
|
@ -57,6 +60,7 @@ pub(crate) fn handle_write_nspawn_flags(
|
|||
load_credentials,
|
||||
&hive_facts,
|
||||
ephemeral,
|
||||
userns,
|
||||
)?;
|
||||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
|
@ -364,6 +368,7 @@ fn write_nspawn_flags(
|
|||
load_credentials: &[CredentialMount],
|
||||
hive_facts: &str,
|
||||
ephemeral: bool,
|
||||
userns: bool,
|
||||
) -> Result<()> {
|
||||
use std::fmt::Write as _;
|
||||
use std::os::unix::fs::MetadataExt as _;
|
||||
|
|
@ -371,21 +376,13 @@ fn write_nspawn_flags(
|
|||
let original = std::fs::read_to_string(&path).with_context(|| format!("read {path}"))?;
|
||||
let lines: Vec<&str> = original
|
||||
.lines()
|
||||
.filter(|line| {
|
||||
let t = line.trim_start();
|
||||
!t.starts_with("EXTRA_NSPAWN_FLAGS=")
|
||||
&& !t.starts_with("PRIVATE_NETWORK=")
|
||||
&& !t.starts_with("HOST_ADDRESS=")
|
||||
&& !t.starts_with("LOCAL_ADDRESS=")
|
||||
&& !t.starts_with("HOST_ADDRESS6=")
|
||||
&& !t.starts_with("LOCAL_ADDRESS6=")
|
||||
&& !t.starts_with("HOST_BRIDGE=")
|
||||
})
|
||||
.filter(|line| !is_owned_var(line))
|
||||
.collect();
|
||||
let mut out = lines.join("\n");
|
||||
if !out.is_empty() {
|
||||
out.push('\n');
|
||||
}
|
||||
out.push_str(private_users_line(userns));
|
||||
{
|
||||
let iso = isolation;
|
||||
out.push_str("PRIVATE_NETWORK=1\n");
|
||||
|
|
@ -406,7 +403,7 @@ fn write_nspawn_flags(
|
|||
out.push_str("LOCAL_ADDRESS6=\n");
|
||||
let _ = writeln!(out, "HOST_BRIDGE={}", iso.bridge);
|
||||
}
|
||||
let flags_joined = nspawn_flags(binds, load_credentials, ephemeral).join(" ");
|
||||
let flags_joined = nspawn_flags(binds, load_credentials, ephemeral, userns).join(" ");
|
||||
let _ = writeln!(out, "EXTRA_NSPAWN_FLAGS=\"{flags_joined}\"");
|
||||
// Keeps the owner and mode `nixos-container` created the file with; this
|
||||
// helper only rewrites its lines.
|
||||
|
|
@ -432,24 +429,57 @@ fn write_nspawn_flags(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Whether a line of the container conf is one `write_nspawn_flags` writes
|
||||
/// itself, and so drops before writing it again. Dropped for every container,
|
||||
/// not only those it writes it for: a `PRIVATE_USERS=pick` left behind would
|
||||
/// keep a container namespaced after it left the list.
|
||||
fn is_owned_var(line: &str) -> bool {
|
||||
let t = line.trim_start();
|
||||
[
|
||||
"EXTRA_NSPAWN_FLAGS=",
|
||||
"PRIVATE_NETWORK=",
|
||||
"PRIVATE_USERS=",
|
||||
"HOST_ADDRESS=",
|
||||
"LOCAL_ADDRESS=",
|
||||
"HOST_ADDRESS6=",
|
||||
"LOCAL_ADDRESS6=",
|
||||
"HOST_BRIDGE=",
|
||||
]
|
||||
.iter()
|
||||
.any(|var| t.starts_with(var))
|
||||
}
|
||||
|
||||
/// The conf line that makes nixos-container pass `--private-users=pick`.
|
||||
fn private_users_line(userns: bool) -> &'static str {
|
||||
if userns { "PRIVATE_USERS=pick\n" } else { "" }
|
||||
}
|
||||
|
||||
/// The flags `EXTRA_NSPAWN_FLAGS` carries, in the order nspawn needs them.
|
||||
///
|
||||
/// `--ephemeral` runs the container on a snapshot of its root and drops the
|
||||
/// snapshot at stop; `crate::rootfs` has made the root fit for that before
|
||||
/// the flag is asked for. The git overlays and credentials follow the binds
|
||||
/// they refer to.
|
||||
///
|
||||
/// `userns` mounts every bind `owneridmap` and idmaps the root instead of
|
||||
/// chowning it, so a different `pick` range never walks the tree.
|
||||
fn nspawn_flags(
|
||||
binds: &[BindMount],
|
||||
load_credentials: &[CredentialMount],
|
||||
ephemeral: bool,
|
||||
userns: bool,
|
||||
) -> Vec<String> {
|
||||
let mut flags: Vec<String> = ephemeral
|
||||
.then(|| "--ephemeral".to_owned())
|
||||
.into_iter()
|
||||
.collect();
|
||||
if userns {
|
||||
flags.push("--private-users-ownership=map".to_owned());
|
||||
}
|
||||
let idmap = if userns { ":owneridmap" } else { "" };
|
||||
flags.extend(binds.iter().map(|b| {
|
||||
let flag = if b.read_only { "--bind-ro" } else { "--bind" };
|
||||
format!("{flag}={}:{}", b.host_path, b.container_path)
|
||||
format!("{flag}={}:{}{idmap}", b.host_path, b.container_path)
|
||||
}));
|
||||
flags.extend(git_overlay_flags(binds));
|
||||
// Credential forwarding: nspawn loads each host secret into the
|
||||
|
|
@ -621,9 +651,9 @@ pub(crate) fn write_container_etc_file_in(rootfs: &Path, name: &str, contents: &
|
|||
mod tests {
|
||||
use super::{
|
||||
BindMount, HiveFacts, NetworkIsolation, git_overlay_flags, handle_write_nspawn_flags,
|
||||
hive_priv_exe, limits_dropin_body, nspawn_flags, publish_file, remove_service_dropin,
|
||||
render_hive_facts, rootfs_check_command, validate_container_system_name,
|
||||
write_bridge_dns_marker_in, write_resource_limits,
|
||||
hive_priv_exe, is_owned_var, limits_dropin_body, nspawn_flags, private_users_line,
|
||||
publish_file, remove_service_dropin, render_hive_facts, rootfs_check_command,
|
||||
validate_container_system_name, write_bridge_dns_marker_in, write_resource_limits,
|
||||
};
|
||||
use crate::shared::test_support::{entries, scratch};
|
||||
|
||||
|
|
@ -1072,14 +1102,55 @@ mod tests {
|
|||
#[test]
|
||||
fn ephemeral_is_passed_only_when_asked() {
|
||||
let binds = [bind("/agents/atlas/work")];
|
||||
let with = nspawn_flags(&binds, &[], true);
|
||||
let with = nspawn_flags(&binds, &[], true, false);
|
||||
assert_eq!(with[0], "--ephemeral", "{with:?}");
|
||||
assert!(with.iter().any(|f| f.starts_with("--bind-ro=")), "{with:?}");
|
||||
let without = nspawn_flags(&binds, &[], false);
|
||||
let without = nspawn_flags(&binds, &[], false, false);
|
||||
assert!(!without.iter().any(|f| f == "--ephemeral"), "{without:?}");
|
||||
assert_eq!(without.len() + 1, with.len());
|
||||
}
|
||||
|
||||
/// Every bind, read-only ones included, is mounted `owneridmap` for a
|
||||
/// namespaced agent and none is for any other; the root is idmapped, not
|
||||
/// chowned. The `false` arm is the control that the lookup finds the binds.
|
||||
#[test]
|
||||
fn binds_are_owneridmap_only_for_a_namespaced_agent() {
|
||||
let binds = [bind("/knowledge"), bind("/agents/a/config")];
|
||||
let plain = nspawn_flags(&binds, &[], false, false);
|
||||
assert!(
|
||||
plain.iter().any(|f| f.starts_with("--bind-ro=")),
|
||||
"{plain:?}"
|
||||
);
|
||||
assert!(
|
||||
plain
|
||||
.iter()
|
||||
.all(|f| !f.contains("idmap") && !f.contains("ownership")),
|
||||
"{plain:?}"
|
||||
);
|
||||
let userns = nspawn_flags(&binds, &[], false, true);
|
||||
assert!(
|
||||
userns.contains(&"--private-users-ownership=map".to_owned()),
|
||||
"{userns:?}"
|
||||
);
|
||||
let mounts: Vec<&String> = userns.iter().filter(|f| f.starts_with("--bind")).collect();
|
||||
assert_eq!(mounts.len(), 2, "{userns:?}");
|
||||
assert!(
|
||||
mounts.iter().all(|f| f.ends_with(":owneridmap")),
|
||||
"{userns:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// `PRIVATE_USERS=` is dropped from the conf whatever the agent now is, so
|
||||
/// leaving the list cannot leave the line behind. `AUTO_START=` is the
|
||||
/// control: a line that is not ours stays.
|
||||
#[test]
|
||||
fn the_private_users_line_is_ours_to_drop_and_to_write() {
|
||||
assert!(is_owned_var("PRIVATE_USERS=pick"));
|
||||
assert!(!is_owned_var("AUTO_START=1"));
|
||||
assert_eq!(private_users_line(true), "PRIVATE_USERS=pick\n");
|
||||
assert_eq!(private_users_line(false), "");
|
||||
}
|
||||
|
||||
/// The swarm's forge and matrix containers are not the hive's: every
|
||||
/// request that names a container as a string refuses them before
|
||||
/// touching the filesystem. Control: an agent container name passes.
|
||||
|
|
|
|||
Loading…
Reference in a new issue