fix(#702): route container journal reads through hive-priv
The privsep drop to the hive-core user left four journalctl -M <container> call sites shelling out directly. -M enters the container namespace via the machine bus, which needs root, so all container-journal reads failed with Permission denied. Add a ReadContainerJournal verb to hive-priv and route dashboard get_journal, manager get_logs, the rebuild-failure journal tail, and the agent host-journal -M path through it. Host-journal reads (no -M) stay direct via systemd-journal group membership.
This commit is contained in:
parent
5da7f6cd3a
commit
9e12012a95
7 changed files with 279 additions and 65 deletions
|
|
@ -1160,10 +1160,10 @@ struct JournalQuery {
|
|||
lines: Option<u32>,
|
||||
}
|
||||
|
||||
/// Shell out to `journalctl -M <container> -b` and return its text
|
||||
/// output. Operator-only by virtue of the dashboard being host-bound;
|
||||
/// hive-c0re already runs as root in its systemd unit so journalctl
|
||||
/// has the access it needs.
|
||||
/// Read `journalctl -M <container> -b` and return its text output.
|
||||
/// Operator-only by virtue of the dashboard being host-bound. hive-c0re
|
||||
/// runs unprivileged (privsep), so the `-M` read — which enters the
|
||||
/// container namespace and needs root — is delegated to hive-priv.
|
||||
async fn get_journal(
|
||||
AxumPath(name): AxumPath<String>,
|
||||
axum::extract::Query(q): axum::extract::Query<JournalQuery>,
|
||||
|
|
@ -1184,40 +1184,45 @@ async fn get_journal(
|
|||
return error_response(&format!("journal: no managed container {prefixed:?}"));
|
||||
}
|
||||
let lines = q.lines.unwrap_or(500).min(5000);
|
||||
let mut cmd = tokio::process::Command::new("journalctl");
|
||||
cmd.args([
|
||||
"-M",
|
||||
&prefixed,
|
||||
"-b",
|
||||
"--no-pager",
|
||||
"--output=short-iso",
|
||||
"--lines",
|
||||
])
|
||||
.arg(lines.to_string());
|
||||
if let Some(u) = q.unit.as_deref().filter(|s| !s.is_empty()) {
|
||||
// accept hive-ag3nt[.service] — anything else refused.
|
||||
let allowed = ["hive-ag3nt.service"];
|
||||
let unit = if u.ends_with(".service") {
|
||||
u.to_owned()
|
||||
} else {
|
||||
format!("{u}.service")
|
||||
};
|
||||
if !allowed.contains(&unit.as_str()) {
|
||||
return error_response(&format!("journal: unknown unit {unit:?}"));
|
||||
let unit = match q.unit.as_deref().filter(|s| !s.is_empty()) {
|
||||
Some(u) => {
|
||||
// accept hive-ag3nt[.service] — anything else refused.
|
||||
let allowed = ["hive-ag3nt.service"];
|
||||
let unit = if u.ends_with(".service") {
|
||||
u.to_owned()
|
||||
} else {
|
||||
format!("{u}.service")
|
||||
};
|
||||
if !allowed.contains(&unit.as_str()) {
|
||||
return error_response(&format!("journal: unknown unit {unit:?}"));
|
||||
}
|
||||
Some(unit)
|
||||
}
|
||||
cmd.args(["-u", &unit]);
|
||||
}
|
||||
match cmd.output().await {
|
||||
Ok(out) => {
|
||||
None => None,
|
||||
};
|
||||
match crate::priv_client::read_container_journal(
|
||||
&prefixed,
|
||||
lines,
|
||||
true,
|
||||
hive_sh4re::priv_proto::JournalOutput::ShortIso,
|
||||
unit,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok((stdout, stderr)) => {
|
||||
// Combine stdout + stderr — journalctl emits to both on errors.
|
||||
let mut body = String::from_utf8_lossy(&out.stdout).into_owned();
|
||||
if !out.status.success() {
|
||||
let mut body = stdout;
|
||||
if !stderr.is_empty() {
|
||||
body.push_str("\n--- stderr ---\n");
|
||||
body.push_str(&String::from_utf8_lossy(&out.stderr));
|
||||
body.push_str(&stderr);
|
||||
}
|
||||
([("content-type", "text/plain; charset=utf-8")], body).into_response()
|
||||
}
|
||||
Err(e) => error_response(&format!("journalctl spawn: {e}")),
|
||||
Err(e) => error_response(&format!("journal read: {e:#}")),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue