bind a namespaced agent's socket dir off /run so its owner maps to the agent user
This commit is contained in:
parent
23f4f1823b
commit
6847ed865a
4 changed files with 53 additions and 7 deletions
|
|
@ -745,6 +745,10 @@ container lifetime:
|
||||||
on the host before every start; the harness binds its sockets
|
on the host before every start; the harness binds its sockets
|
||||||
there as the agent user. Why the mode matters:
|
there as the agent user. Why the mode matters:
|
||||||
[`boundary.md`](../trust-boundary/boundary.md#the-per-agent-socket-dir).
|
[`boundary.md`](../trust-boundary/boundary.md#the-per-agent-socket-dir).
|
||||||
|
A namespaced agent has the dir bound at `/agents/<name>/sockets` and
|
||||||
|
linked from `/run/hive-agent/<name>`; the activation chowns nothing, because
|
||||||
|
a mount point under `/run` would belong to root and the id mapping would
|
||||||
|
give the agent user no ids there.
|
||||||
|
|
||||||
## Matrix per-agent daemon + token-arrival trigger
|
## Matrix per-agent daemon + token-arrival trigger
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -465,11 +465,7 @@ async fn set_nspawn_flags(
|
||||||
// to the agent user. Don't chown or chmod it here.
|
// to the agent user. Don't chown or chmod it here.
|
||||||
let socket_dir = crate::agent_sockets::agent_dir_for(agent_name);
|
let socket_dir = crate::agent_sockets::agent_dir_for(agent_name);
|
||||||
crate::priv_client::ensure_agent_socket_dir(agent_name, private_users).await?;
|
crate::priv_client::ensure_agent_socket_dir(agent_name, private_users).await?;
|
||||||
binds.push(BindMount {
|
binds.push(socket_bind(agent_name, &socket_dir, private_users));
|
||||||
host_path: socket_dir.to_string_lossy().into_owned(),
|
|
||||||
container_path: socket_dir.to_string_lossy().into_owned(),
|
|
||||||
read_only: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Every container runs in a private network namespace with a veth
|
// Every container runs in a private network namespace with a veth
|
||||||
// pair attached to the host bridge — including the manager (all
|
// pair attached to the host bridge — including the manager (all
|
||||||
|
|
@ -495,6 +491,29 @@ async fn set_nspawn_flags(
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The bind of the agent's socket dir.
|
||||||
|
///
|
||||||
|
/// A namespaced agent's binds are mounted `owneridmap`, which maps the owner of
|
||||||
|
/// the mount point inside the container to the host owner. nspawn mounts a
|
||||||
|
/// fresh tmpfs on `/run` before the binds, so a mount point under `/run` is
|
||||||
|
/// created by nspawn owned by root, and the agent user would have no mapping
|
||||||
|
/// (EOVERFLOW). A namespaced agent gets the dir at `/agents/<name>/sockets`,
|
||||||
|
/// whose mount point hive-priv owns to the agent; the agent module links
|
||||||
|
/// `/run/hive-agent/<name>` to it.
|
||||||
|
fn socket_bind(agent_name: &str, socket_dir: &Path, private_users: Option<bool>) -> BindMount {
|
||||||
|
let host_path = socket_dir.to_string_lossy().into_owned();
|
||||||
|
let container_path = if private_users == Some(true) {
|
||||||
|
format!("/agents/{agent_name}/sockets")
|
||||||
|
} else {
|
||||||
|
host_path.clone()
|
||||||
|
};
|
||||||
|
BindMount {
|
||||||
|
host_path,
|
||||||
|
container_path,
|
||||||
|
read_only: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// What hive-priv is told about the agent's user namespace: the record's flag,
|
/// What hive-priv is told about the agent's user namespace: the record's flag,
|
||||||
/// or `None` (leave the agent as it is) when there is no record or it cannot be
|
/// or `None` (leave the agent as it is) when there is no record or it cannot be
|
||||||
/// read, so a swarm outage never turns a namespaced agent off or on.
|
/// read, so a swarm outage never turns a namespaced agent off or on.
|
||||||
|
|
@ -555,6 +574,21 @@ mod tests {
|
||||||
/// The flag in the record is what hive-priv is told, both ways. No record
|
/// The flag in the record is what hive-priv is told, both ways. No record
|
||||||
/// and an unreadable one say nothing, so a swarm outage changes no agent's
|
/// and an unreadable one say nothing, so a swarm outage changes no agent's
|
||||||
/// namespace; a record from before the field reads false.
|
/// namespace; a record from before the field reads false.
|
||||||
|
/// Only a namespaced agent's socket dir moves off `/run`.
|
||||||
|
#[test]
|
||||||
|
fn the_socket_dir_leaves_run_only_for_a_namespaced_agent() {
|
||||||
|
let dir = std::path::Path::new("/run/hive-agent/iris");
|
||||||
|
for private_users in [None, Some(false)] {
|
||||||
|
let b = super::socket_bind("iris", dir, private_users);
|
||||||
|
assert_eq!(b.container_path, "/run/hive-agent/iris");
|
||||||
|
assert_eq!(b.host_path, "/run/hive-agent/iris");
|
||||||
|
}
|
||||||
|
let b = super::socket_bind("iris", dir, Some(true));
|
||||||
|
assert_eq!(b.container_path, "/agents/iris/sockets");
|
||||||
|
assert_eq!(b.host_path, "/run/hive-agent/iris");
|
||||||
|
assert!(!b.read_only);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_records_flag_is_what_the_start_path_passes() {
|
fn the_records_flag_is_what_the_start_path_passes() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,12 @@ in
|
||||||
"d ${homeDir}/.local/state 0755 ${owner} - -"
|
"d ${homeDir}/.local/state 0755 ${owner} - -"
|
||||||
"L ${homeDir}/.local/state/nix - - - - ${persisted}"
|
"L ${homeDir}/.local/state/nix - - - - ${persisted}"
|
||||||
"L ${homeDir}/.nix-profile - - - - ${homeDir}/.local/state/nix/profiles/profile"
|
"L ${homeDir}/.nix-profile - - - - ${homeDir}/.local/state/nix/profiles/profile"
|
||||||
];
|
]
|
||||||
|
# A namespaced agent's socket dir is bound at /agents/<name>/sockets, not
|
||||||
|
# under /run, whose tmpfs nspawn mounts before the binds and so owns the
|
||||||
|
# mount point to root (hive-c0re `socket_bind`). The harness keeps the
|
||||||
|
# /run/hive-agent path. A dir already bound there is left alone.
|
||||||
|
++ lib.optional config.services.hyperhive.agent.user.privateUsers "L /run/hive-agent/${userName} - - - - /agents/${userName}/sockets";
|
||||||
|
|
||||||
# `NOPASSWD: ALL` for the agent user. Lets claude's Bash tool
|
# `NOPASSWD: ALL` for the agent user. Lets claude's Bash tool
|
||||||
# keep working with anything that expected root (systemctl,
|
# keep working with anything that expected root (systemctl,
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ let
|
||||||
++ [
|
++ [
|
||||||
{
|
{
|
||||||
host_path = nsSocketDir;
|
host_path = nsSocketDir;
|
||||||
container_path = nsSocketDir;
|
container_path = "/agents/${nsAgent}/sockets";
|
||||||
read_only = false;
|
read_only = false;
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
@ -368,6 +368,9 @@ pkgs.testers.runNixOSTest {
|
||||||
assert as_ns(f"cat /agents/{NS_AGENT}/state/pre.txt") == "before"
|
assert as_ns(f"cat /agents/{NS_AGENT}/state/pre.txt") == "before"
|
||||||
assert as_ns(f"stat -c %u:%g /agents/{NS_AGENT}/state/pre.txt") == "1000:1000"
|
assert as_ns(f"stat -c %u:%g /agents/{NS_AGENT}/state/pre.txt") == "1000:1000"
|
||||||
as_ns(f"echo after > /agents/{NS_AGENT}/state/post.txt")
|
as_ns(f"echo after > /agents/{NS_AGENT}/state/post.txt")
|
||||||
|
# The socket dir is bound off /run (an owneridmap mount point under the
|
||||||
|
# container's /run tmpfs would belong to root) and linked back.
|
||||||
|
assert as_ns(f"readlink /run/hive-agent/{NS_AGENT}") == f"/agents/{NS_AGENT}/sockets"
|
||||||
as_ns(f"touch /run/hive-agent/{NS_AGENT}/web.sock")
|
as_ns(f"touch /run/hive-agent/{NS_AGENT}/web.sock")
|
||||||
assert as_ns(f"cat /agents/{NS_AGENT}/config/cfg") == "from-config"
|
assert as_ns(f"cat /agents/{NS_AGENT}/config/cfg") == "from-config"
|
||||||
host.fail(f"nixos-container run {NS} -- runuser -l {NS_AGENT} -c 'touch /agents/{NS_AGENT}/config/new'")
|
host.fail(f"nixos-container run {NS} -- runuser -l {NS_AGENT} -c 'touch /agents/{NS_AGENT}/config/new'")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue