mirror of
https://github.com/openai/codex.git
synced 2026-11-13 06:45:07 +00:00
Prepare managed policy validation for Windows sandbox provisioning (#42344)
## What changed - Add a Windows sandbox service validator that loads managed configuration while impersonating the provisioning client. - Reject elevated sandbox or network settings that conflict with managed requirements, including local binding and HTTP or SOCKS proxy-port restrictions. - Add a one-shot cloud configuration loader that bypasses the disk cache so policy checks use a fresh backend response without modifying cached configuration. - Keep the provisioning integration disabled until authenticated transport can supply the policy inputs. ## Testing - Cover elevated sandbox restrictions, disabled networking, local binding, proxy-port classification, malformed policy, impersonation failure, and cache bypass behavior. GitOrigin-RevId: b76aa8959c515c6507c2eaf61af768efa1305253
This commit is contained in:
1 parent
add870a4bf
commit
c4ea7294b9
9 files changed
+560
-28
No files matched your search
@@ -18,7 +18,14 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = { workspace = true }
|
||||
codex-cloud-config = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-windows-sandbox = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros", "rt", "sync", "time"] }
|
||||
|
||||
[dev-dependencies]
|
||||
toml = { workspace = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies.windows-sys]
|
||||
version = "0.52"
|
||||
|
||||
@@ -2,6 +2,10 @@ use anyhow::Result;
|
||||
|
||||
#[cfg(windows)]
|
||||
mod ipc;
|
||||
// Provisioning remains disabled until authenticated transport connects this policy.
|
||||
#[cfg(windows)]
|
||||
#[allow(dead_code)]
|
||||
mod machine_policy;
|
||||
#[cfg(windows)]
|
||||
mod service;
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
//! Validates provisioning requests against the standard managed configuration layers.
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_cloud_config::cloud_config_bundle_loader_for_storage_without_cache;
|
||||
use codex_config::ConfigLoadOptions;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::types::WindowsSandboxModeToml;
|
||||
use codex_core::config::bootstrap_auth_config;
|
||||
use codex_core::config::load_config_toml_with_layer_stack;
|
||||
use codex_windows_sandbox::WindowsSandboxProvisioningSettings;
|
||||
use codex_windows_sandbox::WindowsSandboxProxyListeners;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Notify;
|
||||
use windows_sys::Win32::Foundation::HANDLE;
|
||||
use windows_sys::Win32::Security::ImpersonateLoggedOnUser;
|
||||
|
||||
pub(crate) fn validate_provisioning_settings(
|
||||
codex_home: &Path,
|
||||
settings: &WindowsSandboxProvisioningSettings,
|
||||
listeners: &WindowsSandboxProxyListeners,
|
||||
impersonation_token: HANDLE,
|
||||
) -> Result<()> {
|
||||
let impersonation_failure = Arc::new(Notify::new());
|
||||
let worker_impersonation_failure = Arc::clone(&impersonation_failure);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.on_thread_start(move || {
|
||||
if unsafe { ImpersonateLoggedOnUser(impersonation_token) } == 0 {
|
||||
let error = std::io::Error::last_os_error();
|
||||
worker_impersonation_failure.notify_one();
|
||||
panic!(
|
||||
"failed to impersonate provisioning client on configuration runtime thread: {error}"
|
||||
);
|
||||
}
|
||||
})
|
||||
.build()
|
||||
.context("start managed configuration runtime")?;
|
||||
let requirements = runtime.block_on(async {
|
||||
tokio::select! {
|
||||
biased;
|
||||
() = impersonation_failure.notified() => {
|
||||
Err(anyhow::anyhow!("configuration runtime worker failed to impersonate the provisioning client"))
|
||||
},
|
||||
result = async {
|
||||
let mut bootstrap_config = load_config_toml_with_layer_stack(
|
||||
codex_home,
|
||||
/*cwd*/ None,
|
||||
Vec::new(),
|
||||
ConfigLoadOptions::default(),
|
||||
)
|
||||
.await
|
||||
.context("load bootstrap configuration")?;
|
||||
// Use the default cloud-policy endpoint unless managed requirements override it.
|
||||
bootstrap_config.config_toml.chatgpt_base_url = None;
|
||||
let cloud_config_bundle = cloud_config_bundle_loader_for_storage_without_cache(
|
||||
bootstrap_auth_config(codex_home, &bootstrap_config)
|
||||
.context("resolve cloud configuration authentication")?,
|
||||
/*enable_codex_api_key_env*/ false,
|
||||
)
|
||||
.await
|
||||
.context("initialize cloud configuration authentication")?;
|
||||
let config = load_config_toml_with_layer_stack(
|
||||
codex_home,
|
||||
/*cwd*/ None,
|
||||
Vec::new(),
|
||||
ConfigLoadOptions {
|
||||
cloud_config_bundle,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.context("load managed configuration")?;
|
||||
|
||||
Ok::<_, anyhow::Error>(config.config_layer_stack.requirements_toml().clone())
|
||||
} => result,
|
||||
}
|
||||
})?;
|
||||
|
||||
validate_requirements(settings, listeners, &requirements)
|
||||
.context("enforce managed provisioning requirements")
|
||||
}
|
||||
|
||||
fn validate_requirements(
|
||||
settings: &WindowsSandboxProvisioningSettings,
|
||||
listeners: &WindowsSandboxProxyListeners,
|
||||
requirements: &ConfigRequirementsToml,
|
||||
) -> Result<()> {
|
||||
if requirements
|
||||
.windows
|
||||
.as_ref()
|
||||
.and_then(|windows| windows.allowed_sandbox_implementations.as_ref())
|
||||
.is_some_and(|allowed| !allowed.contains(&WindowsSandboxModeToml::Elevated))
|
||||
{
|
||||
bail!("managed policy does not permit the elevated Windows sandbox");
|
||||
}
|
||||
|
||||
let Some(network) = requirements.network.as_ref() else {
|
||||
return Ok(());
|
||||
};
|
||||
if network.enabled == Some(false)
|
||||
&& (settings.allow_local_binding || !settings.proxy_ports.is_empty())
|
||||
{
|
||||
bail!("managed policy disables sandbox network access");
|
||||
}
|
||||
if settings.allow_local_binding && network.allow_local_binding == Some(false) {
|
||||
bail!("managed policy does not permit local network binding");
|
||||
}
|
||||
if let Some(required) = network.http_port
|
||||
&& let Some(actual) = listeners.http_ports.iter().find(|port| **port != required)
|
||||
{
|
||||
bail!("managed policy does not permit HTTP proxy port {actual}");
|
||||
}
|
||||
if let Some(required) = network.socks_port
|
||||
&& let Some(actual) = listeners.socks_ports.iter().find(|port| **port != required)
|
||||
{
|
||||
bail!("managed policy does not permit SOCKS proxy port {actual}");
|
||||
}
|
||||
// Omitting listener identities must not bypass managed port restrictions.
|
||||
if network.http_port.is_some() || network.socks_port.is_some() {
|
||||
for port in &settings.proxy_ports {
|
||||
if !listeners.http_ports.contains(port)
|
||||
&& !listeners.socks_ports.contains(port)
|
||||
&& network.http_port != Some(*port)
|
||||
&& network.socks_port != Some(*port)
|
||||
{
|
||||
bail!("managed policy does not permit unclassified proxy port {port}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "machine_policy_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,277 @@
|
||||
use anyhow::Result;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_windows_sandbox::WindowsSandboxProvisioningSettings;
|
||||
use codex_windows_sandbox::WindowsSandboxProxyListeners;
|
||||
|
||||
fn validate_requirements(
|
||||
settings: &WindowsSandboxProvisioningSettings,
|
||||
listeners: WindowsSandboxProxyListeners,
|
||||
contents: &str,
|
||||
) -> Result<()> {
|
||||
let requirements: ConfigRequirementsToml = toml::from_str(contents)?;
|
||||
super::validate_requirements(settings, &listeners, &requirements)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_worker_impersonation_failure_rejects_provisioning() {
|
||||
let error = super::validate_provisioning_settings(
|
||||
&std::env::temp_dir(),
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
&WindowsSandboxProxyListeners::default(),
|
||||
/*impersonation_token*/ 0,
|
||||
)
|
||||
.expect_err("runtime workers must not load configuration without impersonating the client");
|
||||
|
||||
assert!(error.to_string().contains("failed to impersonate"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmanaged_policy_allows_default_and_requested_network_settings() -> Result<()> {
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
"",
|
||||
)?;
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![3128, 8080, 9000],
|
||||
allow_local_binding: true,
|
||||
},
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128],
|
||||
socks_ports: vec![8080],
|
||||
},
|
||||
"allowed_sandbox_modes = [\"workspace-write\"]",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevated_sandbox_is_allowed_when_included() -> Result<()> {
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
"[windows]\nallowed_sandbox_implementations = [\"unelevated\", \"elevated\"]",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevated_sandbox_is_rejected_when_prohibited_or_empty() {
|
||||
for implementations in ["[\"unelevated\"]", "[]"] {
|
||||
let policy = format!("[windows]\nallowed_sandbox_implementations = {implementations}");
|
||||
assert!(
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
&policy,
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn forbidden_local_binding_is_rejected() {
|
||||
let settings = WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: Vec::new(),
|
||||
allow_local_binding: true,
|
||||
};
|
||||
assert!(
|
||||
validate_requirements(
|
||||
&settings,
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
"[experimental_network]\nallow_local_binding = false"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_network_rejects_proxy_ports_and_local_binding() -> Result<()> {
|
||||
let policy = "[experimental_network]\nenabled = false";
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
policy,
|
||||
)?;
|
||||
for settings in [
|
||||
WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![3128],
|
||||
allow_local_binding: false,
|
||||
},
|
||||
WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: Vec::new(),
|
||||
allow_local_binding: true,
|
||||
},
|
||||
] {
|
||||
assert!(
|
||||
validate_requirements(&settings, WindowsSandboxProxyListeners::default(), policy)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_network_allows_unrestricted_proxy_ports() -> Result<()> {
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![3128, 8080, 8081, 9000],
|
||||
allow_local_binding: false,
|
||||
},
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128, 8080],
|
||||
socks_ports: vec![8081],
|
||||
},
|
||||
"[experimental_network]\nenabled = true",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disabled_local_binding_is_allowed_when_policy_permits_binding() -> Result<()> {
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
"[experimental_network]\nallow_local_binding = true",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_proxy_ports_allow_only_configured_ports() -> Result<()> {
|
||||
let policy = "[experimental_network]\nhttp_port = 3128\nsocks_port = 1080";
|
||||
let allowed = WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![1080, 3128],
|
||||
allow_local_binding: false,
|
||||
};
|
||||
for listeners in [
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128],
|
||||
socks_ports: vec![1080],
|
||||
},
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
] {
|
||||
validate_requirements(&allowed, listeners, policy)?;
|
||||
}
|
||||
let prohibited = WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![1080, 3128, 8080],
|
||||
allow_local_binding: false,
|
||||
};
|
||||
for listeners in [
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128, 8080],
|
||||
socks_ports: vec![1080],
|
||||
},
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128],
|
||||
socks_ports: vec![1080],
|
||||
},
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
] {
|
||||
assert!(validate_requirements(&prohibited, listeners, policy).is_err());
|
||||
}
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
policy,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_http_port_does_not_restrict_the_unmanaged_socks_listener() -> Result<()> {
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![3128, 8081],
|
||||
allow_local_binding: false,
|
||||
},
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128],
|
||||
socks_ports: vec![8081],
|
||||
},
|
||||
"[experimental_network]\nhttp_port = 3128",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_socks_port_does_not_restrict_the_unmanaged_http_listener() -> Result<()> {
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![1080, 3128],
|
||||
allow_local_binding: false,
|
||||
},
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128],
|
||||
socks_ports: vec![1080],
|
||||
},
|
||||
"[experimental_network]\nsocks_port = 1080",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_proxy_ports_reject_swapped_listener_roles() {
|
||||
assert!(
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![1080, 3128],
|
||||
allow_local_binding: false,
|
||||
},
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![1080],
|
||||
socks_ports: vec![3128],
|
||||
},
|
||||
"[experimental_network]\nhttp_port = 3128\nsocks_port = 1080",
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_socks_port_rejects_a_mismatched_socks_listener() {
|
||||
assert!(
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![1080, 3128, 8081],
|
||||
allow_local_binding: false,
|
||||
},
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128],
|
||||
socks_ports: vec![1080, 8081],
|
||||
},
|
||||
"[experimental_network]\nhttp_port = 3128\nsocks_port = 1080",
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_socks_port_does_not_require_a_disabled_socks_listener() -> Result<()> {
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings {
|
||||
proxy_ports: vec![3128],
|
||||
allow_local_binding: false,
|
||||
},
|
||||
WindowsSandboxProxyListeners {
|
||||
http_ports: vec![3128],
|
||||
socks_ports: Vec::new(),
|
||||
},
|
||||
"[experimental_network]\nhttp_port = 3128\nsocks_port = 1080",
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_machine_policy_fails_closed() {
|
||||
assert!(
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
"[experimental_network]\nhttp_port = \"invalid\""
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
validate_requirements(
|
||||
&WindowsSandboxProvisioningSettings::default(),
|
||||
WindowsSandboxProxyListeners::default(),
|
||||
"[windows]\nallowed_sandbox_implementations = [\"invalid\"]"
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user