From 21213be1b69b46d7d023eab82455ccf7965f67e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sun, 31 May 2026 22:37:24 +0200 Subject: [PATCH] lifecycle: chown per-agent socket dir to the agent user Replace the 0777 fallback with a chown to the in-container agent's uid/gid (resolvable via agent_uid_gid since nspawn shares uids with the host). Keeps the dir at the default 0755 mode and avoids world-writability. Falls back to 0777 only when the agent uid is unavailable (first-spawn race before /etc/passwd is rendered). --- hive-c0re/src/lifecycle.rs | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/hive-c0re/src/lifecycle.rs b/hive-c0re/src/lifecycle.rs index 969ed166..17ab511b 100644 --- a/hive-c0re/src/lifecycle.rs +++ b/hive-c0re/src/lifecycle.rs @@ -1219,18 +1219,27 @@ fn set_nspawn_flags( let socket_dir = crate::agent_sockets::agent_dir_for(agent_name); std::fs::create_dir_all(&socket_dir) .with_context(|| format!("create {}", socket_dir.display()))?; - // 0777 so the in-container agent user (`sock`, `iris`, …) can - // `bind(2)` the `web.sock` file here. `create_dir_all` lands - // the dir at 0755 root:root; the harness runs as the non-root - // agent user inside the container and otherwise fails the - // bind with EACCES — silently, since web_ui::serve's spawn - // drops its JoinHandle (the gateway then sees an empty - // agent-sockets.json and the agent appears unreachable). - // Per-agent dir + per-agent bind-mount means no other - // container ever sees this path, so the wide mode is contained. - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(&socket_dir, std::fs::Permissions::from_mode(0o777)) - .with_context(|| format!("chmod 0777 {}", socket_dir.display()))?; + // chown to the in-container agent user so its harness can + // `bind(2)` web.sock here. `create_dir_all` lands the dir at + // 0755 root:root and the harness runs as the non-root agent + // user; without this chown the bind fails with EACCES, the + // gateway's agent-sockets.json stays empty, and the agent + // looks unreachable. uid resolution can return None on the + // very first spawn (container's /etc/passwd not yet rendered) + // — fall back to a permissive 0777 in that window so the + // first harness boot still binds. nspawn shares uids with the + // host (no PrivateUsers), so the in-container uid is the same + // uid we chown to here. + if let Some((uid, gid)) = agent_uid_gid(agent_name) { + std::os::unix::fs::chown(&socket_dir, Some(uid), Some(gid)) + .with_context(|| format!("chown {} to {uid}:{gid}", socket_dir.display()))?; + } else { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&socket_dir, std::fs::Permissions::from_mode(0o777)) + .with_context(|| { + format!("chmod 0777 {} (uid lookup failed)", socket_dir.display()) + })?; + } let _ = write!( binds, " --bind={socket_dir}:{socket_dir}",