mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Wire managed MITM CA trust into child env
This commit is contained in:
Generated
+2
@@ -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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
¤t_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!(
|
||||
|
||||
Reference in New Issue
Block a user