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:
müde 2026-06-02 23:43:02 +02:00
commit 9e12012a95
7 changed files with 279 additions and 65 deletions

View file

@ -21,8 +21,8 @@ use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail};
use hive_sh4re::priv_proto::{
AGENT_PREFIX, BindMount, MANAGER_NAME, META_DIR, PRIV_SOCK, PrivRequest, PrivResponse,
SIBLING_CONTAINERS,
AGENT_PREFIX, BindMount, JournalOutput, MANAGER_NAME, META_DIR, PRIV_SOCK, PrivRequest,
PrivResponse, SIBLING_CONTAINERS,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{UnixListener, UnixStream};
@ -186,6 +186,24 @@ async fn exec(req: PrivRequest) -> Result<(String, String)> {
PrivRequest::ListContainers => container_run(&["list"]).await,
PrivRequest::ReadContainerJournal {
ref container,
lines,
boot,
output,
ref unit,
ref priority,
ref grep,
ref since,
ref until,
} => {
validate_container_system_name(container)?;
read_container_journal(
container, lines, boot, output, unit, priority, grep, since, until,
)
.await
}
PrivRequest::WriteNspawnFlags {
ref container,
ref binds,
@ -313,6 +331,71 @@ async fn container_run(args: &[&str]) -> Result<(String, String)> {
Ok((stdout, stderr))
}
/// Read a container's journal as root via `journalctl -M`. Returns
/// `(stdout, stderr)`. Unlike `container_run` a non-zero exit is *not* a
/// hard error — journalctl's own diagnostic (folded into `stderr` with
/// the exit status) is what the caller surfaces to the operator, so the
/// helper never bails.
#[allow(clippy::too_many_arguments)]
async fn read_container_journal(
container: &str,
lines: u32,
boot: bool,
output: JournalOutput,
unit: &Option<String>,
priority: &Option<String>,
grep: &Option<String>,
since: &Option<String>,
until: &Option<String>,
) -> Result<(String, String)> {
let mut args: Vec<String> = vec![
"-M".to_owned(),
container.to_owned(),
"--no-pager".to_owned(),
format!("--output={}", output.as_journalctl()),
"-n".to_owned(),
lines.to_string(),
];
if boot {
args.push("-b".to_owned());
}
if let Some(u) = unit {
args.push("-u".to_owned());
args.push(u.clone());
}
if let Some(p) = priority {
args.push("-p".to_owned());
args.push(p.clone());
}
// `--grep=`/`--since=`/`--until=` use the `=`-joined form so a value
// can never be parsed as a separate journalctl flag.
if let Some(g) = grep {
args.push(format!("--grep={g}"));
}
if let Some(s) = since {
args.push(format!("--since={s}"));
}
if let Some(u) = until {
args.push(format!("--until={u}"));
}
let out = Command::new("journalctl")
.args(&args)
.output()
.await
.context("invoke journalctl -M")?;
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let stderr = if out.status.success() {
String::from_utf8_lossy(&out.stderr).into_owned()
} else {
format!(
"journalctl -M {container} exited {}: {}",
out.status,
String::from_utf8_lossy(&out.stderr).trim()
)
};
Ok((stdout, stderr))
}
/// Return the system container name for a logical agent name.
/// All agents (including the manager) use the `h-` prefix.
fn container_system_name(name: &str) -> String {