mirror of
https://github.com/openai/codex.git
synced 2026-11-10 08:43:33 +00:00
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:
1 parent
8edb95f274
commit
f580dd886f
13 files changed
+723
-63
No files matched your search
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user