From 130450a9fac4e2269cefd8d9c4f104ed316fcb7a Mon Sep 17 00:00:00 2001 From: Winston Howes Date: Wed, 3 Jun 2026 22:15:30 -0700 Subject: [PATCH] Track startup MITM CA env --- codex-rs/network-proxy/src/certs.rs | 19 ++++++- codex-rs/network-proxy/src/lib.rs | 2 + codex-rs/network-proxy/src/proxy.rs | 86 ++++++++++++++++++++++++----- 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/codex-rs/network-proxy/src/certs.rs b/codex-rs/network-proxy/src/certs.rs index f316e2c21f75..f99ab36e2715 100644 --- a/codex-rs/network-proxy/src/certs.rs +++ b/codex-rs/network-proxy/src/certs.rs @@ -101,12 +101,14 @@ const MANAGED_MITM_CA_DIR: &str = "proxy"; const MANAGED_MITM_CA_CERT: &str = "ca.pem"; const MANAGED_MITM_CA_KEY: &str = "ca.key"; const MANAGED_MITM_CA_TRUST_BUNDLE_PREFIX: &str = "ca-bundle"; +const SSL_CERT_FILE_ENV_KEY: &str = "SSL_CERT_FILE"; +pub const SSL_CERT_DIR_ENV_KEY: &str = "SSL_CERT_DIR"; // Best-effort compatibility set for common child toolchains that accept a CA bundle path. // This is intentionally curated rather than pretending to cover every TLS client. pub const CUSTOM_CA_ENV_KEYS: [&str; 10] = [ "CODEX_CA_CERTIFICATE", - "SSL_CERT_FILE", + SSL_CERT_FILE_ENV_KEY, "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "NODE_EXTRA_CA_CERTS", @@ -122,6 +124,7 @@ pub const CUSTOM_CA_ENV_KEYS: [&str; 10] = [ pub(crate) struct ManagedMitmCaTrustBundle { pub(crate) path: PathBuf, pub(crate) startup_env_values: HashMap<&'static str, String>, + pub(crate) startup_cwd: PathBuf, } fn managed_ca_paths() -> Result<(PathBuf, PathBuf)> { @@ -146,8 +149,11 @@ fn managed_ca_trust_bundle_for_cert_path( cert_path: &Path, env: &HashMap<&'static str, String>, ) -> Result { + let startup_cwd = + std::env::current_dir().context("failed to resolve startup cwd for managed MITM CA")?; let startup_env_values = CUSTOM_CA_ENV_KEYS .into_iter() + .chain(std::iter::once(SSL_CERT_DIR_ENV_KEY)) .filter_map(|key| { env.get(key) .filter(|value| !value.is_empty()) @@ -160,6 +166,7 @@ fn managed_ca_trust_bundle_for_cert_path( Ok(ManagedMitmCaTrustBundle { path, startup_env_values, + startup_cwd, }) } @@ -512,12 +519,18 @@ mod tests { let dir = tempdir().unwrap(); let managed_ca_cert_path = dir.path().join("ca.pem"); fs::write(&managed_ca_cert_path, "managed ca\n").unwrap(); - let env = HashMap::from([("SSL_CERT_FILE", "/tmp/startup-ca.pem".to_string())]); + let env = HashMap::from([ + ("SSL_CERT_FILE", "/tmp/startup-ca.pem".to_string()), + (SSL_CERT_DIR_ENV_KEY, "/tmp/startup-certs".to_string()), + ]); let trust_bundle = managed_ca_trust_bundle_for_cert_path(&managed_ca_cert_path, &env).unwrap(); assert_eq!( trust_bundle.startup_env_values, - HashMap::from([("SSL_CERT_FILE", "/tmp/startup-ca.pem".to_string())]) + HashMap::from([ + ("SSL_CERT_FILE", "/tmp/startup-ca.pem".to_string()), + (SSL_CERT_DIR_ENV_KEY, "/tmp/startup-certs".to_string()), + ]) ); } diff --git a/codex-rs/network-proxy/src/lib.rs b/codex-rs/network-proxy/src/lib.rs index eb641b81d4ba..dfc9c071157d 100644 --- a/codex-rs/network-proxy/src/lib.rs +++ b/codex-rs/network-proxy/src/lib.rs @@ -18,6 +18,7 @@ mod state; mod upstream; pub use certs::CUSTOM_CA_ENV_KEYS; +pub use certs::SSL_CERT_DIR_ENV_KEY; pub use certs::is_managed_mitm_ca_trust_bundle_path; pub use config::NetworkDomainPermission; pub use config::NetworkDomainPermissionEntry; @@ -46,6 +47,7 @@ pub use proxy::Args; #[cfg(target_os = "macos")] pub use proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER; pub use proxy::DEFAULT_NO_PROXY_VALUE; +pub use proxy::MITM_CA_ENV_ACTIVE_ENV_KEY; pub use proxy::NO_PROXY_ENV_KEYS; pub use proxy::NetworkProxy; pub use proxy::NetworkProxyBuilder; diff --git a/codex-rs/network-proxy/src/proxy.rs b/codex-rs/network-proxy/src/proxy.rs index c3685a4310aa..61b0639cf1c4 100644 --- a/codex-rs/network-proxy/src/proxy.rs +++ b/codex-rs/network-proxy/src/proxy.rs @@ -308,6 +308,7 @@ impl NetworkProxyRuntimeSettings { let mitm_ca_trust_bundle = if config.network.mitm { let env = crate::certs::CUSTOM_CA_ENV_KEYS .into_iter() + .chain(std::iter::once(crate::certs::SSL_CERT_DIR_ENV_KEY)) .filter_map(|key| std::env::var(key).ok().map(|value| (key, value))) .collect(); Some(crate::certs::managed_ca_trust_bundle(&env)?) @@ -376,6 +377,8 @@ pub const PROXY_URL_ENV_KEYS: &[&str] = &[ pub const ALL_PROXY_ENV_KEYS: &[&str] = &["ALL_PROXY", "all_proxy"]; pub const PROXY_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_ACTIVE"; +pub const MITM_CA_ENV_ACTIVE_ENV_KEY: &str = "CODEX_NETWORK_PROXY_MITM_CA_ENV_ACTIVE"; +const STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY: &str = "CODEX_NETWORK_PROXY_STARTUP_CA_ENV_KEYS_PRESENT"; pub const ALLOW_LOCAL_BINDING_ENV_KEY: &str = "CODEX_NETWORK_ALLOW_LOCAL_BINDING"; const ELECTRON_GET_USE_PROXY_ENV_KEY: &str = "ELECTRON_GET_USE_PROXY"; const NODE_USE_ENV_PROXY_ENV_KEY: &str = "NODE_USE_ENV_PROXY"; @@ -383,6 +386,8 @@ const NODE_USE_ENV_PROXY_ENV_KEY: &str = "NODE_USE_ENV_PROXY"; const GIT_SSH_COMMAND_ENV_KEY: &str = "GIT_SSH_COMMAND"; pub const PROXY_ENV_KEYS: &[&str] = &[ PROXY_ACTIVE_ENV_KEY, + MITM_CA_ENV_ACTIVE_ENV_KEY, + STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY, ALLOW_LOCAL_BINDING_ENV_KEY, ELECTRON_GET_USE_PROXY_ENV_KEY, NODE_USE_ENV_PROXY_ENV_KEY, @@ -571,27 +576,55 @@ fn apply_proxy_env_overrides( } if let Some(mitm_ca_trust_bundle) = mitm_ca_trust_bundle { + env.insert(MITM_CA_ENV_ACTIVE_ENV_KEY.to_string(), "1".to_string()); let managed_path = mitm_ca_trust_bundle.path.to_string_lossy().into_owned(); + let startup_ca_env_keys_present_in_child = ca_env_keys() + .filter(|&key| { + env.get(key) + .filter(|value| !value.is_empty()) + .is_some_and(|value| { + mitm_ca_trust_bundle.startup_env_values.get(key) == Some(value) + || (value == &managed_path && is_tracked_startup_ca_env_key(env, key)) + }) + }) + .collect::>(); + if startup_ca_env_keys_present_in_child.is_empty() { + env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY); + } else { + env.insert( + STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY.to_string(), + startup_ca_env_keys_present_in_child.join(","), + ); + } for key in crate::certs::CUSTOM_CA_ENV_KEYS { if env .get(key) .filter(|value| !value.is_empty()) .is_some_and(|value| { - value != &managed_path - && mitm_ca_trust_bundle.startup_env_values.get(key) != Some(value) + value != &managed_path && !startup_ca_env_keys_present_in_child.contains(&key) }) { - // TODO(winston): Materialize policy-checked per-child bundles for readable - // startup and command-scoped CA overrides. For now startup overrides are - // replaced with the default bundle and later command-scoped overrides are - // preserved, either of which can make intercepted TLS fail. continue; } env.insert(key.to_string(), managed_path.clone()); } + } else { + env.remove(MITM_CA_ENV_ACTIVE_ENV_KEY); + env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY); } } +fn ca_env_keys() -> impl Iterator { + crate::certs::CUSTOM_CA_ENV_KEYS + .into_iter() + .chain(std::iter::once(crate::certs::SSL_CERT_DIR_ENV_KEY)) +} + +fn is_tracked_startup_ca_env_key(env: &HashMap, key: &str) -> bool { + env.get(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY) + .is_some_and(|keys| keys.split(',').any(|tracked_key| tracked_key == key)) +} + impl NetworkProxy { pub fn builder() -> NetworkProxyBuilder { NetworkProxyBuilder::default() @@ -643,7 +676,7 @@ impl NetworkProxy { pub fn apply_to_env(&self, env: &mut HashMap) { let runtime_settings = self.runtime_settings(); // Enforce proxying for child processes. Proxy endpoint values are always rewritten; - // managed MITM CA vars preserve child-scoped overrides after proxy startup. + // managed MITM CA vars preserve command-scoped overrides after proxy startup. apply_proxy_env_overrides( env, self.http_addr, @@ -1110,6 +1143,7 @@ mod tests { let mitm_ca_trust_bundle = crate::certs::ManagedMitmCaTrustBundle { path: mitm_ca_trust_bundle_path.to_path_buf(), startup_env_values: HashMap::new(), + startup_cwd: Path::new("/tmp").to_path_buf(), }; apply_proxy_env_overrides( &mut env, @@ -1129,18 +1163,18 @@ mod tests { } #[test] - fn apply_proxy_env_overrides_preserves_command_scoped_mitm_ca_override() { - let command_ca_bundle_path = "/tmp/command-ca.pem".to_string(); + fn apply_proxy_env_overrides_tracks_rewritten_startup_mitm_ca_override() { + let startup_ca_bundle_path = "/tmp/startup-ca.pem".to_string(); let mut env = HashMap::from([( "REQUESTS_CA_BUNDLE".to_string(), - command_ca_bundle_path.clone(), + startup_ca_bundle_path.clone(), )]); let mitm_ca_trust_bundle_path = Path::new("/tmp/codex-proxy/ca-bundle.pem"); let mitm_ca_trust_bundle = crate::certs::ManagedMitmCaTrustBundle { path: mitm_ca_trust_bundle_path.to_path_buf(), - startup_env_values: HashMap::new(), + startup_env_values: HashMap::from([("REQUESTS_CA_BUNDLE", startup_ca_bundle_path)]), + startup_cwd: Path::new("/tmp").to_path_buf(), }; - apply_proxy_env_overrides( &mut env, SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), @@ -1150,11 +1184,37 @@ mod tests { Some(&mitm_ca_trust_bundle), ); - assert_eq!(env.get("REQUESTS_CA_BUNDLE"), Some(&command_ca_bundle_path)); + assert_eq!( + env.get("REQUESTS_CA_BUNDLE"), + Some(&mitm_ca_trust_bundle_path.display().to_string()) + ); + assert_eq!( + env.get(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY), + Some(&"REQUESTS_CA_BUNDLE".to_string()) + ); assert_eq!( env.get("SSL_CERT_FILE"), Some(&mitm_ca_trust_bundle_path.display().to_string()) ); + + env.insert( + "REQUESTS_CA_BUNDLE".to_string(), + "/tmp/command-ca.pem".to_string(), + ); + apply_proxy_env_overrides( + &mut env, + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 3128), + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081), + /*socks_enabled*/ true, + /*allow_local_binding*/ false, + Some(&mitm_ca_trust_bundle), + ); + + assert_eq!( + env.get("REQUESTS_CA_BUNDLE"), + Some(&"/tmp/command-ca.pem".to_string()) + ); + assert_eq!(env.get(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY), None); } #[test]