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