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
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -302,9 +302,7 @@ async fn auto_reset_password(client: &reqwest::Client, name: &str) -> anyhow::Re
|
|||
let new_password = random_password()?;
|
||||
reset_user_password(client, &admin_token, name, &server_name, &new_password)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("matrix: admin API password reset for {name} (auto-recovery)")
|
||||
})?;
|
||||
.with_context(|| format!("matrix: admin API password reset for {name} (auto-recovery)"))?;
|
||||
tracing::info!(%name, "matrix: auto-recovered password via admin API reset");
|
||||
Ok(new_password)
|
||||
}
|
||||
|
|
@ -418,14 +416,14 @@ pub async fn ensure_user_for(
|
|||
Err(other) => return Err(other),
|
||||
};
|
||||
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).ok();
|
||||
}
|
||||
std::fs::write(&path, format!("{access_token}\n"))
|
||||
.with_context(|| format!("matrix: write token to {}", path.display()))?;
|
||||
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
|
||||
crate::lifecycle::chown_to_agent(name, &path, "matrix");
|
||||
tracing::info!(%name, path = %path.display(), "matrix: provisioned access token");
|
||||
// Write the token via hive-priv (root helper): hive-c0re runs as the
|
||||
// unprivileged `hive-core` user and cannot write to agent-owned state
|
||||
// directories directly. hive-priv writes the file 0600 and chowns it
|
||||
// to the agent user so it is readable from inside the container.
|
||||
crate::priv_client::write_agent_state_file(name, "matrix-token", &format!("{access_token}\n"))
|
||||
.await
|
||||
.with_context(|| format!("matrix: write matrix-token for {name} via hive-priv"))?;
|
||||
tracing::info!(%name, "matrix: provisioned access token");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -527,10 +525,7 @@ pub async fn ensure_admin_user(client: &reqwest::Client, register_token: &str) -
|
|||
if let Err(e) = std::fs::write(&pw_path, format!("{password}\n")) {
|
||||
tracing::warn!(error = ?e, "matrix: failed to persist hive admin password");
|
||||
} else {
|
||||
let _ = std::fs::set_permissions(
|
||||
&pw_path,
|
||||
std::fs::Permissions::from_mode(0o600),
|
||||
);
|
||||
let _ = std::fs::set_permissions(&pw_path, std::fs::Permissions::from_mode(0o600));
|
||||
}
|
||||
token
|
||||
}
|
||||
|
|
@ -573,9 +568,7 @@ pub async fn promote_user_to_admin(
|
|||
) -> Result<()> {
|
||||
// URL-encode the @user:server path segment manually — only `@` and
|
||||
// `:` need escaping; localpart + server_name use only safe chars.
|
||||
let url = format!(
|
||||
"{MATRIX_HTTP}/_synapse/admin/v2/users/%40{localpart}%3A{server_name}"
|
||||
);
|
||||
let url = format!("{MATRIX_HTTP}/_synapse/admin/v2/users/%40{localpart}%3A{server_name}");
|
||||
let resp = client
|
||||
.put(&url)
|
||||
.bearer_auth(admin_token)
|
||||
|
|
@ -605,9 +598,7 @@ pub async fn reset_user_password(
|
|||
server_name: &str,
|
||||
new_password: &str,
|
||||
) -> Result<()> {
|
||||
let url = format!(
|
||||
"{MATRIX_HTTP}/_synapse/admin/v2/users/%40{localpart}%3A{server_name}"
|
||||
);
|
||||
let url = format!("{MATRIX_HTTP}/_synapse/admin/v2/users/%40{localpart}%3A{server_name}");
|
||||
let resp = client
|
||||
.put(&url)
|
||||
.bearer_auth(admin_token)
|
||||
|
|
@ -626,10 +617,7 @@ pub async fn reset_user_password(
|
|||
tracing::warn!(%localpart, error = ?e, "matrix: failed to persist reset password");
|
||||
} else {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let _ = std::fs::set_permissions(
|
||||
&pw_path,
|
||||
std::fs::Permissions::from_mode(0o600),
|
||||
);
|
||||
let _ = std::fs::set_permissions(&pw_path, std::fs::Permissions::from_mode(0o600));
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -656,9 +644,7 @@ pub async fn discover_server_name(client: &reqwest::Client) -> Result<String> {
|
|||
.await
|
||||
.context("matrix: parse /_matrix/key/v2/server response")?;
|
||||
if !status.is_success() {
|
||||
anyhow::bail!(
|
||||
"matrix: /_matrix/key/v2/server returned HTTP {status}, body: {body}"
|
||||
);
|
||||
anyhow::bail!("matrix: /_matrix/key/v2/server returned HTTP {status}, body: {body}");
|
||||
}
|
||||
body["server_name"]
|
||||
.as_str()
|
||||
|
|
@ -697,10 +683,7 @@ pub fn read_admin_token() -> Result<String> {
|
|||
///
|
||||
/// Returns an error if the Matrix homeserver is unreachable, the
|
||||
/// `createRoom` call fails, or the room-ID file cannot be written.
|
||||
pub async fn ensure_hive_space(
|
||||
client: &reqwest::Client,
|
||||
admin_token: &str,
|
||||
) -> Result<String> {
|
||||
pub async fn ensure_hive_space(client: &reqwest::Client, admin_token: &str) -> Result<String> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let path = hive_space_room_id_path();
|
||||
if let Ok(existing) = std::fs::read_to_string(&path) {
|
||||
|
|
@ -859,15 +842,19 @@ pub async fn ensure_all() {
|
|||
}
|
||||
};
|
||||
// Invite @hive admin first, then all agents.
|
||||
if let Err(e) =
|
||||
invite_to_room(&client, &admin_token, &room_id, HIVE_ADMIN_LOCALPART, &server_name).await
|
||||
if let Err(e) = invite_to_room(
|
||||
&client,
|
||||
&admin_token,
|
||||
&room_id,
|
||||
HIVE_ADMIN_LOCALPART,
|
||||
&server_name,
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(error = ?e, "matrix: invite @hive to space failed");
|
||||
}
|
||||
for name in &agent_names {
|
||||
if let Err(e) =
|
||||
invite_to_room(&client, &admin_token, &room_id, name, &server_name).await
|
||||
{
|
||||
if let Err(e) = invite_to_room(&client, &admin_token, &room_id, name, &server_name).await {
|
||||
tracing::warn!(%name, error = ?e, "matrix: invite agent to space failed");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -256,6 +256,22 @@ pub async fn run_forge_admin(args: &[&str]) -> Result<(String, String)> {
|
|||
check(call(&PrivRequest::RunForgeAdmin { args: owned }).await?)
|
||||
}
|
||||
|
||||
/// Write `content` to `<agent_state_root>/<agent_name>/state/<filename>`
|
||||
/// via hive-priv (running as root). The file is written 0600 and chowned
|
||||
/// to the agent user so it is readable from inside the agent container.
|
||||
///
|
||||
/// Used for credential files (forge-token, matrix-token) that hive-c0re
|
||||
/// mints but cannot write directly because those paths are inside agent-
|
||||
/// owned (0755) state directories and hive-c0re runs unprivileged.
|
||||
pub async fn write_agent_state_file(agent_name: &str, filename: &str, content: &str) -> Result<()> {
|
||||
ok(call(&PrivRequest::WriteAgentStateFile {
|
||||
agent_name: agent_name.to_owned(),
|
||||
filename: filename.to_owned(),
|
||||
content: content.to_owned(),
|
||||
})
|
||||
.await?)
|
||||
}
|
||||
|
||||
fn check(resp: PrivResponse) -> Result<(String, String)> {
|
||||
if resp.ok {
|
||||
Ok((resp.stdout, resp.stderr))
|
||||
|
|
|
|||
Loading…
Reference in a new issue