From b4343487126b795908ead2ea1388e1281b5c7dea Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Sun, 14 Sep 2025 23:55:13 +0000 Subject: [PATCH 1/3] fix(tls): ensure jailed processes trust httpjail CA under sudo by preferring SUDO_USER path and copying cert if needed\n\n- Prefer SUDO_USER's config path for CA when running under sudo\n- Correct SUDO_USER path computation on Linux/macOS\n- If only /root CA exists, copy it to the invoking user's config so non-root process can read it\n- Keep existing env var injection (SSL_CERT_FILE, SSL_CERT_DIR, etc.)\n\nThis fixes TLS failures like: Post https://api.github.com/graphql: tls: failed to verify certificate: x509: certificate signed by unknown authority, when running gh inside httpjail as a dropped-priv user.\n\nCo-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- src/tls.rs | 114 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 81 insertions(+), 33 deletions(-) diff --git a/src/tls.rs b/src/tls.rs index 1068c29f..ccbda3c6 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -262,51 +262,99 @@ impl CertificateManager { /// Generate environment variables for common tools to use the CA certificate pub fn get_ca_env_vars() -> Result> { - // Try multiple possible locations for the CA certificate - // This handles cases where the effective user changes (e.g., sudo in CI) - let mut ca_path = Self::get_ca_cert_path()?; - - if !ca_path.exists() { - // If not found in current user's config, check common locations - let possible_paths = [ - // Check SUDO_USER's config directory - std::env::var("SUDO_USER").ok().and_then(|sudo_user| { - dirs::home_dir().map(|home| { - home.parent() - .unwrap_or(&home) - .join(sudo_user) - .join(".config/httpjail/ca-cert.pem") - }) - }), - // Check /home/runner for CI - Some(PathBuf::from("/home/runner/.config/httpjail/ca-cert.pem")), - // Check root's config - Some(PathBuf::from("/root/.config/httpjail/ca-cert.pem")), - ]; + // Resolve the most appropriate CA certificate path for the target process. + // Prefer the non-root SUDO_USER's config if available, since we may drop privileges + // before executing the jailed command. + let current_path = Self::get_ca_cert_path()?; // Typically the path under the current user (often root under sudo) + + // Build candidate paths in priority order + let mut candidates: Vec = Vec::new(); + + // If running under sudo, prefer the invoking user's config directory + if let Ok(sudo_user) = std::env::var("SUDO_USER") { + #[cfg(target_os = "linux")] + { + candidates.push(PathBuf::from(format!( + "/home/{}/.config/httpjail/ca-cert.pem", + sudo_user + ))); + } + #[cfg(target_os = "macos")] + { + candidates.push(PathBuf::from(format!( + "/Users/{}/Library/Application Support/httpjail/ca-cert.pem", + sudo_user + ))); + } + } - for path in possible_paths.iter().flatten() { - if path.exists() { - ca_path = Utf8PathBuf::try_from(path.clone()) - .context("CA cert path is not valid UTF-8")?; - debug!("Found CA certificate at alternate location: {}", ca_path); - break; + // Always consider the path for the current effective user (may be /root/.config/...) + candidates.push(PathBuf::from(current_path.as_str())); + + // Common CI/runner locations + candidates.push(PathBuf::from("/home/runner/.config/httpjail/ca-cert.pem")); + candidates.push(PathBuf::from("/root/.config/httpjail/ca-cert.pem")); + + // Pick the first existing path + let mut chosen = candidates + .iter() + .find(|p| p.exists()) + .cloned() + .unwrap_or_else(|| PathBuf::from(current_path.as_str())); + + // If we're under sudo and the chosen path is under /root, try to materialize a readable + // copy for the invoking user so the jailed (non-root) process can read it. + if let Ok(sudo_user) = std::env::var("SUDO_USER") { + #[cfg(target_os = "linux")] + let user_target = + PathBuf::from(format!("/home/{}/.config/httpjail/ca-cert.pem", sudo_user)); + #[cfg(target_os = "macos")] + let user_target = PathBuf::from(format!( + "/Users/{}/Library/Application Support/httpjail/ca-cert.pem", + sudo_user + )); + + let chosen_str = chosen.to_string_lossy(); + if chosen_str.starts_with("/root/") { + if !user_target.exists() { + if let Some(parent) = user_target.parent() { + let _ = fs::create_dir_all(parent); + } + if let Ok(bytes) = fs::read(&chosen) { + // Best-effort write; world-readable cert (default umask 022) is fine + if fs::write(&user_target, bytes).is_ok() { + debug!( + "Copied CA certificate to invoking user's config: {}", + user_target.to_string_lossy() + ); + chosen = user_target; + } + } + } else { + // Prefer the user's existing CA cert if present + chosen = user_target; } } + } - if !ca_path.exists() { - anyhow::bail!( - "CA certificate not found. Searched: {:?} and common locations", - ca_path - ); - } + // Ensure we have a valid, existing file + if !chosen.exists() { + anyhow::bail!( + "CA certificate not found. Looked for {} and common locations", + chosen.to_string_lossy() + ); } + let ca_path = Utf8PathBuf::try_from(chosen).context("CA cert path is not valid UTF-8")?; + let ca_path_str = ca_path.to_string(); let ca_dir = ca_path .parent() .map(|p| p.to_string()) .unwrap_or_else(|| ".".to_string()); + debug!("Using CA certificate at {}", ca_path_str); + let env_vars = vec![ // OpenSSL/LibreSSL-based tools (generic) ("SSL_CERT_FILE".to_string(), ca_path_str.clone()), From 849f7494adfc4578aa8bf4ac05e11c943a89e2d8 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 00:26:42 +0000 Subject: [PATCH 2/3] =?UTF-8?q?test(weak):=20add=20gh=20CLI=20integration?= =?UTF-8?q?=20test=20for=20unauthenticated=20endpoint=20(/zen)=20in=20weak?= =?UTF-8?q?=20mode=20on=20non-macOS\n\nSkips=20if=20gh=20is=20not=20instal?= =?UTF-8?q?led.=20Validates=20we=20don=E2=80=99t=20hit=20x509=20unknown=20?= =?UTF-8?q?authority=20and=20that=20the=20command=20succeeds.\n\nCo-author?= =?UTF-8?q?ed-by:=20ammario=20<7416144+ammario@users.noreply.github.com>?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/weak_gh_integration.rs | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/weak_gh_integration.rs diff --git a/tests/weak_gh_integration.rs b/tests/weak_gh_integration.rs new file mode 100644 index 00000000..c44db1f8 --- /dev/null +++ b/tests/weak_gh_integration.rs @@ -0,0 +1,45 @@ +mod common; + +use common::HttpjailCommand; +use std::process::Command; + +// macOS' Go toolchain uses the platform verifier which ignores SSL_CERT_FILE, so +// tls interception in weak mode will fail for Go clients unless we tunnel. Until +// behavior is adjusted, run this on non-macOS only. +#[cfg(not(target_os = "macos"))] +#[test] +fn test_weak_mode_gh_api_zen() { + // Skip if gh is not available in the environment + if Command::new("gh").arg("--version").output().is_err() { + eprintln!("Skipping test: gh CLI not installed"); + return; + } + + // Allow GitHub API hosts. Use a very permissive allowlist for this test to + // avoid flakes if gh makes auxiliary calls. + let allow_js = "['api.github.com','github.com','uploads.github.com','raw.githubusercontent.com'].includes(r.host)"; + + let result = HttpjailCommand::new() + .weak() + .js(allow_js) + .verbose(1) + .command(vec!["gh", "api", "-X", "GET", "/zen"]) + .execute(); + + match result { + Ok((exit_code, stdout, stderr)) => { + println!("exit={}\nstderr={}\n", exit_code, stderr); + assert_eq!(exit_code, 0, "gh api exited non-zero: {}", stderr); + assert!( + !stderr.contains("x509:") && !stderr.to_lowercase().contains("certificate signed by unknown authority"), + "TLS verification failed under httpjail: {}", + stderr + ); + assert!( + !stdout.trim().is_empty(), + "Expected non-empty /zen response, got empty stdout" + ); + } + Err(e) => panic!("Failed to execute httpjail with gh: {}", e), + } +} From adc074fc8b84a86ca53c842b73485cf74a60ec48 Mon Sep 17 00:00:00 2001 From: "blink-so[bot]" <211532188+blink-so[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 00:29:06 +0000 Subject: [PATCH 3/3] test(weak): gate imports behind non-macOS cfg to fix clippy on macOS\n\nCo-authored-by: ammario <7416144+ammario@users.noreply.github.com> --- tests/weak_gh_integration.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/weak_gh_integration.rs b/tests/weak_gh_integration.rs index c44db1f8..30da2c77 100644 --- a/tests/weak_gh_integration.rs +++ b/tests/weak_gh_integration.rs @@ -1,6 +1,8 @@ mod common; +#[cfg(not(target_os = "macos"))] use common::HttpjailCommand; +#[cfg(not(target_os = "macos"))] use std::process::Command; // macOS' Go toolchain uses the platform verifier which ignores SSL_CERT_FILE, so