mirror of
https://github.com/openai/codex.git
synced 2026-05-01 01:47:18 +00:00
feat(sandbox): enforce proxy-aware network routing in sandbox (#11113)
## Summary - expand proxy env injection to cover common tool env vars (`HTTP_PROXY`/`HTTPS_PROXY`/`ALL_PROXY`/`NO_PROXY` families + tool-specific variants) - harden macOS Seatbelt network policy generation to route through inferred loopback proxy endpoints and fail closed when proxy env is malformed - thread proxy-aware Linux sandbox flags and add minimal bwrap netns isolation hook for restricted non-proxy runs - add/refresh tests for proxy env wiring, Seatbelt policy generation, and Linux sandbox argument wiring
This commit is contained in:
@@ -26,19 +26,47 @@ pub(crate) struct BwrapOptions {
|
||||
/// This is the secure default, but some restrictive container environments
|
||||
/// deny `--proc /proc` even when PID namespaces are available.
|
||||
pub mount_proc: bool,
|
||||
/// How networking should be configured inside the bubblewrap sandbox.
|
||||
pub network_mode: BwrapNetworkMode,
|
||||
}
|
||||
|
||||
impl Default for BwrapOptions {
|
||||
fn default() -> Self {
|
||||
Self { mount_proc: true }
|
||||
Self {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Network policy modes for bubblewrap.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub(crate) enum BwrapNetworkMode {
|
||||
/// Keep access to the host network namespace.
|
||||
#[default]
|
||||
FullAccess,
|
||||
/// Remove access to the host network namespace.
|
||||
Isolated,
|
||||
/// Intended proxy-only mode.
|
||||
///
|
||||
/// Bubblewrap does not currently enforce proxy-only egress, so this is
|
||||
/// treated as isolated for fail-closed behavior.
|
||||
ProxyOnly,
|
||||
}
|
||||
|
||||
impl BwrapNetworkMode {
|
||||
fn should_unshare_network(self) -> bool {
|
||||
!matches!(self, Self::FullAccess)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap a command with bubblewrap so the filesystem is read-only by default,
|
||||
/// with explicit writable roots and read-only subpaths layered afterward.
|
||||
///
|
||||
/// When the policy grants full disk write access, this returns `command`
|
||||
/// unchanged so we avoid unnecessary sandboxing overhead.
|
||||
/// When the policy grants full disk write access and full network access, this
|
||||
/// returns `command` unchanged so we avoid unnecessary sandboxing overhead.
|
||||
/// If network isolation is requested, we still wrap with bubblewrap so network
|
||||
/// namespace restrictions apply while preserving full filesystem access.
|
||||
pub(crate) fn create_bwrap_command_args(
|
||||
command: Vec<String>,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
@@ -46,12 +74,37 @@ pub(crate) fn create_bwrap_command_args(
|
||||
options: BwrapOptions,
|
||||
) -> Result<Vec<String>> {
|
||||
if sandbox_policy.has_full_disk_write_access() {
|
||||
return Ok(command);
|
||||
return if options.network_mode == BwrapNetworkMode::FullAccess {
|
||||
Ok(command)
|
||||
} else {
|
||||
Ok(create_bwrap_flags_full_filesystem(command, options))
|
||||
};
|
||||
}
|
||||
|
||||
create_bwrap_flags(command, sandbox_policy, cwd, options)
|
||||
}
|
||||
|
||||
fn create_bwrap_flags_full_filesystem(command: Vec<String>, options: BwrapOptions) -> Vec<String> {
|
||||
let mut args = vec![
|
||||
"--new-session".to_string(),
|
||||
"--die-with-parent".to_string(),
|
||||
"--bind".to_string(),
|
||||
"/".to_string(),
|
||||
"/".to_string(),
|
||||
"--unshare-pid".to_string(),
|
||||
];
|
||||
if options.network_mode.should_unshare_network() {
|
||||
args.push("--unshare-net".to_string());
|
||||
}
|
||||
if options.mount_proc {
|
||||
args.push("--proc".to_string());
|
||||
args.push("/proc".to_string());
|
||||
}
|
||||
args.push("--".to_string());
|
||||
args.extend(command);
|
||||
args
|
||||
}
|
||||
|
||||
/// Build the bubblewrap flags (everything after `argv[0]`).
|
||||
fn create_bwrap_flags(
|
||||
command: Vec<String>,
|
||||
@@ -65,6 +118,9 @@ fn create_bwrap_flags(
|
||||
args.extend(create_filesystem_args(sandbox_policy, cwd)?);
|
||||
// Isolate the PID namespace.
|
||||
args.push("--unshare-pid".to_string());
|
||||
if options.network_mode.should_unshare_network() {
|
||||
args.push("--unshare-net".to_string());
|
||||
}
|
||||
// Mount a fresh /proc unless the caller explicitly disables it.
|
||||
if options.mount_proc {
|
||||
args.push("--proc".to_string());
|
||||
@@ -250,3 +306,59 @@ fn find_first_non_existent_component(target_path: &Path) -> Option<PathBuf> {
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn full_disk_write_full_network_returns_unwrapped_command() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let args = create_bwrap_command_args(
|
||||
command.clone(),
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
},
|
||||
)
|
||||
.expect("create bwrap args");
|
||||
|
||||
assert_eq!(args, command);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_disk_write_proxy_only_keeps_full_filesystem_but_unshares_network() {
|
||||
let command = vec!["/bin/true".to_string()];
|
||||
let args = create_bwrap_command_args(
|
||||
command,
|
||||
&SandboxPolicy::DangerFullAccess,
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::ProxyOnly,
|
||||
},
|
||||
)
|
||||
.expect("create bwrap args");
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"--new-session".to_string(),
|
||||
"--die-with-parent".to_string(),
|
||||
"--bind".to_string(),
|
||||
"/".to_string(),
|
||||
"/".to_string(),
|
||||
"--unshare-pid".to_string(),
|
||||
"--unshare-net".to_string(),
|
||||
"--proc".to_string(),
|
||||
"/proc".to_string(),
|
||||
"--".to_string(),
|
||||
"/bin/true".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,18 +42,22 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
cwd: &Path,
|
||||
apply_landlock_fs: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> Result<()> {
|
||||
let install_network_seccomp =
|
||||
should_install_network_seccomp(sandbox_policy, allow_network_for_proxy);
|
||||
|
||||
// `PR_SET_NO_NEW_PRIVS` is required for seccomp, but it also prevents
|
||||
// setuid privilege elevation. Many `bwrap` deployments rely on setuid, so
|
||||
// we avoid this unless we need seccomp or we are explicitly using the
|
||||
// legacy Landlock filesystem pipeline.
|
||||
if !sandbox_policy.has_full_network_access()
|
||||
if install_network_seccomp
|
||||
|| (apply_landlock_fs && !sandbox_policy.has_full_disk_write_access())
|
||||
{
|
||||
set_no_new_privs()?;
|
||||
}
|
||||
|
||||
if !sandbox_policy.has_full_network_access() {
|
||||
if install_network_seccomp {
|
||||
install_network_seccomp_filter_on_current_thread()?;
|
||||
}
|
||||
|
||||
@@ -72,6 +76,15 @@ pub(crate) fn apply_sandbox_policy_to_current_thread(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn should_install_network_seccomp(
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> bool {
|
||||
// Managed-network sessions should remain fail-closed even for policies that
|
||||
// would normally grant full network access (for example, DangerFullAccess).
|
||||
!sandbox_policy.has_full_network_access() || allow_network_for_proxy
|
||||
}
|
||||
|
||||
/// Enable `PR_SET_NO_NEW_PRIVS` so seccomp can be applied safely.
|
||||
fn set_no_new_privs() -> Result<()> {
|
||||
let result = unsafe { libc::prctl(libc::PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) };
|
||||
@@ -183,3 +196,38 @@ fn install_network_seccomp_filter_on_current_thread() -> std::result::Result<(),
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::should_install_network_seccomp;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn managed_network_enforces_seccomp_even_for_full_network_policy() {
|
||||
assert_eq!(
|
||||
should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, true),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_network_policy_without_managed_network_skips_seccomp() {
|
||||
assert_eq!(
|
||||
should_install_network_seccomp(&SandboxPolicy::DangerFullAccess, false),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restricted_network_policy_always_installs_seccomp() {
|
||||
assert!(should_install_network_seccomp(
|
||||
&SandboxPolicy::ReadOnly,
|
||||
false
|
||||
));
|
||||
assert!(should_install_network_seccomp(
|
||||
&SandboxPolicy::ReadOnly,
|
||||
true
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use std::os::fd::FromRawFd;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::bwrap::BwrapNetworkMode;
|
||||
use crate::bwrap::BwrapOptions;
|
||||
use crate::bwrap::create_bwrap_command_args;
|
||||
use crate::landlock::apply_sandbox_policy_to_current_thread;
|
||||
@@ -40,6 +41,14 @@ pub struct LandlockCommand {
|
||||
#[arg(long = "apply-seccomp-then-exec", hide = true, default_value_t = false)]
|
||||
pub apply_seccomp_then_exec: bool,
|
||||
|
||||
/// Internal compatibility flag.
|
||||
///
|
||||
/// By default, restricted-network sandboxing uses isolated networking.
|
||||
/// If set, sandbox setup switches to proxy-only network mode
|
||||
/// (currently enforced the same as isolated networking).
|
||||
#[arg(long = "allow-network-for-proxy", hide = true, default_value_t = false)]
|
||||
pub allow_network_for_proxy: bool,
|
||||
|
||||
/// When set, skip mounting a fresh `/proc` even though PID isolation is
|
||||
/// still enabled. This is primarily intended for restrictive container
|
||||
/// environments that deny `--proc /proc`.
|
||||
@@ -64,6 +73,7 @@ pub fn run_main() -> ! {
|
||||
sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
apply_seccomp_then_exec,
|
||||
allow_network_for_proxy,
|
||||
no_proc,
|
||||
command,
|
||||
} = LandlockCommand::parse();
|
||||
@@ -75,18 +85,24 @@ pub fn run_main() -> ! {
|
||||
// Inner stage: apply seccomp/no_new_privs after bubblewrap has already
|
||||
// established the filesystem view.
|
||||
if apply_seccomp_then_exec {
|
||||
if let Err(e) =
|
||||
apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, false)
|
||||
{
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
&sandbox_policy_cwd,
|
||||
false,
|
||||
allow_network_for_proxy,
|
||||
) {
|
||||
panic!("error applying Linux sandbox restrictions: {e:?}");
|
||||
}
|
||||
exec_or_panic(command);
|
||||
}
|
||||
|
||||
if sandbox_policy.has_full_disk_write_access() {
|
||||
if let Err(e) =
|
||||
apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, false)
|
||||
{
|
||||
if sandbox_policy.has_full_disk_write_access() && !allow_network_for_proxy {
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
&sandbox_policy_cwd,
|
||||
false,
|
||||
allow_network_for_proxy,
|
||||
) {
|
||||
panic!("error applying Linux sandbox restrictions: {e:?}");
|
||||
}
|
||||
exec_or_panic(command);
|
||||
@@ -100,15 +116,25 @@ pub fn run_main() -> ! {
|
||||
&sandbox_policy_cwd,
|
||||
&sandbox_policy,
|
||||
use_bwrap_sandbox,
|
||||
allow_network_for_proxy,
|
||||
command,
|
||||
);
|
||||
run_bwrap_with_proc_fallback(&sandbox_policy_cwd, &sandbox_policy, inner, !no_proc);
|
||||
run_bwrap_with_proc_fallback(
|
||||
&sandbox_policy_cwd,
|
||||
&sandbox_policy,
|
||||
inner,
|
||||
!no_proc,
|
||||
allow_network_for_proxy,
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy path: Landlock enforcement only, when bwrap sandboxing is not enabled.
|
||||
if let Err(e) =
|
||||
apply_sandbox_policy_to_current_thread(&sandbox_policy, &sandbox_policy_cwd, true)
|
||||
{
|
||||
if let Err(e) = apply_sandbox_policy_to_current_thread(
|
||||
&sandbox_policy,
|
||||
&sandbox_policy_cwd,
|
||||
true,
|
||||
allow_network_for_proxy,
|
||||
) {
|
||||
panic!("error applying legacy Linux sandbox restrictions: {e:?}");
|
||||
}
|
||||
exec_or_panic(command);
|
||||
@@ -119,6 +145,7 @@ fn run_bwrap_with_proc_fallback(
|
||||
sandbox_policy: &codex_core::protocol::SandboxPolicy,
|
||||
inner: Vec<String>,
|
||||
mount_proc: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> ! {
|
||||
let mut mount_proc = mount_proc;
|
||||
|
||||
@@ -127,11 +154,28 @@ fn run_bwrap_with_proc_fallback(
|
||||
mount_proc = false;
|
||||
}
|
||||
|
||||
let options = BwrapOptions { mount_proc };
|
||||
let network_mode = bwrap_network_mode(sandbox_policy, allow_network_for_proxy);
|
||||
let options = BwrapOptions {
|
||||
mount_proc,
|
||||
network_mode,
|
||||
};
|
||||
let argv = build_bwrap_argv(inner, sandbox_policy, sandbox_policy_cwd, options);
|
||||
exec_vendored_bwrap(argv);
|
||||
}
|
||||
|
||||
fn bwrap_network_mode(
|
||||
sandbox_policy: &codex_core::protocol::SandboxPolicy,
|
||||
allow_network_for_proxy: bool,
|
||||
) -> BwrapNetworkMode {
|
||||
if allow_network_for_proxy {
|
||||
BwrapNetworkMode::ProxyOnly
|
||||
} else if sandbox_policy.has_full_network_access() {
|
||||
BwrapNetworkMode::FullAccess
|
||||
} else {
|
||||
BwrapNetworkMode::Isolated
|
||||
}
|
||||
}
|
||||
|
||||
fn build_bwrap_argv(
|
||||
inner: Vec<String>,
|
||||
sandbox_policy: &codex_core::protocol::SandboxPolicy,
|
||||
@@ -164,7 +208,10 @@ fn preflight_proc_mount_support(
|
||||
preflight_command,
|
||||
sandbox_policy,
|
||||
sandbox_policy_cwd,
|
||||
BwrapOptions { mount_proc: true },
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
},
|
||||
);
|
||||
let stderr = run_bwrap_in_child_capture_stderr(preflight_argv);
|
||||
!is_proc_mount_failure(stderr.as_str())
|
||||
@@ -268,6 +315,7 @@ fn build_inner_seccomp_command(
|
||||
sandbox_policy_cwd: &Path,
|
||||
sandbox_policy: &codex_core::protocol::SandboxPolicy,
|
||||
use_bwrap_sandbox: bool,
|
||||
allow_network_for_proxy: bool,
|
||||
command: Vec<String>,
|
||||
) -> Vec<String> {
|
||||
let current_exe = match std::env::current_exe() {
|
||||
@@ -290,6 +338,9 @@ fn build_inner_seccomp_command(
|
||||
inner.push("--use-bwrap-sandbox".to_string());
|
||||
inner.push("--apply-seccomp-then-exec".to_string());
|
||||
}
|
||||
if allow_network_for_proxy {
|
||||
inner.push("--allow-network-for-proxy".to_string());
|
||||
}
|
||||
inner.push("--".to_string());
|
||||
inner.extend(command);
|
||||
inner
|
||||
@@ -342,7 +393,10 @@ mod tests {
|
||||
vec!["/bin/true".to_string()],
|
||||
&SandboxPolicy::ReadOnly,
|
||||
Path::new("/"),
|
||||
BwrapOptions { mount_proc: true },
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::FullAccess,
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
argv,
|
||||
@@ -366,4 +420,38 @@ mod tests {
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserts_unshare_net_when_network_isolation_requested() {
|
||||
let argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&SandboxPolicy::ReadOnly,
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::Isolated,
|
||||
},
|
||||
);
|
||||
assert_eq!(argv.contains(&"--unshare-net".to_string()), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn inserts_unshare_net_when_proxy_only_network_mode_requested() {
|
||||
let argv = build_bwrap_argv(
|
||||
vec!["/bin/true".to_string()],
|
||||
&SandboxPolicy::ReadOnly,
|
||||
Path::new("/"),
|
||||
BwrapOptions {
|
||||
mount_proc: true,
|
||||
network_mode: BwrapNetworkMode::ProxyOnly,
|
||||
},
|
||||
);
|
||||
assert_eq!(argv.contains(&"--unshare-net".to_string()), true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxy_only_mode_takes_precedence_over_full_network_policy() {
|
||||
let mode = bwrap_network_mode(&SandboxPolicy::DangerFullAccess, true);
|
||||
assert_eq!(mode, BwrapNetworkMode::ProxyOnly);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user