From 7a3614552b22087552d81ac93e78a52562a25f28 Mon Sep 17 00:00:00 2001 From: damocles Date: Sun, 26 Jul 2026 15:02:29 +0200 Subject: [PATCH] hive-priv: redact password-bearing lines before logging forgejo admin output --- hive-priv/src/main.rs | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 9610d605..905679ae 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -1366,6 +1366,21 @@ fn validate_forge_admin_arg(arg: &str) -> Result<()> { Ok(()) } +/// Redact a line before it hits the (root-readable, but still +/// unnecessarily exposed) host journal. `forgejo admin user create +/// --random-password` prints the generated password straight to stdout — +/// case-insensitive substring match on "password" is deliberately broad +/// (not pinned to forgejo's exact wording, which can change across +/// versions) so any password-bearing line gets caught rather than relying +/// on a phrase that could silently drift out of sync. +fn redact_password_line(line: &str) -> std::borrow::Cow<'_, str> { + if line.to_ascii_lowercase().contains("password") { + std::borrow::Cow::Borrowed("[redacted: line mentions a password]") + } else { + std::borrow::Cow::Borrowed(line) + } +} + /// Run `forgejo admin ` inside the `hive-forge` container as the /// `forgejo` unix user. Requires root (for nsenter into the container's /// namespaces). Returns `(stdout, stderr)`. @@ -1394,10 +1409,10 @@ async fn run_forge_admin(args: &[String]) -> Result<(String, String)> { let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); for line in stdout.lines() { - tracing::info!(target: "forgejo-admin", "{line}"); + tracing::info!(target: "forgejo-admin", "{}", redact_password_line(line)); } for line in stderr.lines() { - tracing::warn!(target: "forgejo-admin", "{line}"); + tracing::warn!(target: "forgejo-admin", "{}", redact_password_line(line)); } if !out.status.success() { bail!( @@ -1986,10 +2001,26 @@ async fn sync_agent_tmpfiles(agents: &[String]) -> Result<(String, String)> { #[cfg(test)] mod tests { - use super::write_state_file_nofollow; + use super::{redact_password_line, write_state_file_nofollow}; use std::path::PathBuf; use std::sync::atomic::{AtomicU32, Ordering}; + #[test] + fn redacts_lines_mentioning_password_case_insensitively() { + assert_eq!( + redact_password_line("New password: hunter2"), + "[redacted: line mentions a password]" + ); + assert_eq!( + redact_password_line("PASSWORD=hunter2"), + "[redacted: line mentions a password]" + ); + assert_eq!( + redact_password_line("User \"foo\" was successfully created."), + "User \"foo\" was successfully created." + ); + } + /// Unique scratch dir per test, no external tempfile dep. fn scratch() -> PathBuf { static CTR: AtomicU32 = AtomicU32::new(0);