Enforce environment network policies for remote execution (#39980)

## Why

`EnvironmentConfig.network_policy` could describe attachment-owned traffic
restrictions, but core rejected every configured policy because execution did
not enforce it.

## What changed

- Resolve each remote environment's network policy for the selected command
  and apply it to the execution-scoped proxy.
- Compose owner rules with controller constraints and saved network decisions
  while preserving inherited domain and Unix-socket denials.
- Keep strict allowlists non-expandable, allow reviewable policies to use
  network approvals, and reject sandbox escalation that would bypass an owner
  policy.
- Reject policies for local execution, disabled managed enforcement, or a
  disabled controller proxy.

## Testing

Added coverage for policy composition, scoped remote proxy behavior, approval
and denial flows, offline execution, and unsupported environment authority.

GitOrigin-RevId: d9331f616df24de6cd13ed68196f0ff7b0ca4dd9
This commit is contained in:
sayan-oai authored and copyberry committed 2026-08-21 17:29:41 +00:00
1 parent 8edb95f274
commit f580dd886f
13 files changed
+723 -63

No files matched your search

+70 -1
View File
@@ -179,6 +179,75 @@ impl NetworkProxySpec {
EnvironmentNetworkPolicy::from_config(&self.config, self.hard_deny_allowlist_misses)
}
pub(crate) fn for_environment(
controller: Option<&Self>,
policy: &EnvironmentNetworkPolicy,
permission_profile: &PermissionProfile,
exec_policy: &Policy,
) -> std::io::Result<Self> {
if matches!(permission_profile, PermissionProfile::Disabled) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"environment network policy requires managed network enforcement",
));
}
if controller.is_some_and(|controller| !controller.enabled()) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"environment network policy cannot override a disabled controller proxy",
));
}
let mut spec = match controller {
Some(controller) => controller.recompute_for_permission_profile(permission_profile)?,
None => Self::from_config_and_constraints(
NetworkProxyConfig {
enabled: true,
// Without a controller, the owner supplies the entire permission ceiling.
dangerously_allow_all_unix_sockets: true,
allow_local_binding: true,
..NetworkProxyConfig::default()
},
/*requirements*/ None,
permission_profile,
)?,
};
policy.apply_to(&mut spec.config);
let protected_denials = spec.config.denied_domains().unwrap_or_default();
// A fixed controller allowlist remains a ceiling; an expandable one is only a baseline.
// Profiles without managed approvals must not reuse approvals cached by another profile.
let fixed_allowlist = spec.hard_deny_allowlist_misses
|| spec.constraints.allowlist_expansion_enabled == Some(false);
spec.hard_deny_allowlist_misses |= policy.managed_allowed_domains_only
|| !Self::managed_sandbox_active(permission_profile);
let allow_owner_grants = !spec.hard_deny_allowlist_misses && !fixed_allowlist;
if fixed_allowlist {
spec.constraints.allowlist_expansion_enabled = None;
} else {
spec.constraints.allowed_domains =
Some(spec.config.allowed_domains().unwrap_or_default());
spec.constraints.allowlist_expansion_enabled = Some(allow_owner_grants);
}
spec.constraints.denylist_expansion_enabled = Some(true);
// Saved grants can extend a reviewable owner policy; owner denials are restored last.
let (allowed_domains, denied_domains) = exec_policy.compiled_network_domains();
if allow_owner_grants {
upsert_network_domains(&mut spec.config, allowed_domains, /*allow*/ true);
}
upsert_network_domains(&mut spec.config, denied_domains, /*allow*/ false);
upsert_network_domains(&mut spec.config, protected_denials, /*allow*/ false);
spec.constraints.denied_domains = spec.config.denied_domains();
validate_policy_against_constraints(&spec.config, &spec.constraints).map_err(|error| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("environment network policy violates managed requirements: {error}"),
)
})?;
Ok(spec)
}
pub(crate) fn with_exec_policy_network_rules(
&self,
exec_policy: &Policy,
@@ -208,7 +277,7 @@ impl NetworkProxySpec {
})
}
fn build_state_with_audit_metadata(
pub(crate) fn build_state_with_audit_metadata(
&self,
audit_metadata: NetworkProxyAuditMetadata,
) -> std::io::Result<NetworkProxyState> {
@@ -1,7 +1,11 @@
use super::*;
use codex_config::NetworkDomainPermissionToml;
use codex_config::NetworkDomainPermissionsToml;
use codex_execpolicy::Decision::Allow;
use codex_execpolicy::NetworkRuleProtocol::Https;
use codex_network_proxy::NetworkDomainPermission;
use codex_network_proxy::NetworkUnixSocketPermission;
use codex_network_proxy::NetworkUnixSocketPermissions;
use codex_protocol::models::ManagedFileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_protocol::permissions::NetworkSandboxPolicy;
@@ -40,6 +44,117 @@ fn build_state_with_audit_metadata_threads_metadata_to_state() {
assert_eq!(state.audit_metadata(), &metadata);
}
#[test]
fn environment_policy_replaces_soft_controller_allowlist_and_preserves_denials() {
let requirements = NetworkConstraints {
domains: Some(domain_permissions([
("controller.example", NetworkDomainPermissionToml::Allow),
("blocked.example", NetworkDomainPermissionToml::Deny),
])),
..Default::default()
};
let profile = PermissionProfile::workspace_write();
let spec = NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig {
enabled: true,
allow_upstream_proxy: false,
unix_sockets: Some(NetworkUnixSocketPermissions {
entries: [
(
"/tmp/controller.sock".to_string(),
NetworkUnixSocketPermission::Deny,
),
(
"/tmp/allowed.sock".to_string(),
NetworkUnixSocketPermission::Allow,
),
]
.into(),
}),
..NetworkProxyConfig::default()
},
Some(requirements),
&profile,
)
.expect("controller policy should be valid");
let mut owner = NetworkProxyConfig::default();
owner.set_allowed_domains(vec!["owner.example".to_string()]);
owner.set_denied_domains(vec!["owner-blocked.example".to_string()]);
owner.set_allow_unix_sockets(vec![
"/tmp/controller.sock".to_string(),
"/private/tmp/controller.sock".to_string(),
"/tmp/allowed.sock".to_string(),
]);
owner.dangerously_allow_all_unix_sockets = true;
owner.allow_local_binding = true;
let owner_policy =
EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false);
let compose = NetworkProxySpec::for_environment;
let empty = Policy::empty();
let disabled_controller = NetworkProxySpec::from_config_and_constraints(
NetworkProxyConfig::default(),
/*requirements*/ None,
&profile,
)
.expect("disabled controller policy should be valid");
assert!(compose(Some(&disabled_controller), &owner_policy, &profile, &empty).is_err());
let restricted = compose(Some(&spec), &owner_policy, &profile, &empty)
.expect("owner policy should replace soft controller grants");
let mut saved = Policy::empty();
for host in ["saved.example", "owner-blocked.example"] {
saved
.add_network_rule(host, Https, Allow, /*justification*/ None)
.expect("saved network grant should be valid");
}
let rootless = compose(/*controller*/ None, &owner_policy, &profile, &saved)
.expect("an owner policy can create executor-side proxy state");
assert_eq!(
rootless.config.allowed_domains().unwrap(),
["owner.example", "saved.example"]
);
owner.upsert_domain_permission(
"blocked.example".to_string(),
NetworkDomainPermission::Deny,
normalize_host,
);
owner.unix_sockets.clone_from(&spec.config.unix_sockets);
owner.allow_upstream_proxy = false;
owner.dangerously_allow_all_unix_sockets = false;
owner.allow_local_binding = false;
assert_eq!(
restricted.environment_policy(),
EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false)
);
let external = PermissionProfile::External {
network: NetworkSandboxPolicy::Enabled,
};
let external_rootless = compose(/*controller*/ None, &owner_policy, &external, &saved)
.expect("an externally sandboxed owner policy should remain strict");
assert_eq!(
external_rootless.environment_policy(),
EnvironmentNetworkPolicy {
managed_allowed_domains_only: true,
..owner_policy.clone()
}
);
let controller_policy = spec.environment_policy();
let external_rooted = compose(Some(&spec), &controller_policy, &external, &saved)
.expect("an externally sandboxed owner policy may retain its controller allowlist");
assert_eq!(
external_rooted.environment_policy(),
EnvironmentNetworkPolicy {
managed_allowed_domains_only: true,
..controller_policy
}
);
assert!(compose(Some(&spec), &owner_policy, &external, &empty).is_err());
owner.set_allowed_domains(vec!["*".to_string()]);
let wildcard_policy =
EnvironmentNetworkPolicy::from_config(&owner, /*managed_allowed_domains_only*/ false);
assert!(compose(Some(&spec), &wildcard_policy, &profile, &empty).is_err());
}
#[test]
fn requirements_allowed_domains_are_a_baseline_for_user_allowlist() {
let mut config = NetworkProxyConfig::default();