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
|
||||
there as the agent user. Why the mode matters:
|
||||
[`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
|
||||
|
||||
|
|
|
|||
|
|
@ -465,11 +465,7 @@ async fn set_nspawn_flags(
|
|||
// to the agent user. Don't chown or chmod it here.
|
||||
let socket_dir = crate::agent_sockets::agent_dir_for(agent_name);
|
||||
crate::priv_client::ensure_agent_socket_dir(agent_name, private_users).await?;
|
||||
binds.push(BindMount {
|
||||
host_path: socket_dir.to_string_lossy().into_owned(),
|
||||
container_path: socket_dir.to_string_lossy().into_owned(),
|
||||
read_only: false,
|
||||
});
|
||||
binds.push(socket_bind(agent_name, &socket_dir, private_users));
|
||||
|
||||
// Every container runs in a private network namespace with a veth
|
||||
// pair attached to the host bridge — including the manager (all
|
||||
|
|
@ -495,6 +491,29 @@ async fn set_nspawn_flags(
|
|||
.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,
|
||||
/// 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.
|
||||
|
|
@ -555,6 +574,21 @@ mod tests {
|
|||
/// 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
|
||||
/// 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]
|
||||
fn the_records_flag_is_what_the_start_path_passes() {
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -199,7 +199,12 @@ in
|
|||
"d ${homeDir}/.local/state 0755 ${owner} - -"
|
||||
"L ${homeDir}/.local/state/nix - - - - ${persisted}"
|
||||
"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
|
||||
# keep working with anything that expected root (systemctl,
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ let
|
|||
++ [
|
||||
{
|
||||
host_path = nsSocketDir;
|
||||
container_path = nsSocketDir;
|
||||
container_path = "/agents/${nsAgent}/sockets";
|
||||
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"stat -c %u:%g /agents/{NS_AGENT}/state/pre.txt") == "1000:1000"
|
||||
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")
|
||||
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'")
|
||||
|
|
|
|||
Loading…
Reference in a new issue