fix: declare the agent socket dir's owner in tmpfiles, not by chown after
/run/hive-agent/<name> was 0777 root root in steady state, not just during first spawn. A directory without the sticky bit lets any user unlink files in it, and the gateway container has all of /run/hive-agent bind-mounted in, so anything that could reach the path could delete an agent's agent.sock, bind its own, and receive that agent's todos from hive-c0re. Two mechanisms were writing the dir and undoing each other: the tmpfiles.d entry wrote 0777 root root, then hive-c0re round-tripped through hive-priv's ChownSocketDir to narrow it. `d` re-asserts mode and owner on every apply and the file is regenerated on any agent's spawn or destroy, so every such event reset every agent's dir back to world-writable. SyncAgentTmpfiles now carries each agent's container uid/gid and the entry declares the answer: 0751 <uid> <gid>. Three principals need the dir and no two share a group -- the harness binds its sockets (owner rwx), hive-c0re dials agent.sock and the gateway's nginx dials web.sock (both only need traverse, and both sockets are already 0666). Deletes ChownSocketDir and ChmodSocketDir, both priv_client wrappers, the either/or in host_config with its two swallowed warn!s, and the now-dead socket_dir_path -- two verbs off the privileged helper's surface and one round-trip off every agent spawn. Also makes the two tmpfiles rules for /run/hive-agent itself agree: the gateway module said hive-core, the generated file said root, and which won depended on the order systemd read them in.
This commit is contained in:
parent
642377c5e0
commit
3fc1588e83
8 changed files with 110 additions and 103 deletions
|
|
@ -22,10 +22,10 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use hive_priv_sock::{
|
||||
AGENT_PREFIX, AGENT_RUNTIME_ROOT, AGENT_STATE_ROOT, BindMount, CredentialMount, InfraAction,
|
||||
InfraContainer, JournalQuery, META_DIR, MIGRATE_STAGING_ROOT, NetworkIsolation,
|
||||
PAUSED_MARKER_FILE, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream,
|
||||
PrivStreamLine, SIBLING_CONTAINERS,
|
||||
AGENT_PREFIX, AGENT_RUNTIME_ROOT, AGENT_STATE_ROOT, AgentTmpfilesEntry, BindMount,
|
||||
CredentialMount, InfraAction, InfraContainer, JournalQuery, META_DIR, MIGRATE_STAGING_ROOT,
|
||||
NetworkIsolation, PAUSED_MARKER_FILE, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse,
|
||||
PrivStream, PrivStreamLine, SIBLING_CONTAINERS,
|
||||
};
|
||||
use tokio::io::{AsyncWriteExt, BufReader};
|
||||
use tokio::net::unix::OwnedWriteHalf;
|
||||
|
|
@ -418,17 +418,6 @@ async fn exec(
|
|||
|
||||
PrivRequest::ReloadGatewayNginx => sync_gateway_nginx().await,
|
||||
|
||||
PrivRequest::ChownSocketDir {
|
||||
ref agent_name,
|
||||
uid,
|
||||
gid,
|
||||
} => chown_socket_dir(agent_name, uid, gid),
|
||||
|
||||
PrivRequest::ChmodSocketDir {
|
||||
ref agent_name,
|
||||
mode,
|
||||
} => chmod_socket_dir(agent_name, mode),
|
||||
|
||||
PrivRequest::RunForgeAdmin { ref args } => {
|
||||
for arg in args {
|
||||
validate_forge_admin_arg(arg)?;
|
||||
|
|
@ -714,26 +703,6 @@ fn remove_service_dropin(container: &str) -> Result<(String, String)> {
|
|||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// `ChownSocketDir` — chown the agent's host socket dir to its
|
||||
/// container uid/gid.
|
||||
fn chown_socket_dir(agent_name: &str, uid: u32, gid: u32) -> Result<(String, String)> {
|
||||
validate_agent_name(agent_name)?;
|
||||
let path = socket_dir_path(agent_name);
|
||||
std::os::unix::fs::chown(&path, Some(uid), Some(gid))
|
||||
.with_context(|| format!("chown {} to {uid}:{gid}", path.display()))?;
|
||||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// `ChmodSocketDir` — set the mode on the agent's host socket dir.
|
||||
fn chmod_socket_dir(agent_name: &str, mode: u32) -> Result<(String, String)> {
|
||||
use std::os::unix::fs::PermissionsExt as _;
|
||||
validate_agent_name(agent_name)?;
|
||||
let path = socket_dir_path(agent_name);
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
|
||||
.with_context(|| format!("chmod {:o} {}", mode, path.display()))?;
|
||||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// `WriteResourceLimits` — drop the systemd resource settings into the
|
||||
/// container service's drop-in dir, together with a
|
||||
/// `ConditionPathIsDirectory=` guard on the agent's MCP runtime dir.
|
||||
|
|
@ -2205,11 +2174,6 @@ fn container_system_name(name: &str) -> String {
|
|||
format!("{AGENT_PREFIX}{name}")
|
||||
}
|
||||
|
||||
/// Path of the per-agent unix-socket dir on the host.
|
||||
fn socket_dir_path(agent_name: &str) -> PathBuf {
|
||||
PathBuf::from(format!("{SOCKET_DIR_ROOT}/{agent_name}"))
|
||||
}
|
||||
|
||||
/// Validate a logical agent name (the name hive-c0re uses internally,
|
||||
/// before the `h-` container prefix is applied).
|
||||
fn validate_agent_name(name: &str) -> Result<()> {
|
||||
|
|
@ -2427,10 +2391,10 @@ fn write_bridge_dns_marker(container: &str, isolation: Option<&NetworkIsolation>
|
|||
/// - `/run/hive-agent/<name>` (web socket dir, bind-mounted into container)
|
||||
const TMPFILES_PATH: &str = "/etc/tmpfiles.d/hyperhive-agents.conf";
|
||||
|
||||
async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> {
|
||||
async fn sync_agent_tmpfiles(agents: &[AgentTmpfilesEntry]) -> Result<(String, String)> {
|
||||
use std::fmt::Write as _;
|
||||
for name in agents {
|
||||
validate_agent_name(name)?;
|
||||
for entry in agents {
|
||||
validate_agent_name(&entry.name)?;
|
||||
}
|
||||
|
||||
// Build tmpfiles.d content. Root dirs first, then per-agent.
|
||||
|
|
@ -2441,20 +2405,48 @@ async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> {
|
|||
// tmpfiles.d entry here ensures it exists before hive-c0re starts (boot race).
|
||||
content.push_str("d /run/hyperhive 0750 hive-core hive-core -\n");
|
||||
writeln!(content, "d {AGENT_RUNTIME_ROOT} 0755 hive-core hive-core -").ok();
|
||||
writeln!(content, "d {SOCKET_DIR_ROOT} 0755 root root -").ok();
|
||||
// `hive-core`, not root: c0re does the `create_dir_all` for a new agent's
|
||||
// subdir itself, so a root-owned parent EACCESes on the first spawn of a
|
||||
// fresh host. This must stay in step with the identical rule in
|
||||
// `nix/host-modules/hive-gateway/default.nix` — the two files declared
|
||||
// different owners for this one path, and which won depended on the order
|
||||
// systemd happened to read them in.
|
||||
writeln!(content, "d {SOCKET_DIR_ROOT} 0755 hive-core hive-core -").ok();
|
||||
// Per-agent dirs.
|
||||
for name in agents {
|
||||
for entry in agents {
|
||||
let name = &entry.name;
|
||||
writeln!(
|
||||
content,
|
||||
"d {AGENT_RUNTIME_ROOT}/{name} 0755 hive-core hive-core -"
|
||||
)
|
||||
.ok();
|
||||
// 0777: agent harness (non-root uid) must bind sockets here.
|
||||
// `d` adjusts mode/owner on existing dirs; world-writable matches
|
||||
// the chmod_socket_dir(0o777) fallback so a runtime re-sync doesn't
|
||||
// break a live agent's socket dir. host_config's chown_socket_dir
|
||||
// tightens ownership afterwards when the agent uid is available.
|
||||
writeln!(content, "d {SOCKET_DIR_ROOT}/{name} 0777 root root -").ok();
|
||||
// The agent's socket dir. Three principals need it and no two share a
|
||||
// group, so the mode has to say so explicitly:
|
||||
//
|
||||
// owner = the agent user rwx binds + unlinks agent.sock/web.sock
|
||||
// other = --x traverse only, no listing
|
||||
//
|
||||
// "other" covers hive-c0re (dials agent.sock) and the gateway's nginx
|
||||
// (dials web.sock, and has all of /run/hive-agent bind-mounted in).
|
||||
// Both sockets are 0666, so traversal is all they need.
|
||||
//
|
||||
// 0751 rather than the historical 0777 is a fix, not a tidy-up: a
|
||||
// directory without the sticky bit lets *any* user unlink files in it,
|
||||
// so world-writable here means anything that can reach the path could
|
||||
// delete an agent's socket, bind its own, and receive that agent's
|
||||
// todos. Declaring the owner here also ends the tug-of-war with the
|
||||
// old ChownSocketDir: `d` re-applies on every sync, so a chown made
|
||||
// afterwards was reset by the next agent's spawn.
|
||||
if let (Some(uid), Some(gid)) = (entry.uid, entry.gid) {
|
||||
writeln!(content, "d {SOCKET_DIR_ROOT}/{name} 0751 {uid} {gid} -").ok();
|
||||
} else {
|
||||
// Before the container's /etc/passwd exists there is no uid to
|
||||
// name, and the harness must still be able to bind. Keep the old
|
||||
// permissive mode for that agent alone; the next sync (any spawn
|
||||
// or destroy, or c0re restart) resolves the uid and tightens it.
|
||||
tracing::info!(%name, "tmpfiles.d: agent uid unknown, deferring 0751 on socket dir");
|
||||
writeln!(content, "d {SOCKET_DIR_ROOT}/{name} 0777 root root -").ok();
|
||||
}
|
||||
}
|
||||
|
||||
// Atomic write: write to a tmp file then rename so a concurrent reader
|
||||
|
|
|
|||
Loading…
Reference in a new issue