Prepare managed network sandbox context (#29456)

## Why

Managed network configures commands to use local HTTP and SOCKS proxies.
For commands delegated to the exec server, the proxy environment and the
sandbox policy were prepared separately. On macOS, that meant a command
could receive `HTTPS_PROXY=http://127.0.0.1:43123` while Seatbelt still
denied access to port `43123`.

## What changed

`NetworkProxy` now prepares the command environment and sandbox context
together from the same runtime snapshot:

```text
Prepared managed network
├── command environment: HTTPS_PROXY=http://127.0.0.1:43123
└── sandbox context: allow outbound to 127.0.0.1:43123
```

That context travels with remote exec requests. The exec server
preserves the managed proxy and CA environment, and macOS Seatbelt
allows only the prepared loopback proxy ports without enabling broad
network access or local binding.

The protocol field is optional and the existing enforcement flag remains
in place, preserving compatibility with callers that do not send the new
context.
This commit is contained in:
jif
2026-06-23 20:07:09 +01:00
committed by GitHub
Unverified
parent 8d80b0176a
commit e476fc16ce
30 changed files with 472 additions and 78 deletions
+5
View File
@@ -13,6 +13,7 @@ use crate::resolve_windows_elevated_filesystem_overrides;
use crate::resolve_windows_restricted_token_filesystem_overrides;
#[cfg(target_os = "windows")]
use crate::windows_sandbox_uses_elevated_backend;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkProxy;
use codex_protocol::config_types::WindowsSandboxLevel;
use codex_protocol::models::AdditionalPermissionProfile;
@@ -99,6 +100,7 @@ pub struct SandboxCommand {
pub args: Vec<String>,
pub cwd: PathUri,
pub env: HashMap<String, String>,
pub managed_network: Option<ManagedNetworkSandboxContext>,
pub additional_permissions: Option<AdditionalPermissionProfile>,
}
@@ -332,6 +334,8 @@ impl SandboxManager {
windows_sandbox_level,
windows_sandbox_private_desktop,
} = request;
#[cfg(target_os = "macos")]
let managed_network = command.managed_network.as_ref();
let additional_permissions = command.additional_permissions.take();
let managed_mitm_ca_trust_bundle_path =
network.and_then(NetworkProxy::managed_mitm_ca_trust_bundle_path);
@@ -364,6 +368,7 @@ impl SandboxManager {
network_sandbox_policy: pending.effective_network_policy,
sandbox_policy_cwd: pending.native_sandbox_policy_cwd.as_path(),
enforce_managed_network,
managed_network,
environment_id,
network,
extra_allow_unix_sockets: &[],
+5
View File
@@ -92,6 +92,7 @@ fn unsandboxed_transform_preserves_foreign_cwd_and_unrestricted_file_system_poli
args: Vec::new(),
cwd: cwd_uri.clone(),
env: HashMap::new(),
managed_network: None,
additional_permissions: None,
},
permissions: &permissions,
@@ -139,6 +140,7 @@ fn transform_additional_permissions_enable_network_for_external_sandbox() {
args: Vec::new(),
cwd: cwd_uri.clone(),
env: HashMap::new(),
managed_network: None,
additional_permissions: Some(AdditionalPermissionProfile {
network: Some(NetworkPermissions {
enabled: Some(true),
@@ -211,6 +213,7 @@ fn transform_additional_permissions_preserves_denied_entries() {
args: Vec::new(),
cwd: cwd_uri.clone(),
env: HashMap::new(),
managed_network: None,
additional_permissions: Some(AdditionalPermissionProfile {
file_system: Some(FileSystemPermissions::from_read_write_roots(
/*read*/ None,
@@ -314,6 +317,7 @@ fn transform_linux_seccomp_request(
args: Vec::new(),
cwd: cwd_uri.clone(),
env: HashMap::new(),
managed_network: None,
additional_permissions: None,
},
permissions: &permissions,
@@ -504,6 +508,7 @@ fn transform_for_direct_spawn_windows_materializes_inner_helper() {
"Path".to_string(),
r"C:\Windows\System32".to_string(),
)]),
managed_network: None,
additional_permissions: None,
},
permissions: &permissions,
+47 -25
View File
@@ -1,3 +1,4 @@
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::PROXY_URL_ENV_KEYS;
use codex_network_proxy::has_proxy_url_env_vars;
@@ -103,6 +104,7 @@ struct UnixSocketPathParam {
}
fn proxy_policy_inputs(
managed_network: Option<&ManagedNetworkSandboxContext>,
network: Option<&NetworkProxy>,
environment_id: Option<&str>,
extra_allow_unix_sockets: &[AbsolutePathBuf],
@@ -112,33 +114,47 @@ fn proxy_policy_inputs(
.filter_map(|socket_path| normalize_path_for_sandbox(socket_path.as_path()))
.collect::<Vec<_>>();
let unix_domain_socket_policy = match network {
Some(network) if network.dangerously_allow_all_unix_sockets() => {
UnixDomainSocketPolicy::AllowAll
}
Some(network) => {
let mut allowed = network
.allow_unix_sockets()
.iter()
.filter_map(|socket_path| {
match normalize_path_for_sandbox(Path::new(socket_path)) {
Some(path) => Some(path),
None => {
warn!(
"ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}"
);
None
}
}
})
.collect::<Vec<_>>();
allowed.extend(extra_allowed);
UnixDomainSocketPolicy::Restricted { allowed }
}
None => UnixDomainSocketPolicy::Restricted {
allowed: extra_allowed,
},
};
if let Some(managed_network) = managed_network {
return Ok(ProxyPolicyInputs {
ports: managed_network.loopback_ports.clone(),
has_proxy_config: true,
allow_local_binding: managed_network.allow_local_binding,
unix_domain_socket_policy,
});
}
match network {
Some(network) => {
let mut env = HashMap::new();
network
.apply_to_env_for_optional_environment(&mut env, environment_id)
.map_err(|err| err.to_string())?;
let unix_domain_socket_policy = if network.dangerously_allow_all_unix_sockets() {
UnixDomainSocketPolicy::AllowAll
} else {
let mut allowed = network
.allow_unix_sockets()
.iter()
.filter_map(|socket_path| {
match normalize_path_for_sandbox(Path::new(socket_path)) {
Some(path) => Some(path),
None => {
warn!(
"ignoring network.allow_unix_sockets entry because it could not be normalized: {socket_path}"
);
None
}
}
})
.collect::<Vec<_>>();
allowed.extend(extra_allowed);
UnixDomainSocketPolicy::Restricted { allowed }
};
Ok(ProxyPolicyInputs {
ports: proxy_loopback_ports_from_env(&env),
has_proxy_config: has_proxy_url_env_vars(&env),
@@ -147,9 +163,7 @@ fn proxy_policy_inputs(
})
}
None => Ok(ProxyPolicyInputs {
unix_domain_socket_policy: UnixDomainSocketPolicy::Restricted {
allowed: extra_allowed,
},
unix_domain_socket_policy,
..Default::default()
}),
}
@@ -586,6 +600,7 @@ fn create_seatbelt_command_args_for_legacy_policy(
network_sandbox_policy: NetworkSandboxPolicy::from(sandbox_policy),
sandbox_policy_cwd,
enforce_managed_network,
managed_network: None,
environment_id: None,
network,
extra_allow_unix_sockets: &[],
@@ -599,6 +614,7 @@ pub struct CreateSeatbeltCommandArgsParams<'a> {
pub network_sandbox_policy: NetworkSandboxPolicy,
pub sandbox_policy_cwd: &'a Path,
pub enforce_managed_network: bool,
pub managed_network: Option<&'a ManagedNetworkSandboxContext>,
pub environment_id: Option<&'a str>,
pub network: Option<&'a NetworkProxy>,
pub extra_allow_unix_sockets: &'a [AbsolutePathBuf],
@@ -613,6 +629,7 @@ pub fn create_seatbelt_command_args(
network_sandbox_policy,
sandbox_policy_cwd,
enforce_managed_network,
managed_network,
environment_id,
network,
extra_allow_unix_sockets,
@@ -709,7 +726,12 @@ pub fn create_seatbelt_command_args(
}
};
let proxy = proxy_policy_inputs(network, environment_id, extra_allow_unix_sockets)?;
let proxy = proxy_policy_inputs(
managed_network,
network,
environment_id,
extra_allow_unix_sockets,
)?;
let network_policy =
dynamic_network_policy_for_network(network_sandbox_policy, enforce_managed_network, &proxy);
+37
View File
@@ -14,6 +14,7 @@ use super::unix_socket_policy;
use codex_network_proxy::ConfigReloader;
use codex_network_proxy::ConfigReloaderFuture;
use codex_network_proxy::ConfigState;
use codex_network_proxy::ManagedNetworkSandboxContext;
use codex_network_proxy::NetworkMode;
use codex_network_proxy::NetworkProxy;
use codex_network_proxy::NetworkProxyConfig;
@@ -205,6 +206,7 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access()
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: Path::new("/"),
enforce_managed_network: false,
managed_network: None,
environment_id: None,
network: None,
extra_allow_unix_sockets: &[],
@@ -258,6 +260,37 @@ fn explicit_unreadable_paths_are_excluded_from_full_disk_read_and_write_access()
);
}
#[test]
fn prepared_managed_network_context_allows_only_its_proxy_ports() {
let file_system_policy = FileSystemSandboxPolicy::from_legacy_sandbox_policy_for_cwd(
&SandboxPolicy::new_read_only_policy(),
Path::new("/"),
);
let managed_network = ManagedNetworkSandboxContext {
loopback_ports: vec![43123, 48081],
allow_local_binding: false,
};
let args = create_seatbelt_command_args(CreateSeatbeltCommandArgsParams {
command: vec!["/bin/true".to_string()],
file_system_sandbox_policy: &file_system_policy,
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: Path::new("/"),
enforce_managed_network: true,
managed_network: Some(&managed_network),
environment_id: None,
network: None,
extra_allow_unix_sockets: &[],
})
.unwrap();
let policy = seatbelt_policy_arg(&args);
assert!(policy.contains("(allow network-outbound (remote ip \"localhost:43123\"))"));
assert!(policy.contains("(allow network-outbound (remote ip \"localhost:48081\"))"));
assert!(!policy.contains("(allow network-outbound (remote ip \"localhost:9999\"))"));
assert!(!policy.contains("(allow network-bind (local ip \"*:*\"))"));
assert!(!policy.contains("(allow network-outbound)\n"));
}
#[test]
fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
let root = absolute_path("/tmp/codex-readable");
@@ -279,6 +312,7 @@ fn explicit_unreadable_paths_are_excluded_from_readable_roots() {
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: Path::new("/"),
enforce_managed_network: false,
managed_network: None,
environment_id: None,
network: None,
extra_allow_unix_sockets: &[],
@@ -585,6 +619,7 @@ fn create_seatbelt_args_allowlists_explicit_unix_socket_paths_without_proxy() {
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
managed_network: None,
environment_id: None,
network: None,
extra_allow_unix_sockets: &extra_allow_unix_sockets,
@@ -645,6 +680,7 @@ async fn create_seatbelt_args_merges_proxy_and_explicit_unix_socket_paths() -> a
network_sandbox_policy: NetworkSandboxPolicy::Restricted,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
managed_network: None,
environment_id: None,
network: Some(&network_proxy),
extra_allow_unix_sockets: &extra_allow_unix_sockets,
@@ -688,6 +724,7 @@ fn create_seatbelt_args_preserves_full_network_with_explicit_unix_socket_paths()
network_sandbox_policy: NetworkSandboxPolicy::Enabled,
sandbox_policy_cwd: cwd.path(),
enforce_managed_network: false,
managed_network: None,
environment_id: None,
network: None,
extra_allow_unix_sockets: &extra_allow_unix_sockets,