Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 12 additions & 22 deletions codex-rs/core/src/tools/runtimes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ use codex_install_context::InstallContext;
#[cfg(target_os = "macos")]
use codex_network_proxy::CODEX_PROXY_GIT_SSH_COMMAND_MARKER;
use codex_network_proxy::CUSTOM_CA_ENV_KEYS;
use codex_network_proxy::MITM_CA_ENV_ACTIVE_ENV_KEY;
use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
use codex_network_proxy::PROXY_ENV_KEYS;
#[cfg(target_os = "macos")]
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
use codex_network_proxy::is_managed_mitm_ca_trust_bundle_path;
use codex_network_proxy::SSL_CERT_DIR_ENV_KEY;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissionProfile;
use codex_sandboxing::SandboxCommand;
Expand Down Expand Up @@ -68,25 +69,7 @@ pub(crate) fn exec_env_for_sandbox_permissions(
}

pub(crate) fn strip_managed_proxy_env(env: &mut HashMap<String, String>) {
for key in PROXY_ENV_KEYS {
env.remove(*key);
}
for key in CUSTOM_CA_ENV_KEYS {
if env
.get(key)
.is_some_and(|value| is_managed_mitm_ca_trust_bundle_path(value))
{
env.remove(key);
}
}
// Only macOS injects a Codex-owned SSH wrapper for the managed SOCKS proxy.
#[cfg(target_os = "macos")]
if env
.get(PROXY_GIT_SSH_COMMAND_ENV_KEY)
.is_some_and(|command| command.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER))
{
env.remove(PROXY_GIT_SSH_COMMAND_ENV_KEY);
}
codex_network_proxy::strip_managed_proxy_env(env);
}

/// Prepends `path_entry` to `PATH`, removing duplicate and empty existing
Expand Down Expand Up @@ -336,10 +319,17 @@ fn build_proxy_env_exports() -> (String, String) {
let (captures, restores) =
build_override_exports_for_keys("__CODEX_SNAPSHOT_PROXY_OVERRIDE", &keys);
let key = PROXY_ACTIVE_ENV_KEY;
let (ssl_cert_dir_captures, ssl_cert_dir_restores) = build_override_exports_for_keys(
"__CODEX_SNAPSHOT_MITM_CA_OVERRIDE",
&[SSL_CERT_DIR_ENV_KEY],
);
let mitm_ca_key = MITM_CA_ENV_ACTIVE_ENV_KEY;
let proxy_blocks = (
format!("{captures}\n__CODEX_SNAPSHOT_PROXY_ENV_SET=\"${{{key}+x}}\""),
format!(
"if [ -n \"$__CODEX_SNAPSHOT_PROXY_ENV_SET\" ] || [ -n \"${{{key}+x}}\" ]; then\n{restores}\nfi"
"{captures}\n__CODEX_SNAPSHOT_PROXY_ENV_SET=\"${{{key}+x}}\"\n{ssl_cert_dir_captures}\n__CODEX_SNAPSHOT_MITM_CA_ENV_SET=\"${{{mitm_ca_key}+x}}\""
),
format!(
"__CODEX_SNAPSHOT_MITM_CA_ENV_AFTER_SET=\"${{{mitm_ca_key}+x}}\"\nif [ -n \"$__CODEX_SNAPSHOT_PROXY_ENV_SET\" ] || [ -n \"${{{key}+x}}\" ]; then\n{restores}\nfi\nif [ -n \"$__CODEX_SNAPSHOT_MITM_CA_ENV_SET\" ] || [ -n \"$__CODEX_SNAPSHOT_MITM_CA_ENV_AFTER_SET\" ]; then\n{ssl_cert_dir_restores}\nfi"
),
);
let git_blocks = build_codex_proxy_git_ssh_command_exports();
Expand Down
57 changes: 57 additions & 0 deletions codex-rs/core/src/tools/runtimes/mod_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use codex_network_proxy::CUSTOM_CA_ENV_KEYS;
use codex_network_proxy::ConfigReloader;
use codex_network_proxy::ConfigReloaderFuture;
use codex_network_proxy::ConfigState;
use codex_network_proxy::MITM_CA_ENV_ACTIVE_ENV_KEY;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyConfig;
use codex_network_proxy::NetworkProxyConstraints;
Expand All @@ -19,6 +20,7 @@ use codex_network_proxy::PROXY_ACTIVE_ENV_KEY;
use codex_network_proxy::PROXY_ENV_KEYS;
#[cfg(target_os = "macos")]
use codex_network_proxy::PROXY_GIT_SSH_COMMAND_ENV_KEY;
use codex_network_proxy::SSL_CERT_DIR_ENV_KEY;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::PermissionProfile;
use codex_sandboxing::SandboxManager;
Expand Down Expand Up @@ -580,6 +582,61 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_proxy_env_from_process_env() {
);
}

