Windows 沙箱 · 沙箱用户、ACL 与防火墙

Windows 没有现成的“按策略跑命令”工具,Codex 用三种实现拼出沙箱:unelevated 以当前用户的写受限令牌运行,靠能力 SID 与 ACL 限制写入;elevated 先经 UAC 建两个本地沙箱用户、配好 ACL 与防火墙,再以沙箱用户身份启动 codex-command-runner,由它在私有桌面上派生受限令牌跑命令;mxc 则交给系统的进程安全环境。本篇讲清三者的分工与各自的边界。

作者 David更新于 第 31 篇(共 57 篇)

Windows 沙箱 · 沙箱用户、ACL 与防火墙

前两篇的 macOS 与 Linux 都有内核现成的隔离原语可用。Windows 这边,codex-windows-sandbox(目录 windows-sandbox-rs,去掉测试约 1.9 万行)把令牌、ACL、本地用户、防火墙、桌面对象这些 Win32 安全机制组合起来,拼出与另外两个平台同等语义的沙箱。

用户看到的样子

在 config.toml 的 [windows] 表里设 sandbox = "elevated"、"unelevated" 或 "mxc"。选 elevated 时第一次需要管理员授权(UAC 弹窗),TUI 里的 /setup-default-sandbox 就是做这件事的。没有启用 Windows 沙箱时,sandbox_mode = "workspace-write" 会被降级为只读,隐式默认的配置档也只给 :read-only。排查问题看 CODEX_HOME/.sandbox/ 下按天滚动的日志 sandbox.<日期>.log,最多保留 90 份。各模式的取舍见手册的沙箱、审批与安全。

三种实现怎么选

impl WindowsSandboxLevelExt for WindowsSandboxLevel {
    fn from_config(config: &Config) -> WindowsSandboxLevel {
        match config.permissions.windows_sandbox_mode {
            Some(WindowsSandboxModeToml::Elevated) => WindowsSandboxLevel::Elevated,
            Some(WindowsSandboxModeToml::Unelevated) => WindowsSandboxLevel::RestrictedToken,
            Some(WindowsSandboxModeToml::Mxc) => WindowsSandboxLevel::Disabled,
            None => Self::from_features(&config.features),
        }
    }

(codex-rs/core/src/windows_sandbox.rs:61)

elevated 与 unelevated 在 SandboxManager 眼里是同一种 SandboxType::WindowsRestrictedToken,由级别区分后端;MXC 的级别是 Disabled,另用独立的 SandboxType::WindowsMxc 表示(effective_windows_sandbox_type,codex-rs/protocol/src/sandbox.rs:31)。没写 [windows] sandbox 时,还会读已移除的旧特性开关 elevated_windows_sandbox、experimental_windows_sandbox 做兼容。

图表加载中…

共同底座:写受限令牌与能力 SID

两种非 MXC 实现最终都用 CreateRestrictedToken 造一个令牌来跑命令:

