mirror of
https://github.com/openai/codex.git
synced 2026-04-28 02:11:08 +03:00
fix(network-proxy): add unix socket allow-all and update seatbelt rules (#11368)
## Summary Adds support for a Unix socket escape hatch so we can bypass socket allowlisting when explicitly enabled. ## Description * added a new flag, `network.dangerously_allow_all_unix_sockets` as an explicit escape hatch * In codex-network-proxy, enabling that flag now allows any absolute Unix socket path from x-unix-socket instead of requiring each path to be explicitly allowlisted. Relative paths are still rejected. * updated the macOS seatbelt path in core so it enforces the same Unix socket behavior: * allowlisted sockets generate explicit network* subpath rules * allow-all generates a broad network* (subpath "/") rule --------- Co-authored-by: Codex <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
This commit is contained in:
@@ -47,6 +47,9 @@ allow_local_binding = true
|
||||
|
||||
# macOS-only: allows proxying to a unix socket when request includes `x-unix-socket: /path`.
|
||||
allow_unix_sockets = ["/tmp/example.sock"]
|
||||
# DANGEROUS (macOS-only): bypasses unix socket allowlisting and permits any
|
||||
# absolute socket path from `x-unix-socket`.
|
||||
dangerously_allow_all_unix_sockets = false
|
||||
```
|
||||
|
||||
### 2) Run the proxy
|
||||
@@ -116,8 +119,9 @@ let handle = proxy.run().await?;
|
||||
handle.shutdown().await?;
|
||||
```
|
||||
|
||||
When unix socket proxying is enabled, HTTP/admin bind overrides are still clamped to loopback
|
||||
to avoid turning the proxy into a remote bridge to local daemons.
|
||||
When unix socket proxying is enabled (`allow_unix_sockets` or
|
||||
`dangerously_allow_all_unix_sockets`), HTTP/admin bind overrides are still clamped to loopback to
|
||||
avoid turning the proxy into a remote bridge to local daemons.
|
||||
|
||||
### Policy hook (exec-policy mapping)
|
||||
|
||||
@@ -176,6 +180,8 @@ what it can reasonably guarantee.
|
||||
`dangerously_allow_non_loopback_proxy`
|
||||
- when unix socket proxying is enabled, both listeners are forced to loopback to avoid turning the
|
||||
proxy into a remote bridge into local daemons.
|
||||
- `dangerously_allow_all_unix_sockets = true` bypasses the unix socket allowlist entirely (still
|
||||
macOS-only and absolute-path-only). Use only in tightly controlled environments.
|
||||
- `enabled` is enforced at runtime; when false the proxy no-ops and does not bind listeners.
|
||||
Limitations:
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use anyhow::Context;
|
||||
use anyhow::Result;
|
||||
use anyhow::bail;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::net::IpAddr;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use tracing::warn;
|
||||
use url::Url;
|
||||
|
||||
@@ -33,6 +35,8 @@ pub struct NetworkProxySettings {
|
||||
#[serde(default)]
|
||||
pub dangerously_allow_non_loopback_admin: bool,
|
||||
#[serde(default)]
|
||||
pub dangerously_allow_all_unix_sockets: bool,
|
||||
#[serde(default)]
|
||||
pub mode: NetworkMode,
|
||||
#[serde(default)]
|
||||
pub allowed_domains: Vec<String>,
|
||||
@@ -55,6 +59,7 @@ impl Default for NetworkProxySettings {
|
||||
allow_upstream_proxy: true,
|
||||
dangerously_allow_non_loopback_proxy: false,
|
||||
dangerously_allow_non_loopback_admin: false,
|
||||
dangerously_allow_all_unix_sockets: false,
|
||||
mode: NetworkMode::default(),
|
||||
allowed_domains: Vec::new(),
|
||||
denied_domains: Vec::new(),
|
||||
@@ -136,7 +141,7 @@ pub(crate) fn clamp_bind_addrs(
|
||||
cfg.dangerously_allow_non_loopback_admin,
|
||||
"admin API",
|
||||
);
|
||||
if cfg.allow_unix_sockets.is_empty() {
|
||||
if cfg.allow_unix_sockets.is_empty() && !cfg.dangerously_allow_all_unix_sockets {
|
||||
return (http_addr, socks_addr, admin_addr);
|
||||
}
|
||||
|
||||
@@ -172,7 +177,49 @@ pub struct RuntimeConfig {
|
||||
pub admin_addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct UnixStyleAbsolutePath(String);
|
||||
|
||||
impl UnixStyleAbsolutePath {
|
||||
fn parse(value: &str) -> Option<Self> {
|
||||
value.starts_with('/').then(|| Self(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum ValidatedUnixSocketPath {
|
||||
Native(AbsolutePathBuf),
|
||||
UnixStyleAbsolute(UnixStyleAbsolutePath),
|
||||
}
|
||||
|
||||
impl ValidatedUnixSocketPath {
|
||||
pub(crate) fn parse(socket_path: &str) -> Result<Self> {
|
||||
let path = Path::new(socket_path);
|
||||
if path.is_absolute() {
|
||||
let path = AbsolutePathBuf::from_absolute_path(path)
|
||||
.with_context(|| format!("failed to normalize unix socket path {socket_path:?}"))?;
|
||||
return Ok(Self::Native(path));
|
||||
}
|
||||
|
||||
if let Some(path) = UnixStyleAbsolutePath::parse(socket_path) {
|
||||
return Ok(Self::UnixStyleAbsolute(path));
|
||||
}
|
||||
|
||||
bail!("expected an absolute path, got {socket_path:?}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_unix_socket_allowlist_paths(cfg: &NetworkProxyConfig) -> Result<()> {
|
||||
for (index, socket_path) in cfg.network.allow_unix_sockets.iter().enumerate() {
|
||||
ValidatedUnixSocketPath::parse(socket_path)
|
||||
.with_context(|| format!("invalid network.allow_unix_sockets[{index}]"))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn resolve_runtime(cfg: &NetworkProxyConfig) -> Result<RuntimeConfig> {
|
||||
validate_unix_socket_allowlist_paths(cfg)?;
|
||||
|
||||
let http_addr = resolve_addr(&cfg.network.proxy_url, 3128)
|
||||
.with_context(|| format!("invalid network.proxy_url: {}", cfg.network.proxy_url))?;
|
||||
let socks_addr = resolve_addr(&cfg.network.socks_url, 8081)
|
||||
@@ -340,6 +387,7 @@ mod tests {
|
||||
allow_upstream_proxy: true,
|
||||
dangerously_allow_non_loopback_proxy: false,
|
||||
dangerously_allow_non_loopback_admin: false,
|
||||
dangerously_allow_all_unix_sockets: false,
|
||||
mode: NetworkMode::Full,
|
||||
allowed_domains: Vec::new(),
|
||||
denied_domains: Vec::new(),
|
||||
@@ -526,4 +574,61 @@ mod tests {
|
||||
assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
|
||||
assert_eq!(admin_addr, "127.0.0.1:8080".parse::<SocketAddr>().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamp_bind_addrs_forces_loopback_when_all_unix_sockets_enabled() {
|
||||
let cfg = NetworkProxySettings {
|
||||
dangerously_allow_non_loopback_proxy: true,
|
||||
dangerously_allow_non_loopback_admin: true,
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..Default::default()
|
||||
};
|
||||
let http_addr = "0.0.0.0:3128".parse::<SocketAddr>().unwrap();
|
||||
let socks_addr = "0.0.0.0:8081".parse::<SocketAddr>().unwrap();
|
||||
let admin_addr = "0.0.0.0:8080".parse::<SocketAddr>().unwrap();
|
||||
|
||||
let (http_addr, socks_addr, admin_addr) =
|
||||
clamp_bind_addrs(http_addr, socks_addr, admin_addr, &cfg);
|
||||
|
||||
assert_eq!(http_addr, "127.0.0.1:3128".parse::<SocketAddr>().unwrap());
|
||||
assert_eq!(socks_addr, "127.0.0.1:8081".parse::<SocketAddr>().unwrap());
|
||||
assert_eq!(admin_addr, "127.0.0.1:8080".parse::<SocketAddr>().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_runtime_rejects_relative_allow_unix_sockets_entries() {
|
||||
let cfg = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
allow_unix_sockets: vec!["relative.sock".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
let err = match resolve_runtime(&cfg) {
|
||||
Ok(runtime) => panic!(
|
||||
"relative allow_unix_sockets should fail, but resolve_runtime succeeded: {:?}",
|
||||
runtime.http_addr
|
||||
),
|
||||
Err(err) => err,
|
||||
};
|
||||
assert!(
|
||||
err.to_string().contains("network.allow_unix_sockets[0]"),
|
||||
"error should point at the invalid allow_unix_sockets entry: {err:#}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_runtime_accepts_unix_style_absolute_allow_unix_sockets_entries() {
|
||||
let cfg = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
allow_unix_sockets: vec!["/private/tmp/example.sock".to_string()],
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(
|
||||
resolve_runtime(&cfg).is_ok(),
|
||||
"unix-style absolute allow_unix_sockets entry should be accepted"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,8 +378,8 @@ async fn http_plain_proxy(
|
||||
};
|
||||
|
||||
// `x-unix-socket` is an escape hatch for talking to local daemons. We keep it tightly scoped:
|
||||
// macOS-only + explicit allowlist, to avoid turning the proxy into a general local capability
|
||||
// escalation mechanism.
|
||||
// macOS-only + explicit allowlist by default, to avoid turning the proxy into a general local
|
||||
// capability escalation mechanism.
|
||||
if let Some(unix_socket_header) = req.headers().get("x-unix-socket") {
|
||||
let socket_path = match unix_socket_header.to_str() {
|
||||
Ok(value) => value.to_string(),
|
||||
|
||||
@@ -206,6 +206,10 @@ impl NetworkProxyBuilder {
|
||||
socks_addr,
|
||||
socks_enabled: current_cfg.network.enable_socks5,
|
||||
allow_local_binding: current_cfg.network.allow_local_binding,
|
||||
allow_unix_sockets: current_cfg.network.allow_unix_sockets.clone(),
|
||||
dangerously_allow_all_unix_sockets: current_cfg
|
||||
.network
|
||||
.dangerously_allow_all_unix_sockets,
|
||||
admin_addr,
|
||||
reserved_listeners,
|
||||
policy_decider: self.policy_decider,
|
||||
@@ -240,6 +244,8 @@ pub struct NetworkProxy {
|
||||
socks_addr: SocketAddr,
|
||||
socks_enabled: bool,
|
||||
allow_local_binding: bool,
|
||||
allow_unix_sockets: Vec<String>,
|
||||
dangerously_allow_all_unix_sockets: bool,
|
||||
admin_addr: SocketAddr,
|
||||
reserved_listeners: Option<Arc<ReservedListeners>>,
|
||||
policy_decider: Option<Arc<dyn NetworkPolicyDecider>>,
|
||||
@@ -419,6 +425,18 @@ impl NetworkProxy {
|
||||
self.admin_addr
|
||||
}
|
||||
|
||||
pub fn allow_local_binding(&self) -> bool {
|
||||
self.allow_local_binding
|
||||
}
|
||||
|
||||
pub fn allow_unix_sockets(&self) -> &[String] {
|
||||
&self.allow_unix_sockets
|
||||
}
|
||||
|
||||
pub fn dangerously_allow_all_unix_sockets(&self) -> bool {
|
||||
self.dangerously_allow_all_unix_sockets
|
||||
}
|
||||
|
||||
pub fn apply_to_env(&self, env: &mut HashMap<String, String>) {
|
||||
// Enforce proxying for child processes. We intentionally override existing values so
|
||||
// command-level environment cannot bypass the managed proxy endpoint.
|
||||
@@ -441,7 +459,9 @@ impl NetworkProxy {
|
||||
ensure_rustls_crypto_provider();
|
||||
|
||||
if !unix_socket_permissions_supported() {
|
||||
warn!("allowUnixSockets is macOS-only; requests will be rejected on this platform");
|
||||
warn!(
|
||||
"allowUnixSockets and dangerouslyAllowAllUnixSockets are macOS-only; requests will be rejected on this platform"
|
||||
);
|
||||
}
|
||||
|
||||
let reserved_listeners = self.reserved_listeners.as_ref();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::config::NetworkMode;
|
||||
use crate::config::NetworkProxyConfig;
|
||||
use crate::config::ValidatedUnixSocketPath;
|
||||
use crate::policy::Host;
|
||||
use crate::policy::is_loopback_host;
|
||||
use crate::policy::is_non_public_ip;
|
||||
@@ -418,6 +419,10 @@ impl NetworkProxyState {
|
||||
}
|
||||
|
||||
let guard = self.state.read().await;
|
||||
if guard.config.network.dangerously_allow_all_unix_sockets {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Normalize the path while keeping the absolute-path requirement explicit.
|
||||
let requested_abs = match AbsolutePathBuf::from_absolute_path(requested_path) {
|
||||
Ok(path) => path,
|
||||
@@ -425,7 +430,16 @@ impl NetworkProxyState {
|
||||
};
|
||||
let requested_canonical = std::fs::canonicalize(requested_abs.as_path()).ok();
|
||||
for allowed in &guard.config.network.allow_unix_sockets {
|
||||
if allowed == path {
|
||||
let allowed_path = match ValidatedUnixSocketPath::parse(allowed) {
|
||||
Ok(ValidatedUnixSocketPath::Native(path)) => path,
|
||||
Ok(ValidatedUnixSocketPath::UnixStyleAbsolute(_)) => continue,
|
||||
Err(err) => {
|
||||
warn!("ignoring invalid network.allow_unix_sockets entry at runtime: {err:#}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if allowed_path.as_path() == requested_abs.as_path() {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -434,7 +448,7 @@ impl NetworkProxyState {
|
||||
let Some(requested_canonical) = &requested_canonical else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(allowed_canonical) = std::fs::canonicalize(allowed)
|
||||
if let Ok(allowed_canonical) = std::fs::canonicalize(allowed_path.as_path())
|
||||
&& &allowed_canonical == requested_canonical
|
||||
{
|
||||
return Ok(true);
|
||||
@@ -1075,6 +1089,77 @@ mod tests {
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_policy_against_constraints_disallows_allow_all_unix_sockets_without_managed_opt_in()
|
||||
{
|
||||
let constraints = NetworkProxyConstraints {
|
||||
dangerously_allow_all_unix_sockets: Some(false),
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_policy_against_constraints_disallows_allow_all_unix_sockets_when_allowlist_is_managed()
|
||||
{
|
||||
let constraints = NetworkProxyConstraints {
|
||||
allow_unix_sockets: Some(vec!["/tmp/allowed.sock".to_string()]),
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_policy_against_constraints_allows_allow_all_unix_sockets_with_managed_opt_in() {
|
||||
let constraints = NetworkProxyConstraints {
|
||||
dangerously_allow_all_unix_sockets: Some(true),
|
||||
..NetworkProxyConstraints::default()
|
||||
};
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_policy_against_constraints_allows_allow_all_unix_sockets_when_unmanaged() {
|
||||
let constraints = NetworkProxyConstraints::default();
|
||||
|
||||
let config = NetworkProxyConfig {
|
||||
network: NetworkProxySettings {
|
||||
enabled: true,
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..NetworkProxySettings::default()
|
||||
},
|
||||
};
|
||||
|
||||
assert!(validate_policy_against_constraints(&config, &constraints).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compile_globset_is_case_insensitive() {
|
||||
let patterns = vec!["ExAmPle.CoM".to_string()];
|
||||
@@ -1172,6 +1257,19 @@ mod tests {
|
||||
assert!(state.is_unix_socket_allowed(&link_s).await.unwrap());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn unix_socket_allow_all_flag_bypasses_allowlist() {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
|
||||
assert!(state.is_unix_socket_allowed("/tmp/any.sock").await.unwrap());
|
||||
assert!(!state.is_unix_socket_allowed("relative.sock").await.unwrap());
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
#[tokio::test]
|
||||
async fn unix_socket_allowlist_is_rejected_on_non_macos() {
|
||||
@@ -1179,6 +1277,7 @@ mod tests {
|
||||
let state = network_proxy_state_for_policy(NetworkProxySettings {
|
||||
allowed_domains: vec!["example.com".to_string()],
|
||||
allow_unix_sockets: vec![socket_path.clone()],
|
||||
dangerously_allow_all_unix_sockets: true,
|
||||
..NetworkProxySettings::default()
|
||||
});
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ pub struct NetworkProxyConstraints {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
pub denied_domains: Option<Vec<String>>,
|
||||
pub allow_unix_sockets: Option<Vec<String>>,
|
||||
@@ -38,6 +39,7 @@ pub struct PartialNetworkConfig {
|
||||
pub allow_upstream_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_proxy: Option<bool>,
|
||||
pub dangerously_allow_non_loopback_admin: Option<bool>,
|
||||
pub dangerously_allow_all_unix_sockets: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub allowed_domains: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
@@ -52,6 +54,7 @@ pub fn build_config_state(
|
||||
config: NetworkProxyConfig,
|
||||
constraints: NetworkProxyConstraints,
|
||||
) -> anyhow::Result<ConfigState> {
|
||||
crate::config::validate_unix_socket_allowlist_paths(&config)?;
|
||||
let deny_set = compile_globset(&config.network.denied_domains)?;
|
||||
let allow_set = compile_globset(&config.network.allowed_domains)?;
|
||||
Ok(ConfigState {
|
||||
@@ -173,6 +176,24 @@ pub fn validate_policy_against_constraints(
|
||||
},
|
||||
)?;
|
||||
|
||||
let allow_all_unix_sockets = constraints
|
||||
.dangerously_allow_all_unix_sockets
|
||||
.unwrap_or(constraints.allow_unix_sockets.is_none());
|
||||
validate(
|
||||
config.network.dangerously_allow_all_unix_sockets,
|
||||
move |candidate| {
|
||||
if *candidate && !allow_all_unix_sockets {
|
||||
Err(invalid_value(
|
||||
"network.dangerously_allow_all_unix_sockets",
|
||||
"true",
|
||||
"false (disabled by managed config)",
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Some(allow_local_binding) = constraints.allow_local_binding {
|
||||
validate(config.network.allow_local_binding, move |candidate| {
|
||||
if *candidate && !allow_local_binding {
|
||||
|
||||
Reference in New Issue
Block a user