#[test]
fn maybe_wrap_shell_lc_with_snapshot_restores_ssl_cert_dir_for_mitm_state() {
let cases = [
(false, Some("1"), "unset"),
(false, None, "/tmp/snapshot-certs"),
(true, None, "unset"),
];

for (index, (snapshot_mitm, live_mitm, expected)) in cases.into_iter().enumerate() {
let dir = tempdir().expect("create temp dir");
let snapshot_path = dir.path().join(format!("snapshot-{index}.sh"));
let snapshot_mitm = if snapshot_mitm {
format!("export {MITM_CA_ENV_ACTIVE_ENV_KEY}=1\n")
} else {
Default::default()
};
std::fs::write(
&snapshot_path,
format!(
"# Snapshot file\n{snapshot_mitm}export {SSL_CERT_DIR_ENV_KEY}='/tmp/snapshot-certs'\n"
),
)
.expect("write snapshot");
let (session_shell, shell_snapshot) =
shell_with_snapshot(ShellType::Bash, "/bin/bash", snapshot_path.abs());
let command = vec![
"/bin/bash".to_string(),
"-lc".to_string(),
format!("printf '%s' \"${{{SSL_CERT_DIR_ENV_KEY}-unset}}\""),
];
let rewritten = maybe_wrap_shell_lc_with_snapshot(
&command,
&session_shell,
Some(&shell_snapshot),
&HashMap::new(),
&HashMap::new(),
&RuntimePathPrepends::default(),
);
let mut command = Command::new(&rewritten[0]);
command
.args(&rewritten[1..])
.env(PROXY_ACTIVE_ENV_KEY, "1")
.env_remove(SSL_CERT_DIR_ENV_KEY);
if let Some(live_mitm) = live_mitm {
command.env(MITM_CA_ENV_ACTIVE_ENV_KEY, live_mitm);
} else {
command.env_remove(MITM_CA_ENV_ACTIVE_ENV_KEY);
}
let output = command.output().expect("run rewritten command");

assert!(output.status.success(), "command failed: {output:?}");
assert_eq!(String::from_utf8_lossy(&output.stdout), expected);
}
}