    // Exact order: Capabilities..., ExtraRestricting..., Logon, Everyone
    // ...
    let mut new_token: HANDLE = 0;
    let flags = DISABLE_MAX_PRIVILEGE | LUA_TOKEN | WRITE_RESTRICTED;
    let ok = CreateRestrictedToken(
        base_token,
        flags,
        0,
        std::ptr::null(),
        0,
        std::ptr::null(),
        entries.len() as u32,
        entries.as_mut_ptr(),
        &mut new_token,

(codex-rs/windows-sandbox-rs/src/token.rs:481)

DISABLE_MAX_PRIVILEGE 去掉特权,LUA_TOKEN 按受限用户处理,关键是 WRITE_RESTRICTED:访问检查时,“限制 SID 列表”只参与写操作的判定。列表里放的是若干“能力 SID”,外加登录 SID 与 Everyone。能力 SID 是 Codex 随机生成的 S-1-5-21-... 形式的 SID,持久化在 CODEX_HOME/cap_sid 里:每个工作目录一个,每个额外可写根一个(codex-rs/windows-sandbox-rs/src/cap.rs)。Codex 在可写根上给对应的能力 SID 加允许写的 ACE,在 .codex、.agents 这类保护目录上加拒绝写的 ACE,于是持有这个令牌的进程只能写进 ACL 明确放行的地方,读则不受影响。令牌的默认 DACL 还被改成只授予本次登录会话,注释的理由是能力 SID 可能被多次启动共享,不能让一次启动碰到另一次的进程、线程和 IPC 对象。

WRITE_RESTRICTED 也划出了这条路的边界:拒绝读的 ACE 对能力 SID 不起作用,所以读禁区只能由 elevated 后端执行。unelevated 遇到带读限制的配置档会直接拒绝运行(“refusing to run unsandboxed”,codex-rs/sandboxing/src/windows.rs:109 的注释说明了原因),不会假装做到了。

unelevated:当前用户,隔离较弱

unelevated(代码里叫 legacy 后端)不需要管理员权限:以当前用户的令牌为底派生写受限令牌,直接用 ConPTY 或管道起进程,放进作业对象(job object)统一管理。它的弱点在网络:网络受限时,apply_no_network_to_env 只是改写环境变量,把 HTTP_PROXY、HTTPS_PROXY、ALL_PROXY 指向 127.0.0.1:9,给 pip、npm、cargo 打开离线开关,把 GIT_SSH_COMMAND 设成一个必然失败的命令,再把一个放着 ssh、scp 替身的目录插到 PATH 最前面(codex-rs/windows-sandbox-rs/src/env.rs:126)。不理会代理变量的程序照样能联网。受管网络代理也要求 elevated 后端,unelevated 遇到会直接报错。

elevated:两个专用的沙箱用户

elevated 的前提是一次性的 setup。run_elevated_setup 用 ShellExecuteExW 的 runas 动词拉起 codex-windows-sandbox-setup.exe,弹出 UAC,把要做的事以 base64 负载传过去:

  • 建本地组 CodexSandboxUsers 与两个用户 CodexSandboxOffline、CodexSandboxOnline,密码是 24 位随机字符,经 DPAPI 加密后存进 CODEX_HOME/.sandbox-secrets/sandbox_users.json;两个账户还登记到 Winlogon 的 SpecialAccounts\UserList,不出现在登录界面。
  • 给沙箱用户配读 ACL。它们是另一个账户,本来读不了你的文件,所以按配置档授予读权限:全盘可读的配置档对应 C:\Windows、Program Files、ProgramData、工作区与可写根,以及用户目录下的第一层条目,但排除 .ssh、.aws、.azure、.kube、.docker、.gnupg、.config 等十二个目录(USERPROFILE_ROOT_EXCLUSIONS)。
  • 只针对离线用户的 SID 装 Windows 防火墙规则:拦截非回环的出站与入站、除代理端口外的回环 TCP、全部回环 UDP(允许本地绑定时去掉两条回环规则);再用 Windows 筛选平台(WFP)拦截该用户的 ICMP、DNS(53、853 端口)与 SMB(445、139 端口),普通 setup 中这一步尽力而为。
  • 写入 CODEX_HOME/.sandbox/setup_marker.json,版本号 SETUP_VERSION 当前为 5,版本不符就要重新 setup。

之后每次执行前,只需不提权地刷新一遍当前工作区的 ACL。选哪个账户取决于网络:

impl SandboxNetworkIdentity {
    pub(crate) fn from_permissions(
        permissions: &ResolvedWindowsSandboxPermissions,
        proxy_enforced: bool,
    ) -> Self {
        if proxy_enforced || !permissions.network_policy().is_enabled() {
            Self::Offline
        } else {
            Self::Online
        }
    }

(codex-rs/windows-sandbox-rs/src/setup.rs:745)

网络的隔离就这样落在“以哪个用户身份运行”上:离线用户受防火墙约束,受管代理时也用它,只放行到代理端口的回环连接;在线用户不受这些规则约束。

执行时,Codex 先建一对命名管道,DACL 只允许沙箱用户连接,然后以沙箱用户的账号密码调用 CreateProcessWithLogonW,启动 codex-command-runner.exe --pipe-in=... --pipe-out=...:

    let spawn_res = unsafe {
        CreateProcessWithLogonW(
            user_w.as_ptr(),
            domain_w.as_ptr(),
            password_w.as_ptr(),
            if registered_alias.is_some() {
                LOGON_WITH_PROFILE
            } else {
                0
            },
            exe_w.as_ptr(),
            cmdline_vec.as_mut_ptr(),
            windows_sys::Win32::System::Threading::CREATE_NO_WINDOW
                | windows_sys::Win32::System::Threading::CREATE_UNICODE_ENVIRONMENT,
            // ...
        )

(codex-rs/windows-sandbox-rs/src/elevated/runner_client.rs:392)

runner 连上管道,读取带长度前缀的 JSON 帧(协议版本 IPC_PROTOCOL_VERSION 为 6):父进程发 SpawnRequest、Stdin、CloseStdin、Resize、Terminate,runner 回 SpawnReady、Output、Exit、Error。收到 SpawnRequest 后,runner 从沙箱用户自己的令牌派生写受限令牌(能力 SID 之外再加上用户 SID),要终端就用 ConPTY,否则用管道,把命令放进作业对象启动,输出一帧帧流回去。unified exec 的后台终端在 Windows 上就是这样跨账户工作的。

私有桌面

窗口站里的桌面是 Windows 的 GUI 隔离边界,同一桌面上的进程可以互相发窗口消息、装钩子。两种非 MXC 实现一律在私有桌面上运行:CreateDesktopW 建一个名为 CodexSandboxDesktop- 加 32 位十六进制随机数的桌面,DACL 只给当前用户全部权限、给沙箱账户去掉改 DACL、改属主和删除之外的参与权限,进程以 Winsta0\<名字> 为启动桌面。桌面按“沙箱账户 + 生效权限”复用,注释说明不同策略分开放,GUI 钩子就不会自动跨越策略。旧配置项 windows.sandbox_private_desktop 已经删除,残留时 strict config 会提示移除。

MXC:交给系统

mxc 走另一条路。SandboxManager 先确认 codex_mxc_sandbox::is_available():系统能创建原生的进程安全环境,而且注释写明刻意排除了 MXC 旧的 AppContainer 后备后端;不可用就直接报错,不退回其他实现。命令被改写成 codex --__codex-windows-mxc,配置档、策略 cwd、受管网络和原始 argv 编码进几个只给启动器用的环境变量,助手进程解码后生成 MXC 的执行请求,交给 BaseContainerRunner 启动;配置档里有拒绝路径时,还要求这个 Windows 版本支持拒绝路径。受管网络在 MXC 下必须有专用的代理端口,而且 allow_local_binding 必须为 true,因为 MXC 的主机回环访问是双向的,做不到“只出不进”(codex-rs/mxc-sandbox/src/lib.rs:25)。开发中的 prefer_mxc 开关打开后,系统可用时会优先选 MXC。

另外,codex-windows-sandbox-service 是一个注册到 SCM 的 Windows 服务,经本机 IPC 替已认证的客户端完成 elevated 沙箱的配置(provisioning);对应的 windows_sandbox_service 特性仍在开发中,默认关闭。

和《从 LLM 到 Coding Agent》对照

那本书的权限系统提醒 bypass 只该在一次性容器里开。Windows 上 elevated 实现做的事,正是在同一台机器上造出这样一个“别人”:独立的账户、独立的防火墙身份、独立的桌面,让命令天然碰不到你的凭据目录和窗口。代价是 setup 需要管理员权限、每次执行多一跳进程与管道;unelevated 省掉这些,换来的是只靠环境变量约束网络。三个平台里,Windows 是唯一需要“先建用户”的一个,这也是 macOS 与 Linux 两篇里没有的复杂度。


上一篇:Linux 沙箱 · bubblewrap 与 Landlock · 下一篇:网络代理 · 联网权限怎么落地

本页目录