Track startup MITM CA env

This commit is contained in:
Winston Howes
2026-06-03 22:15:30 -07:00
parent 5891f041b2
commit 130450a9fa
3 changed files with 91 additions and 16 deletions

View File

@@ -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<ManagedMitmCaTrustBundle> {
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()),
])
);
}

View File

@@ -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;

View File

@@ -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::<Vec<_>>();
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<Item = &'static str> {
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<String, String>, 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<String, String>) {
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]