fix: route forge/matrix token writes through hive-priv

hive-c0re runs as the unprivileged hive-core user (privsep from #702)
and cannot write to agent-owned state directories. forge-token and
matrix-token writes were failing with EACCES on every startup sweep.

Add WriteAgentStateFile to PrivRequest: hive-priv (root) writes the
file 0600 and chowns it to the agent user so the agent can read it.

- hive-sh4re: add AGENT_STATE_ROOT constant + WriteAgentStateFile variant
- hive-priv: validate agent name + filename (no traversal), write via root
- priv_client: add write_agent_state_file helper
- forge: mint_and_persist_token routes agent paths through priv
- matrix: ensure_user_for routes matrix-token through priv

Closes #1257
This commit is contained in:
atlas 2026-06-04 12:11:59 +02:00 committed by mara
commit eb51362d50
5 changed files with 172 additions and 46 deletions

View file

@ -100,9 +100,9 @@ async fn forge_admin(args: &[&str]) -> Result<String> {
// hive-c0re runs as the unprivileged `hive-core` user and cannot call
// nsenter directly — doing so produces:
// nsenter: stat of /proc/<pid>/ns/user failed: Permission denied
let (stdout, _stderr) = crate::priv_client::run_forge_admin(args).await.with_context(
|| format!("forgejo admin {} (via hive-priv)", args.join(" ")),
)?;
let (stdout, _stderr) = crate::priv_client::run_forge_admin(args)
.await
.with_context(|| format!("forgejo admin {} (via hive-priv)", args.join(" ")))?;
Ok(stdout)
}
@ -291,19 +291,34 @@ async fn mint_token(name: &str, scopes: &str) -> Result<String> {
Ok(token)
}
/// Mint a fresh access token for `name` and persist it to `path`
/// (0600). Wraps [`mint_token`] for callers that want the token on
/// disk under an agent state dir.
/// Mint a fresh access token for `name` and persist it to `path` (0600).
/// For paths outside the agent state tree (e.g. the core admin token at
/// `/var/lib/hyperhive/forge-core-token`), writes directly — hive-c0re
/// owns those paths. For paths inside `AGENT_STATE_ROOT` the write is
/// routed through hive-priv (root helper) because hive-c0re runs
/// unprivileged and cannot write to agent-owned state directories.
async fn mint_and_persist_token(name: &str, path: &Path, scopes: &str) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let token = mint_token(name, scopes).await?;
// Agent state paths must go through hive-priv.
if path.components().any(|c| c.as_os_str() == "agents") {
let filename = path
.file_name()
.and_then(|n| n.to_str())
.context("could not determine filename for priv write")?;
return crate::priv_client::write_agent_state_file(name, filename, &format!("{token}\n"))
.await
.with_context(|| format!("write {filename} for {name} via hive-priv"));
}
// Non-agent paths (core admin token, etc.): write directly.
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
std::fs::write(path, format!("{token}\n"))
.with_context(|| format!("write token to {}", path.display()))?;
let _ = std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
crate::lifecycle::chown_to_agent(name, path, "forge");
tracing::info!(%name, path = %path.display(), "forge: persisted access token");
Ok(())
}