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
|
|
@ -10,8 +10,8 @@ use hive_priv_sock::{BindMount, CredentialMount};
|
|||
use crate::coordinator::{AgentPaths, HiveEnv};
|
||||
|
||||
use super::{
|
||||
AGENT_PREFIX, CONTAINER_RUNTIME_MOUNT, CONTAINER_SHARED_MOUNT, agent_uid_gid,
|
||||
bridge_gateway_ip, container_claude_mount, container_name, validate,
|
||||
AGENT_PREFIX, CONTAINER_RUNTIME_MOUNT, CONTAINER_SHARED_MOUNT, bridge_gateway_ip,
|
||||
container_claude_mount, container_name, validate,
|
||||
};
|
||||
|
||||
/// Re-apply the per-container host-side config: nspawn flags (bind
|
||||
|
|
@ -289,16 +289,11 @@ async 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()))?;
|
||||
// Chown to the agent user so the non-root harness can bind(2) here.
|
||||
// Falls back to 0777 on first spawn when uid lookup returns None
|
||||
// (container /etc/passwd not yet rendered).
|
||||
if let Some((uid, gid)) = agent_uid_gid(agent_name) {
|
||||
if let Err(e) = crate::priv_client::chown_socket_dir(agent_name, uid, gid).await {
|
||||
tracing::warn!(%agent_name, error = ?e, "chown socket dir failed");
|
||||
}
|
||||
} else if let Err(e) = crate::priv_client::chmod_socket_dir(agent_name, 0o777).await {
|
||||
tracing::warn!(%agent_name, error = ?e, "chmod socket dir failed");
|
||||
}
|
||||
// Ownership is NOT repaired here. The dir's owner + mode are declared by
|
||||
// the tmpfiles.d entry (`SyncAgentTmpfiles`), which is the mechanism that
|
||||
// re-applies on every boot and every spawn — so a chown made here was
|
||||
// silently reverted the next time any agent was spawned or destroyed.
|
||||
// This `create_dir_all` only covers the window before that sync lands.
|
||||
binds.push(BindMount {
|
||||
host_path: socket_dir.to_string_lossy().into_owned(),
|
||||
container_path: socket_dir.to_string_lossy().into_owned(),
|
||||
|
|
|
|||
|
|
@ -712,6 +712,16 @@ pub async fn sync_tmpfiles() {
|
|||
Ok(containers) => containers
|
||||
.into_iter()
|
||||
.filter_map(|c| c.strip_prefix(AGENT_PREFIX).map(str::to_owned))
|
||||
.map(|name| {
|
||||
// Resolved here, not in hive-priv: the mapping lives in the
|
||||
// container's /etc/passwd, which is c0re's to read. `None`
|
||||
// until the container's first boot renders it.
|
||||
let (uid, gid) = match agent_uid_gid(&name) {
|
||||
Some((uid, gid)) => (Some(uid), Some(gid)),
|
||||
None => (None, None),
|
||||
};
|
||||
hive_priv_sock::AgentTmpfilesEntry { name, uid, gid }
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
Err(e) => {
|
||||
tracing::warn!(error = ?e, "sync_tmpfiles: list failed; skipping");
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use hive_priv_sock::{
|
||||
BindMount, CredentialMount, InfraAction, InfraContainer, JournalQuery, NetworkIsolation,
|
||||
PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream,
|
||||
AgentTmpfilesEntry, BindMount, CredentialMount, InfraAction, InfraContainer, JournalQuery,
|
||||
NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream,
|
||||
};
|
||||
use std::os::fd::{AsRawFd as _, OwnedFd, RawFd};
|
||||
|
||||
|
|
@ -338,23 +338,6 @@ pub async fn reload_gateway_nginx() -> Result<()> {
|
|||
ok(call(&PrivRequest::ReloadGatewayNginx).await?)
|
||||
}
|
||||
|
||||
pub async fn chown_socket_dir(agent_name: &str, uid: u32, gid: u32) -> Result<()> {
|
||||
ok(call(&PrivRequest::ChownSocketDir {
|
||||
agent_name: agent_name.to_owned(),
|
||||
uid,
|
||||
gid,
|
||||
})
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn chmod_socket_dir(agent_name: &str, mode: u32) -> Result<()> {
|
||||
ok(call(&PrivRequest::ChmodSocketDir {
|
||||
agent_name: agent_name.to_owned(),
|
||||
mode,
|
||||
})
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Run `forgejo admin <args>` inside the `hive-forge` container via
|
||||
/// hive-priv (which runs as root and can nsenter into the container).
|
||||
/// Returns `(stdout, stderr)` on success.
|
||||
|
|
@ -652,15 +635,16 @@ pub async fn send_agent_snapshot_to_file(
|
|||
Ok(stdout)
|
||||
}
|
||||
|
||||
/// Write `/etc/tmpfiles.d/hyperhive-agents.conf` for `agents` (logical names,
|
||||
/// e.g. `"atlas"`) and immediately apply it with `systemd-tmpfiles --create`.
|
||||
/// See [`PrivRequest::SyncAgentTmpfiles`] for the full semantics.
|
||||
/// Write `/etc/tmpfiles.d/hyperhive-agents.conf` for `agents` and immediately
|
||||
/// apply it with `systemd-tmpfiles --create`. Each entry carries the agent's
|
||||
/// container uid/gid so the socket dir's ownership is *declared* here rather
|
||||
/// than corrected afterwards. See [`PrivRequest::SyncAgentTmpfiles`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if the priv socket call fails, if any agent name is
|
||||
/// invalid, or if `systemd-tmpfiles --create` exits non-zero.
|
||||
pub async fn sync_agent_tmpfiles(agents: &[String]) -> Result<()> {
|
||||
pub async fn sync_agent_tmpfiles(agents: &[AgentTmpfilesEntry]) -> Result<()> {
|
||||
ok(call(&PrivRequest::SyncAgentTmpfiles {
|
||||
agents: agents.to_vec(),
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue