Wire managed MITM CA trust into child env

This commit is contained in:
Winston Howes
2026-05-15 05:21:18 -07:00
parent 60c59733f2
commit a4101509c5
5 changed files with 238 additions and 7 deletions
+2
View File
@@ -3304,6 +3304,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"async-trait",
"base64 0.22.1",
"chrono",
"clap",
"codex-utils-absolute-path",
@@ -3319,6 +3320,7 @@ dependencies = [
"rama-tcp",
"rama-tls-rustls",
"rama-unix",
"rustls-native-certs",
"serde",
"serde_json",
"tempfile",
+2
View File
@@ -15,6 +15,7 @@ workspace = true
[dependencies]
anyhow = { workspace = true }
async-trait = { workspace = true }
base64 = { workspace = true }
clap = { workspace = true, features = ["derive"] }
chrono = { workspace = true }
codex-utils-absolute-path = { workspace = true }
@@ -35,6 +36,7 @@ rama-net = { version = "=0.3.0-alpha.4", features = ["http", "tls"] }
rama-socks5 = { version = "=0.3.0-alpha.4" }
rama-tcp = { version = "=0.3.0-alpha.4", features = ["http"] }
rama-tls-rustls = { version = "=0.3.0-alpha.4", features = ["http"] }
rustls-native-certs = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
+2
View File
@@ -35,6 +35,8 @@ dangerously_allow_non_loopback_proxy = false
mode = "full" # default when unset; use "limited" for read-only mode
# HTTPS MITM is enabled automatically when `mode = "limited"` or when MITM hooks are configured.
# CA cert/key are managed internally under $CODEX_HOME/proxy/ (ca.pem + ca.key).
# When MITM is active, spawned commands receive CA bundle env vars pointing at
# $CODEX_HOME/proxy/ca-bundle.pem so common HTTPS clients trust the managed CA.
# If false, local/private networking is rejected. Explicit allowlisting of local IP literals
# (or `localhost`) is required to permit them.
+145
View File
@@ -1,6 +1,7 @@
use anyhow::Context as _;
use anyhow::Result;
use anyhow::anyhow;
use base64::Engine as _;
use codex_utils_home_dir::find_codex_home;
use rama_net::tls::ApplicationProtocol;
use rama_tls_rustls::dep::pki_types::CertificateDer;
@@ -19,6 +20,7 @@ use rama_tls_rustls::dep::rcgen::PKCS_ECDSA_P256_SHA256;
use rama_tls_rustls::dep::rcgen::SanType;
use rama_tls_rustls::dep::rustls;
use rama_tls_rustls::server::TlsAcceptorData;
use std::collections::HashMap;
use std::fs;
use std::fs::File;
use std::fs::OpenOptions;
@@ -29,6 +31,7 @@ use std::path::PathBuf;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;
use tracing::info;
use tracing::warn;
pub(super) struct ManagedMitmCa {
issuer: Issuer<'static, KeyPair>,
@@ -95,6 +98,20 @@ fn issue_host_certificate_pem(
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: &str = "ca-bundle.pem";
const CUSTOM_CA_ENV_KEYS: &[&str] = &[
"CODEX_CA_CERTIFICATE",
"SSL_CERT_FILE",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"NODE_EXTRA_CA_CERTS",
"GIT_SSL_CAINFO",
"PIP_CERT",
"BUNDLE_SSL_CA_CERT",
"npm_config_cafile",
"NPM_CONFIG_CAFILE",
];
fn managed_ca_paths() -> Result<(PathBuf, PathBuf)> {
let codex_home =
@@ -106,6 +123,85 @@ fn managed_ca_paths() -> Result<(PathBuf, PathBuf)> {
))
}
pub(crate) fn managed_ca_trust_bundle_path(env: &HashMap<String, String>) -> Result<PathBuf> {
let (cert_path, _) = managed_ca_paths()?;
let trust_bundle_path = cert_path
.parent()
.ok_or_else(|| anyhow!("managed MITM CA cert path is missing a parent"))?
.join(MANAGED_MITM_CA_TRUST_BUNDLE);
let trust_bundle = build_managed_ca_trust_bundle(&cert_path, env)?;
write_atomic_replace(
&trust_bundle_path,
trust_bundle.as_bytes(),
/*mode*/ 0o644,
)
.with_context(|| {
format!(
"failed to persist managed MITM CA trust bundle {}",
trust_bundle_path.display()
)
})?;
Ok(trust_bundle_path)
}
fn build_managed_ca_trust_bundle(
managed_ca_cert_path: &Path,
env: &HashMap<String, String>,
) -> Result<String> {
let mut trust_bundle = String::new();
let rustls_native_certs::CertificateResult { certs, errors, .. } =
rustls_native_certs::load_native_certs();
if !errors.is_empty() {
warn!(
native_root_error_count = errors.len(),
"encountered errors while loading native root certificates for MITM trust bundle"
);
}
for cert in certs {
push_certificate_pem(&mut trust_bundle, cert.as_ref());
}
let mut custom_ca_paths = Vec::new();
for key in CUSTOM_CA_ENV_KEYS {
let Some(path) = env.get(*key).filter(|path| !path.is_empty()) else {
continue;
};
let path = PathBuf::from(path);
if path == managed_ca_cert_path || custom_ca_paths.contains(&path) {
continue;
}
custom_ca_paths.push(path);
}
for path in custom_ca_paths {
append_pem_file(&mut trust_bundle, &path)?;
}
append_pem_file(&mut trust_bundle, managed_ca_cert_path)?;
Ok(trust_bundle)
}
fn append_pem_file(bundle: &mut String, path: &Path) -> Result<()> {
if !bundle.ends_with('\n') {
bundle.push('\n');
}
let pem = fs::read_to_string(path)
.with_context(|| format!("failed to read CA bundle {}", path.display()))?;
bundle.push_str(&pem);
if !bundle.ends_with('\n') {
bundle.push('\n');
}
Ok(())
}
fn push_certificate_pem(bundle: &mut String, der: &[u8]) {
bundle.push_str("-----BEGIN CERTIFICATE-----\n");
let encoded = base64::engine::general_purpose::STANDARD.encode(der);
for chunk in encoded.as_bytes().chunks(64) {
bundle.push_str(&String::from_utf8_lossy(chunk));
bundle.push('\n');
}
bundle.push_str("-----END CERTIFICATE-----\n");
}
fn load_or_create_ca() -> Result<(String, String)> {
let (cert_path, key_path) = managed_ca_paths()?;
@@ -238,6 +334,55 @@ fn write_atomic_create_new(path: &Path, contents: &[u8], mode: u32) -> Result<()
Ok(())
}
fn write_atomic_replace(path: &Path, contents: &[u8], mode: u32) -> Result<()> {
if fs::read(path).ok().as_deref() == Some(contents) {
return Ok(());
}
let parent = path
.parent()
.ok_or_else(|| anyhow!("missing parent directory"))?;
fs::create_dir_all(parent).with_context(|| format!("failed to create {}", parent.display()))?;
if fs::symlink_metadata(path)
.ok()
.is_some_and(|metadata| metadata.file_type().is_symlink())
{
return Err(anyhow!("refusing to overwrite symlink {}", path.display()));
}
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let pid = std::process::id();
let file_name = path.file_name().unwrap_or_default().to_string_lossy();
let tmp_path = parent.join(format!(".{file_name}.tmp.{pid}.{nanos}"));
let mut file = open_create_new_with_mode(&tmp_path, mode)?;
file.write_all(contents)
.with_context(|| format!("failed to write {}", tmp_path.display()))?;
file.sync_all()
.with_context(|| format!("failed to fsync {}", tmp_path.display()))?;
drop(file);
#[cfg(windows)]
if path.exists() {
fs::remove_file(path).with_context(|| format!("failed to remove {}", path.display()))?;
}
fs::rename(&tmp_path, path).with_context(|| {
format!(
"failed to rename {} -> {}",
tmp_path.display(),
path.display()
)
})?;
let dir = File::open(parent).with_context(|| format!("failed to open {}", parent.display()))?;
dir.sync_all()
.with_context(|| format!("failed to fsync {}", parent.display()))?;
Ok(())
}
#[cfg(unix)]
fn validate_existing_ca_key_file(path: &Path) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
+87 -7
View File
@@ -12,6 +12,8 @@ use clap::Parser;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::net::TcpListener as StdTcpListener;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::RwLock;
@@ -223,7 +225,7 @@ impl NetworkProxyBuilder {
socks_enabled: current_cfg.network.enable_socks5,
runtime_settings: Arc::new(RwLock::new(NetworkProxyRuntimeSettings::from_config(
&current_cfg,
))),
)?)),
reserved_listeners,
policy_decider: self.policy_decider,
})
@@ -299,15 +301,22 @@ struct NetworkProxyRuntimeSettings {
allow_local_binding: bool,
allow_unix_sockets: Arc<[String]>,
dangerously_allow_all_unix_sockets: bool,
mitm_ca_trust_bundle_path: Option<PathBuf>,
}
impl NetworkProxyRuntimeSettings {
fn from_config(config: &config::NetworkProxyConfig) -> Self {
Self {
fn from_config(config: &config::NetworkProxyConfig) -> Result<Self> {
let mitm_ca_trust_bundle_path = config
.network
.mitm
.then(|| crate::certs::managed_ca_trust_bundle_path(&std::env::vars().collect()))
.transpose()?;
Ok(Self {
allow_local_binding: config.network.allow_local_binding,
allow_unix_sockets: config.network.allow_unix_sockets().into(),
dangerously_allow_all_unix_sockets: config.network.dangerously_allow_all_unix_sockets,
}
mitm_ca_trust_bundle_path,
})
}
}
@@ -403,6 +412,16 @@ pub const PROXY_ENV_KEYS: &[&str] = &[
"all_proxy",
"FTP_PROXY",
"ftp_proxy",
"CODEX_CA_CERTIFICATE",
"SSL_CERT_FILE",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"NODE_EXTRA_CA_CERTS",
"GIT_SSL_CAINFO",
"PIP_CERT",
"BUNDLE_SSL_CA_CERT",
"npm_config_cafile",
"NPM_CONFIG_CAFILE",
];
#[cfg(target_os = "macos")]
@@ -475,6 +494,7 @@ fn apply_proxy_env_overrides(
socks_addr: SocketAddr,
socks_enabled: bool,
allow_local_binding: bool,
mitm_ca_trust_bundle_path: Option<&Path>,
) {
let http_proxy_url = format!("http://{http_addr}");
let socks_proxy_url = format!("socks5h://{socks_addr}");
@@ -552,6 +572,26 @@ fn apply_proxy_env_overrides(
}
}
}
if let Some(mitm_ca_trust_bundle_path) = mitm_ca_trust_bundle_path {
let mitm_ca_trust_bundle_path = mitm_ca_trust_bundle_path.to_string_lossy();
set_env_keys(
env,
&[
"CODEX_CA_CERTIFICATE",
"SSL_CERT_FILE",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"NODE_EXTRA_CA_CERTS",
"GIT_SSL_CAINFO",
"PIP_CERT",
"BUNDLE_SSL_CA_CERT",
"npm_config_cafile",
"NPM_CONFIG_CAFILE",
],
&mitm_ca_trust_bundle_path,
);
}
}
impl NetworkProxy {
@@ -592,7 +632,7 @@ impl NetworkProxy {
}
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
let allow_local_binding = self.allow_local_binding();
let runtime_settings = self.runtime_settings();
// Enforce proxying for child processes. We intentionally override existing values so
// command-level environment cannot bypass the managed proxy endpoint.
apply_proxy_env_overrides(
@@ -600,7 +640,8 @@ impl NetworkProxy {
self.http_addr,
self.socks_addr,
self.socks_enabled,
allow_local_binding,
runtime_settings.allow_local_binding,
runtime_settings.mitm_ca_trust_bundle_path.as_deref(),
);
}
@@ -627,7 +668,7 @@ impl NetworkProxy {
"cannot update network.enable_socks5_udp on a running proxy"
);
let settings = NetworkProxyRuntimeSettings::from_config(&new_state.config);
let settings = NetworkProxyRuntimeSettings::from_config(&new_state.config)?;
self.state.replace_config_state(new_state).await?;
let mut guard = self
.runtime_settings
@@ -975,6 +1016,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
/*socks_enabled*/ true,
/*allow_local_binding*/ false,
/*mitm_ca_trust_bundle_path*/ None,
);
assert_eq!(
@@ -1037,6 +1079,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
/*socks_enabled*/ true,
/*allow_local_binding*/ false,
/*mitm_ca_trust_bundle_path*/ None,
);
for key in env.keys() {
@@ -1049,6 +1092,38 @@ mod tests {
}
}
#[test]
fn apply_proxy_env_overrides_sets_mitm_ca_trust_bundle_vars() {
let mut env = HashMap::new();
let mitm_ca_trust_bundle_path = Path::new("/tmp/codex-proxy/ca-bundle.pem");
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_path),
);
for key in [
"CODEX_CA_CERTIFICATE",
"SSL_CERT_FILE",
"REQUESTS_CA_BUNDLE",
"CURL_CA_BUNDLE",
"NODE_EXTRA_CA_CERTS",
"GIT_SSL_CAINFO",
"PIP_CERT",
"BUNDLE_SSL_CA_CERT",
"npm_config_cafile",
"NPM_CONFIG_CAFILE",
] {
assert_eq!(
env.get(key),
Some(&mitm_ca_trust_bundle_path.display().to_string())
);
}
}
#[test]
fn apply_proxy_env_overrides_uses_http_for_all_proxy_without_socks() {
let mut env = HashMap::new();
@@ -1058,6 +1133,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
/*socks_enabled*/ false,
/*allow_local_binding*/ true,
/*mitm_ca_trust_bundle_path*/ None,
);
assert_eq!(
@@ -1076,6 +1152,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
/*socks_enabled*/ true,
/*allow_local_binding*/ false,
/*mitm_ca_trust_bundle_path*/ None,
);
assert_eq!(
@@ -1124,6 +1201,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081),
/*socks_enabled*/ true,
/*allow_local_binding*/ false,
/*mitm_ca_trust_bundle_path*/ None,
);
assert_eq!(
@@ -1146,6 +1224,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 48081),
/*socks_enabled*/ true,
/*allow_local_binding*/ false,
/*mitm_ca_trust_bundle_path*/ None,
);
assert_eq!(
@@ -1169,6 +1248,7 @@ mod tests {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 48081),
/*socks_enabled*/ true,
/*allow_local_binding*/ false,
/*mitm_ca_trust_bundle_path*/ None,
);
assert_eq!(