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:
parent
be8d8e48bf
commit
eb51362d50
5 changed files with 172 additions and 46 deletions
|
|
@ -21,8 +21,9 @@ use std::path::{Path, PathBuf};
|
|||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use hive_sh4re::priv_proto::{
|
||||
AGENT_PREFIX, BindMount, JournalOutput, MANAGER_NAME, META_DIR, NetworkIsolation, PRIV_SOCK,
|
||||
PrivEvent, PrivRequest, PrivResponse, PrivStream, PrivStreamLine, SIBLING_CONTAINERS,
|
||||
AGENT_PREFIX, AGENT_STATE_ROOT, BindMount, JournalOutput, MANAGER_NAME, META_DIR,
|
||||
NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream, PrivStreamLine,
|
||||
SIBLING_CONTAINERS,
|
||||
};
|
||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||
use tokio::net::unix::OwnedWriteHalf;
|
||||
|
|
@ -320,9 +321,86 @@ async fn exec(req: PrivRequest, writer: &mut OwnedWriteHalf) -> Result<(String,
|
|||
}
|
||||
run_forge_admin(args).await
|
||||
}
|
||||
|
||||
PrivRequest::WriteAgentStateFile {
|
||||
ref agent_name,
|
||||
ref filename,
|
||||
ref content,
|
||||
} => {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_state_filename(filename)?;
|
||||
write_agent_state_file(agent_name, filename, content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate a filename destined for `WriteAgentStateFile`. Rejects
|
||||
/// path separators, null bytes, newlines, and `..` to prevent path traversal.
|
||||
fn validate_state_filename(filename: &str) -> Result<()> {
|
||||
if filename.is_empty() {
|
||||
bail!("state filename must not be empty");
|
||||
}
|
||||
if filename == ".." || filename == "." {
|
||||
bail!("state filename must not be . or ..");
|
||||
}
|
||||
if filename
|
||||
.bytes()
|
||||
.any(|b| b == 0 || b == b'\n' || b == b'\r' || b == b'/')
|
||||
{
|
||||
bail!("state filename {filename:?} contains null byte, newline, or path separator");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Write `content` to `AGENT_STATE_ROOT/<agent_name>/state/<filename>`,
|
||||
/// chown to the agent user (derived from the state dir's existing owner),
|
||||
/// and chmod 0600. Running as root (hive-priv), so all of this succeeds
|
||||
/// regardless of the file's prior owner/permissions.
|
||||
fn write_agent_state_file(
|
||||
agent_name: &str,
|
||||
filename: &str,
|
||||
content: &str,
|
||||
) -> Result<(String, String)> {
|
||||
use std::os::unix::fs::{MetadataExt as _, PermissionsExt as _};
|
||||
|
||||
let state_dir = PathBuf::from(AGENT_STATE_ROOT)
|
||||
.join(agent_name)
|
||||
.join("state");
|
||||
std::fs::create_dir_all(&state_dir)
|
||||
.with_context(|| format!("create state dir {}", state_dir.display()))?;
|
||||
|
||||
let path = state_dir.join(filename);
|
||||
std::fs::write(&path, content).with_context(|| format!("write {}", path.display()))?;
|
||||
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600))
|
||||
.with_context(|| format!("chmod 600 {}", path.display()))?;
|
||||
|
||||
// Chown to the state dir's owner so the agent process can read the file.
|
||||
// If stat fails (e.g. dir just created, owner is root), the file stays
|
||||
// root-owned and 0600 — still unreadable by others, just not agent-readable.
|
||||
// Log a warning so operators can diagnose.
|
||||
match std::fs::metadata(&state_dir) {
|
||||
Ok(meta) => {
|
||||
if let Err(e) = std::os::unix::fs::chown(&path, Some(meta.uid()), Some(meta.gid())) {
|
||||
tracing::warn!(
|
||||
agent = %agent_name,
|
||||
path = %path.display(),
|
||||
error = %e,
|
||||
"write_agent_state_file: chown failed"
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!(
|
||||
agent = %agent_name,
|
||||
error = %e,
|
||||
"write_agent_state_file: stat state_dir failed, leaving file root-owned"
|
||||
);
|
||||
}
|
||||
}
|
||||
tracing::info!(agent = %agent_name, file = %filename, "wrote agent state file");
|
||||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// Validate a single argument destined for `forgejo admin`. Rejects
|
||||
/// null bytes and newlines (which could corrupt the subprocess args list
|
||||
/// or log output). Shell metacharacters are harmless since the command
|
||||
|
|
|
|||
Loading…
Reference in a new issue