#[cfg(target_os = "macos")]
#[test]
fn maybe_wrap_shell_lc_with_snapshot_refreshes_codex_proxy_git_ssh_command() {
Expand Down
142 changes: 142 additions & 0 deletions codex-rs/network-proxy/src/child_env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use super::NetworkProxyRuntimeSettings;
use super::STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY;
use super::apply_proxy_env_overrides;
use super::ca_env_keys;
use super::is_tracked_startup_ca_env_key;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::HashMap;
use std::net::SocketAddr;
use std::path::Path;
use tracing::warn;

/// Immutable proxy settings used to prepare one child process environment.
///
/// Keeping the managed CA path and environment rewrite on the same snapshot
/// prevents a live proxy configuration reload from changing the MITM state in
/// the middle of sandbox policy construction.
#[derive(Clone)]
pub struct NetworkProxyChildEnvSnapshot {
http_addr: SocketAddr,
socks_addr: SocketAddr,
socks_enabled: bool,
runtime_settings: NetworkProxyRuntimeSettings,
}

impl NetworkProxyChildEnvSnapshot {
pub(super) fn new(
http_addr: SocketAddr,
socks_addr: SocketAddr,
socks_enabled: bool,
runtime_settings: NetworkProxyRuntimeSettings,
) -> Self {
Self {
http_addr,
socks_addr,
socks_enabled,
runtime_settings,
}
}

pub fn has_managed_mitm_ca(&self) -> bool {
self.runtime_settings.mitm_ca_trust_bundle.is_some()
}

/// Returns the generated MITM CA bundle path this snapshot will expose.
pub fn managed_mitm_ca_trust_bundle_path(&self) -> Option<AbsolutePathBuf> {
self.runtime_settings
.mitm_ca_trust_bundle
.as_ref()
.and_then(|bundle| {
AbsolutePathBuf::from_absolute_path(&bundle.path)
.map_err(|err| warn!("managed MITM CA trust bundle path is invalid: {err}"))
.ok()
})
}

pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
apply_proxy_env_overrides(
env,
self.http_addr,
self.socks_addr,
self.socks_enabled,
self.runtime_settings.allow_local_binding,
self.runtime_settings.mitm_ca_trust_bundle.as_ref(),
);
}

/// Prepares a child environment without creating a command-specific CA bundle.
///
/// Persistent sandbox identities cannot safely receive a read grant for a
/// derived bundle because later commands would retain that grant. Preserve
/// the pre-materialization behavior there and expose only the stable
/// managed baseline path.
pub fn prepare_persistent_sandbox_child_env(
&self,
env: &mut HashMap<String, String>,
) -> Vec<AbsolutePathBuf> {
self.apply_to_env(env);
env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY);
self.managed_mitm_ca_trust_bundle_path()
.into_iter()
.collect()
}

/// Returns whether this child would need a command-specific managed CA bundle.
///
/// Persistent sandbox identities must reject this shape rather than grant
/// access to a derived bundle that later commands could continue reading.
pub fn requires_child_specific_mitm_ca_bundle(&self, env: &HashMap<String, String>) -> bool {
let Some(mitm_ca_trust_bundle) = self.runtime_settings.mitm_ca_trust_bundle.as_ref() else {
return false;
};
if env
.get(crate::certs::SSL_CERT_DIR_ENV_KEY)
.is_some_and(|value| !value.is_empty())
{
// The stable Windows baseline only embeds file-backed startup CA
// overrides. Directory contents can change after proxy startup,
// so they still require per-child materialization and cannot be
// exposed through a persistent sandbox identity.
return true;
}

let managed_path = mitm_ca_trust_bundle.path.to_string_lossy();
crate::certs::CUSTOM_CA_ENV_KEYS.into_iter().any(|key| {
env.get(key)
.filter(|value| !value.is_empty())
.is_some_and(|value| {
value != managed_path.as_ref()
&& mitm_ca_trust_bundle.startup_env_values.get(key) != Some(value)
})
})
}

/// Rewrites readable child-selected CA bundles into immutable managed MITM bundles.
pub fn prepare_child_env<F>(
&self,
env: &mut HashMap<String, String>,
cwd: &Path,
can_read_path: F,
) -> Vec<AbsolutePathBuf>
where
F: Fn(&Path) -> bool,
{
self.apply_to_env(env);
let startup_ca_env_keys_present_in_child = ca_env_keys()
.filter(|&key| is_tracked_startup_ca_env_key(env, key))
.collect::<Vec<_>>();
env.remove(STARTUP_CA_ENV_KEYS_PRESENT_ENV_KEY);
self.runtime_settings
.mitm_ca_trust_bundle
.as_ref()
.map_or_else(Vec::new, |mitm_ca_trust_bundle| {
crate::child_ca::prepare_mitm_ca_trust_bundle_env(
mitm_ca_trust_bundle,
env,
cwd,
&startup_ca_env_keys_present_in_child,
can_read_path,
)
})
}
}
4 changes: 3 additions & 1 deletion codex-rs/network-proxy/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#![deny(clippy::print_stdout, clippy::print_stderr)]
#![allow(dead_code)]

