构建安全有效的沙箱,在 Windows 上启用 Codex
Building a safe, effective sandbox to enable Codex on Windows
OpenAI的Codex工程团队为Windows平台实现了编码agent沙箱,以解决Windows用户在使用Codex时缺乏安全执行环境的问题。团队评估了AppContainer、Windows Sandbox和强制完整性控制标签等现有工具,发现均不适用。首个原型采用非提权沙箱,通过合成SID和写限制令牌控制文件写入,但网络限制仅依赖环境变量,易被绕过。最终设计为提权沙箱,创建专用Windows用户(CodexSandboxOffline/Online)并配合防火墙规则,通过codex.exe、codex-windows-sandbox-setup.exe和codex-command-runner.exe三个二进制文件协同工作,在安全性与可用性间取得平衡。
当我于2025年9月加入Codex工程团队时,Codex for Windows还没有沙箱实现,这意味着Windows用户在使用OpenAI的编码agent时,被迫在两种次优选项之间做出选择:
- 批准编码agent想要运行的几乎所有命令(甚至包括读取操作),这既低效又烦人。使用Codex的一大好处就是你不需要亲自完成所有繁琐的工作。
- 启用完全访问模式:让Codex无需批准或限制即可运行所有命令,这消除了摩擦,但牺牲了监督。
我们的编码agent在开发者的笔记本电脑上运行——无论是通过CLI、IDE扩展还是桌面应用。它管理着键盘前的人类与云端运行的模型之间的对话,以处理推理。
默认情况下,Codex以真实用户的权限运行,这意味着它可以做用户能做的一切事情。这既强大又潜在危险。编码模型可能会指示harness在本地运行命令,从运行测试、读取或编辑文件,到创建Git分支,因此Codex的默认模式试图在有效性和安全性之间找到合适的平衡。这种默认模式允许Codex在几乎所有地方读取文件,并在你的工作区(即你运行Codex的目录)内写入文件,除非你指定需要,否则无法访问互联网。为了在安全范围内自动实现对文件写入和网络访问的约束,Codex需要一个能够实际执行这些约束的沙箱环境。
沙箱是一个受限的执行环境。当开发者使用Codex时,他们计算机的操作系统会以降低的权限启动一个命令,并且这些约束会沿着进程树向下传播。每个Codex命令从一开始就被沙箱化,并且每个后代进程都保持在相同的边界内。
Codex需要由计算机操作系统强制执行的隔离功能来实现有效的沙箱。一些操作系统提供了能很好实现这一点的工具(例如macOS上的Seatbelt,Linux上的seccomp或bubblewrap);然而,Windows目前并不开箱即用地提供这种能力。
为了让Codex在Windows上的使用与其他地方一样安全且令人愉悦,我们需要实现自己的沙箱。
现有Windows工具的不足之处
Windows提供了一些用于隔离的工具和原语。虽然它们都不完全符合我们的要求,但我们研究了许多潜在的解决方案——即AppContainer、Windows Sandbox和强制完整性控制(MIC)标签。
AppContainer
- 是什么: AppContainer是原生的Windows沙箱,一种基于能力的隔离模型,专为那些预先确切知道需要访问什么的应用而构建。
- 为什么考虑: 很有吸引力,因为它提供了真正的操作系统边界,而不是尽力而为的限制。
- 为什么不选: Codex不是一个范围严格限定的应用。它驱动着开放式的开发者工作流程:shell、Git、Python、包管理器、构建工具,以及agent决定需要的任何其他二进制文件。在实践中,这使得AppContainer不适合解决这个问题。它的隔离性很强,但针对的工作负载范围比“让agent像开发者一样操作”要窄得多。
Windows Sandbox
- 是什么: Windows Sandbox是微软的一次性轻量级虚拟机。你会得到一个具有强隔离边界的全新Windows桌面,并且你在其中所做的任何事情都会在会话结束时消失。
- 为什么考虑: 出于显而易见的原因很有趣——与任意软件的兼容性远高于AppContainer,并且从安全角度来看,它是一个更强大的盒子。
- 为什么不选: Codex需要直接作用于用户实际的代码检出、工具和环境,而不是在一个需要设置和主机/客户机桥接的单独一次性桌面内部。它还有一个根本性的产品问题:Windows Sandbox甚至在Windows Home版上不可用。
强制完整性控制(MIC)完整性标签
- 是什么: Windows有一个称为“完整性级别”的概念,例如低、中、高,它决定了系统对对象和进程的信任程度。基本规则是,低完整性进程不能写入具有更高完整性级别的对象,即使正常的ACL(访问控制列表)允许这样做。例如,低完整性进程被视为不太受信任,因此Windows会阻止它写入正常的中完整性对象,除非这些对象被明确重新标记以允许写入。
- 为什么考虑: MIC在纸面上看起来很优雅——以低完整性运行Codex,将可写根目录重新标记为低完整性,然后让Windows在其他所有地方强制执行禁止写入。这将为我们提供一条非管理员路径,背后有真正的操作系统机制支持。
- 为什么不选: 与ACL一样,完整性标签会修改真实的主机文件系统,并且在这种情况下,语义变化尤其广泛。将工作区标记为低完整性不仅仅意味着“Codex可以在这里写入”。它意味着低完整性进程通常可以在这里写入。在真实的开发者机器上,这会将用户实际的代码检出变成主机的低完整性接收器,这比为单个沙箱设计授予精心定位的ACL要危险得多。即使中完整性的开发者工具继续工作,工作区的底层信任模型也已经以一种难以控制且更难证明合理的方式发生了变化。
在评估了所有选项并认为它们都不可行之后,我们开始设计自己的解决方案,以便为Windows用户带来良好的Codex体验。
第一个原型:“非提权沙箱”
我们的第一个工作原型结合使用了Windows的概念和工具来实现我们需要的隔离。从一开始,一个目标就是使其无需提权即可工作,这意味着Codex不需要仅仅为了设置或运行沙箱而提示用户授予管理员权限。这意味着要弄清楚如何对两件事施加合理的限制:文件写入和网络访问。
如果我们完全不限制文件写入,就会存在安全问题。如果我们过度限制文件写入,沙箱会损害用户的生产力,需要不断请求批准。为了解决这个问题,我们依赖两个重要的Windows构建块:SID和写限制令牌。
SID,即安全标识符,是Windows与权限关联的身份。每个用户都有一个SID,组有SID,甚至单个登录会话也有自己的SID。例如,当前登录的会话可能有一个像S-1-5-5-X-Y这样的SID。分配给本地管理员组的SID可能是S-1-5-32-544。
Windows还允许你创建不对应于真实用户但可以出现在ACL中的合成SID,ACL定义了谁可以读取/写入/执行特定文件或目录。这使得SID成为我们沙箱的一个有用原语:我们可以创建专供Codex沙箱使用的SID,而不会干扰机器上的任何其他内容。
进程令牌是Windows中的安全对象,用于定义正在运行的进程的身份和权限。它们决定了进程可以执行哪些操作。写限制令牌是一种特殊类型的进程令牌,它使Windows对写入操作执行额外的访问检查。
为了使写入成功,必须通过两项检查:
- 正常的用户身份(令牌“所有者”)必须被允许执行该操作
- 令牌受限SID列表中的至少一个SID也必须被授予访问权限
在实践中,这些检查让我们能够使用ACL精确定义沙箱可以在文件系统的哪些位置进行修改,这为我们提供了写入操作所需的粒度。
通过SID和写限制令牌,我们的非提权沙箱工作方式如下:
- 沙箱设置创建了一个名为
sandbox-write的合成SID。 sandbox-writeSID被授予对以下位置的写入、执行和删除访问权限:- 当前工作目录
- 在
config.toml中配置的任何其他writable_roots。
- 沙箱设置明确拒绝同一SID对“可写区域内只读”位置的写入访问,例如:
<cwd>/.git<cwd>/.codex<cwd>/.agents
- Codex在写限制令牌下启动命令,该令牌的受限SID列表包括
Everyone、当前登录会话SID和sandbox-write合成SID。
这个流程有效地解决了限制文件写入的问题,看起来很有希望。现在我们需要一个解决方案来限制沙箱的网络访问。
限制网络访问是沙箱的重要组成部分;没有它,恶意代码可以将机器上的数据泄露到互联网。因为我们希望避免提权要求,所以我们在强力阻止网络流量方面的选择有限。我们想使用的工具,如Windows防火墙,通常在没有管理员权限的情况下无法安装。
由于无法使用Windows防火墙,我们只能限制我们能控制的内容。我们试图让子环境对于开发者实际使用的联网工具采用“失败关闭”模式,这样Git命令、包安装程序等会在沙箱中失败,用户必须批准任何面向互联网的操作。我们的想法是毒化明显的逃生通道:将代理感知流量发送到一个死端点,让Git的HTTP(S)传输也这样做,并使基于SSH的Git立即失败。除此之外,我们在PATH前面添加了一个小的denybin目录,并重新排序了PATHEXT,以便存根SSH和SCP脚本能在真实二进制文件之前被解析。
例如,以下是我们用来限制网络访问的一些特定环境覆盖:
HTTPS_PROXY=http://127.0.0.1:9ALL_PROXY=http://127.0.0.1:9GIT_HTTPS_PROXY=http://127.0.0.1:9NO_PROXY=localhost,127.0.0.1,::1GIT_SSH_COMMAND=cmd /c exit 1
这捕获了大量正常的工具驱动流量,但它仍然只是建议性的。一个进程可以忽略环境变量、绕过PATH,或者直接打开套接字——风险太大。
与任何有趣的软件实现一样,第一个原型有一些优点和缺点。虽然它仅使用几个标准的Windows功能就完成了工作,允许非常明确和细粒度的文件系统写入,并且无需提权即可运行——减少了用户接受过多提权提示或成为本地机器管理员的需求——但它也有一些真正的缺点,其中一些缺点使其无法成为我们的最终设计:
- 设置速度:根据工作区目录的拓扑结构,应用工作区ACL可能代价高昂。
- 占用空间:我们对开发者的系统应用了真实的ACL,尽管这种占用空间并非特别具有侵入性,因为所有应用的ACL都只与沙箱使用的自定义创建的合成SID相关。
- 难以更改的语义:依赖ACL进行基于文件的限制意味着更改沙箱语义代价高昂且复杂。而在macOS上,我们可以动态更改用于配置Seatbelt的
.sbpl文件的生成方式,Windows沙箱可能需要缓慢且密集的操作来调整ACL。 - 网络保护薄弱。如前所述,它是“建议性的”,肯定会被一些实现自己网络栈的程序绕过,并且其设计初衷并非抵御对抗性代码。
前三个问题是自定义沙箱实现所固有的,这种实现需要足够灵活以支持agentic工作流。然而,网络抑制的情况则不同。
除了恶意agent能够轻易绕过基于环境的网络抑制之外,许多善意的代码/二进制文件也会仅仅因为不遵守环境代理变量,或者实现了自己的基于套接字的网络代码而绕过它。我们认为这方面足以让我们考虑投入资源开发更好的沙箱模式。
为了获得更好的网络抑制,我们想使用Windows防火墙,它允许我们阻止用户或程序的出站网络流量。不幸的是,由于几个原因,我们无法有效地创建一个仅适用于由Codex harness生成的命令的防火墙规则:
- Windows不允许将防火墙规则与受限令牌的非主体身份匹配。这意味着我们无法将防火墙规则应用于“任何在其受限SID列表中包含我们合成SID的令牌”。
- 虽然我们可以创建匹配特定二进制文件的防火墙规则,但这只能限制
codex.exe本身的网络功能。它不适用于agent代表用户生成的进程,例如Git或Python进程。 - 其他防火墙匹配维度也不合适。用户范围的规则仍然匹配非提权设计中的真实Windows用户,而不仅仅是受限子进程。程序路径规则过于粗糙:它们可以普遍阻止
codex.exe或python.exe,但不能阻止这一次沙箱化的python.exe调用。基于端口或地址的规则也完全是错误的策略。例如,我们不想阻止端口443;我们想阻止这个特定受限进程树的任意出站访问。
为了将防火墙规则专门应用于我们的沙箱化命令,我们需要将它们作为单独的主体运行,而不是作为“真实”用户。这种方法引导我们走上了一条新路,一条我们放宽了“无需提权”约束的道路。
重新设计:“提权沙箱”
沙箱的下一个迭代版本,也就是我们当前的实现,在设置时需要提升的管理员权限。因此,我将其称为“提权沙箱”。在Codex在系统上生成命令的边界处,提权沙箱看起来与非提权沙箱类似。它仍然在受限令牌下运行子进程——类似地是一个write_restricted令牌,具有相同的受限SID列表[Everyone, Logon, Synthetic]——然而,这个令牌的主体不再是实际的Windows用户,而是Codex自身创建的两个本地用户之一:
CodexSandboxOffline(受防火墙规则限制的那个)CodexSandboxOnline(不受防火墙规则限制的那个)
这个看似微小的细节实际上对沙箱、谁可以使用它以及其设置和运行时执行的复杂性有着重大影响。
它在视觉上与非提权原型相似,但引入了防火墙规则和一个专用的Windows用户,该用户实际运行命令。(然而,这些新概念的引入意味着在沙箱可以开始运行和保护命令之前,需要进行更多的设置工作。)
非提权沙箱设计的设置步骤很简单,但相对较少:
- 如果需要,创建一个合成SID
- 为sandbox-write合成SID应用ACL
然而,提权沙箱需要做更多事情。
- 如果尚未创建,创建一个合成SID
- 如果尚未创建,创建在线和离线沙箱用户
- 在本地存储新创建用户的凭据,并使用Windows数据保护API(DPAPI)在沙箱用户无法实际读取的位置进行加密
- 创建防火墙规则,阻止
CodexSandboxOffline用户的所有出站网络访问,或者如果规则已存在,则验证它们是否正确
设置阶段还有一个额外的复杂之处。Codex的沙箱应具有等同于实际Windows用户的读取访问权限。在非提权沙箱中,受限令牌的主体SID是Windows用户,这得以实现。然而,当主体变为一个新的CodexSandbox用户时,这并非免费获得。Windows上的许多相关目录会授予“已验证用户”读取/执行权限。一个显著的例子是用户的配置文件目录。默认情况下,Windows用户无法读取其他Windows用户的配置文件目录,因此在许多场景下,即使是简单的文件读取也会失败。
为了解决这个问题,我们在沙箱设置过程中增加了另一层——为沙箱用户授予可能尚不存在的读取ACL。例如,针对一些常用的Windows目录:
C:\Users\<real-user>C:\Windows\C:\Program Files\C:\Program Files (x86)\C:\ProgramData\
由于这个目录列表是尽力而为的,并且在每个目录上安装ACL可能相当昂贵,我们异步运行此逻辑,以便阻塞用户的沙箱设置步骤不必等待它们完成。
我们将设置逻辑封装在自己的二进制文件中,部分原因是为了仅在需要时跨越UAC边界。但更深层的原因是架构上的:沙箱设置与codex.exe有着根本不同的工作。将沙箱设置逻辑保留在专用二进制文件中,使得codex.exe可以保持为普通的、非提权的harness;防止仅限Windows的设置机制在其他平台上膨胀codex.exe;将运行时间较长的设置工作与主进程的生命周期解耦;并为我们提供了一个地方来处理沙箱所需的不同设置路径。
由于Windows用户和令牌登录边界的工作方式,我们无法像在非提权沙箱中那样继续创建受限令牌并在其下生成进程。为了实际以不同的Windows用户身份生成命令,我们的第一个想法是以下流程:
codex.exe以真实Windows用户身份运行。然后,按顺序,Codex:- 为沙箱用户调用
LogonUserW(...)。 - 在该沙箱用户令牌上调用
CreateRestrictedToken(...)。 - 使用该受限的沙箱用户令牌,调用
CreateProcessAsUserW(...)来启动最终的子进程。
- 为沙箱用户调用
在实践中,由于CreateProcessAsUserW(...)处的权限壁垒,这个期望的流程没有成功。这意味着codex.exe可以为沙箱用户创建一个受限令牌,但它无法从真实用户侧边界可靠地使用该令牌启动子进程。我们需要一个已经以沙箱用户身份运行的进程——这将允许限制步骤和最终生成发生在沙箱用户侧边界,而不是真实用户侧边界。
这个需求催生了codex-command-runner.exe,一个新的二进制文件,其唯一工作是生成受限令牌并启动请求的命令。我们没有要求codex.exe自己完成整个流程(真实用户 → 沙箱用户 → 受限令牌 → 子进程),而是将流程分为两部分:
第1部分
codex.exe调用CreateProcessWithLogonW(...)来启动codex-command-runner.exe作为沙箱用户,此时尚未使用受限令牌。
第2部分
- 在runner内部,
OpenProcessToken(GetCurrentProcess(), ...)打开runner自己的令牌,该令牌已经属于沙箱用户。 - Runner调用
GetTokenInformation(...)来提取沙箱登录SID,然后调用CreateRestrictedToken(...)来构建最终的受限令牌。 - 仍在runner内部,它使用该受限令牌调用
CreateProcessAsUserW(...)来启动真正的子进程。
阿尔伯特·爱因斯坦说过:“凡事都应尽可能简洁,但不能过于简单。”本着这种精神,我们的设计充分解决了每个问题。最终的架构包含我们之前介绍过的四个层次:
codex.exe本身codex-windows-sandbox-setup.exe用于处理所有与提权设置相关的工作codex-command-runner.exe用于运行受限令牌命令- 子进程
当我最初接触这个项目时,我并不清楚它会走向何方。我的方法是首先在Codex和操作系统之间的边界上检测沙箱能力。这种方法与Codex在MacOS和Linux上实现沙箱的方式非常接近。
随着我对Windows提供的特定工具有了更多了解,并通过数十次在安全性和易用性之间权衡的决策,系统逐渐发展成现在的形式——多个二进制文件、自定义用户、防火墙规则、提权设置步骤、异步进程等等。
这不是一个特别简单的系统,但每一处复杂性都是出于必要而添加的,目的是构建一个既安全又尽可能不打扰用户的沙箱。
在安全性与实际可用性之间取得平衡
为了为Windows上的Codex用户提供良好的用户体验,我们的目标是构建一个安全且不牺牲可用性的东西——使用Codex的全部意义在于让agent能够在无需你持续关注的情况下完成工作。
从这个项目中学到的最重要的教训之一是,Windows并没有直接提供一个可以干净地映射到“安全的自主编码agent”的原语。我们组合了几种工具和概念来构建一个连贯的整体。一些早期的想法是死胡同。最终设计是早期原型的混合体,每个原型都解决了部分问题。
另一个教训是,编码agent的安全性与更经典的应用程序安全性是不同的事物。Codex必须适用于真实的开发者工作流程。工程工作在于平衡与agentic工作负载的兼容性和实际执行。这种张力塑造了最终设计中的权衡。
想看看Codex沙箱的实际效果吗?立即尝试。