mod certs;
mod child_ca;
mod config;
mod connect_policy;
mod http_proxy;
mod managed_env;
mod mitm;
mod mitm_hook;
mod native_certs;
Expand All @@ -30,6 +30,7 @@ pub use config::NetworkProxyConfig;
pub use config::NetworkUnixSocketPermission;
pub use config::NetworkUnixSocketPermissions;
pub use config::host_and_port_from_network_addr;
pub use managed_env::strip_managed_proxy_env;
pub use mitm_hook::InjectedHeaderConfig;
pub use mitm_hook::MitmHookActionsConfig;
pub use mitm_hook::MitmHookBodyConfig;
Expand All @@ -54,6 +55,7 @@ pub use proxy::MITM_CA_ENV_ACTIVE_ENV_KEY;
pub use proxy::NO_PROXY_ENV_KEYS;
pub use proxy::NetworkProxy;
pub use proxy::NetworkProxyBuilder;
pub use proxy::NetworkProxyChildEnvSnapshot;
pub use proxy::NetworkProxyHandle;
pub use proxy::PROXY_ACTIVE_ENV_KEY;
pub use proxy::PROXY_ENV_KEYS;
Expand Down
36 changes: 36 additions & 0 deletions codex-rs/network-proxy/src/managed_env.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#[cfg(target_os = "macos")]
use crate::CODEX_PROXY_GIT_SSH_COMMAND_MARKER;
use crate::CUSTOM_CA_ENV_KEYS;
use crate::PROXY_ENV_KEYS;
#[cfg(target_os = "macos")]
use crate::PROXY_GIT_SSH_COMMAND_ENV_KEY;
use crate::is_managed_mitm_ca_trust_bundle_path;
use std::collections::HashMap;

/// Removes environment values owned by a managed network proxy before a command escapes its
/// sandbox and proxy containment.
pub fn strip_managed_proxy_env(env: &mut HashMap<String, String>) {
for key in PROXY_ENV_KEYS {
env.remove(*key);
}
for key in CUSTOM_CA_ENV_KEYS {
if env
.get(key)
.is_some_and(|value| is_managed_mitm_ca_trust_bundle_path(value))
{
env.remove(key);
}
}
// Only macOS injects a Codex-owned SSH wrapper for the managed SOCKS proxy.
#[cfg(target_os = "macos")]
if env
.get(PROXY_GIT_SSH_COMMAND_ENV_KEY)
.is_some_and(|command| command.starts_with(CODEX_PROXY_GIT_SSH_COMMAND_MARKER))
{
env.remove(PROXY_GIT_SSH_COMMAND_ENV_KEY);
}
}

#[cfg(test)]
#[path = "managed_env_tests.rs"]
mod tests;
40 changes: 40 additions & 0 deletions codex-rs/network-proxy/src/managed_env_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use super::*;
use crate::PROXY_ACTIVE_ENV_KEY;
use pretty_assertions::assert_eq;

#[test]
fn strips_managed_proxy_env() {
let mut env = HashMap::from([
(PROXY_ACTIVE_ENV_KEY.to_string(), "1".to_string()),
(
"HTTPS_PROXY".to_string(),
"http://127.0.0.1:1234".to_string(),
),
("CUSTOM_ENV".to_string(), "kept".to_string()),
]);

strip_managed_proxy_env(&mut env);

assert_eq!(
env,
HashMap::from([("CUSTOM_ENV".to_string(), "kept".to_string())])
);
}

#[test]
fn preserves_unmanaged_ca_env() {
let mut env = HashMap::from([(
"SSL_CERT_FILE".to_string(),
"/tmp/user-ca-bundle.pem".to_string(),
)]);

strip_managed_proxy_env(&mut env);

assert_eq!(
env,
HashMap::from([(
"SSL_CERT_FILE".to_string(),
"/tmp/user-ca-bundle.pem".to_string(),
)])
);
}
Loading
Loading