用 Windows 安全原语为 AI 代码 Agent 构建沙箱——OpenAI Codex 的隔离设计拆解

2026-06-05 24 预计阅读时间:1 分钟
来源:infoq.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:11 分钟

自主编码 Agent 在本地开发环境里跑起来,听起来很酷,直到你意识到它可能删掉你的 .git、装一个恶意 npm 包、或者把 SSH 密钥读走。OpenAI 最近公开了 Codex Windows 沙箱的架构细节——他们没有另起炉灶发明一套隔离机制,而是把 Windows 已有的 SID、ACL、受限令牌(restricted token)和专用沙箱账户组合起来,让 Agent 在真实开发流程中安全执行。这套思路值得每个在做本地 Agent 安全的人看看。

难题:Agent 既要干活,又不能乱跑

Codex Agent 的场景不是"在容器里跑个脚本就完了"。它要:

  • 读写项目目录里的源码
  • 执行 npm installpip install 等构建命令
  • 调用 Git 操作(checkout、commit)
  • 不能访问用户的 SSH 密钥、浏览器 cookie、其他项目的代码

传统沙箱要么隔离太狠(Agent 连文件都看不到),要么太松(和用户同权限)。OpenAI 的方案是在 Windows 安全模型里找到中间地带:给 Agent 一个受限身份,让它只能碰允许碰的东西

四块积木:SID、ACL、受限令牌、沙箱账户

SID——身份的原子单位

Windows 里一切权限判断的起点是 SID(Security Identifier)。每个用户、组都有一个唯一 SID。Codex 沙箱的核心思路是:给 Agent 创建一个专用 SID,而不是复用用户自己的 SID

这意味着 Agent 进程在权限检查时,系统看到的是"沙箱账户"的身份,不是"你的登录账户"。即使 Agent 进程被攻破,攻击者拿到的也是一个低权限身份。

ACL——资源上的门禁列表

每个文件、目录、注册表键、管道对象上都挂着一条 ACL(Access Control List),里面逐条写明"谁可以做什么"。Codex 的做法是:

  • 项目目录:在 ACL 里给沙箱 SID 加上 Read/Write/Execute 权限
  • 用户主目录下的敏感子目录.sshAppData\Roaming 等):确保沙箱 SID 不在 ACL 的允许列表里
  • 系统关键路径C:\Windows\System32):只保留标准受限访问

ACL 的粒度可以精确到单个文件。这让"Agent 能改 src/ 但不能读 .ssh/id_rsa"变成一条声明式规则,而不是靠运行时拦截。

受限令牌——进程级别的权限剪裁

Windows 提供了一个强力但常被忽视的 API:CreateRestrictedToken。它可以从一个基础令牌上:

  • 禁用某些 SID(Deny-only:遇到这些 SID 时权限检查直接失败)
  • 删除某些特权(如 SeDebugPrivilegeSeLoadDriverPrivilege
  • 限制令牌为仅限当前会话

Codex 用受限令牌把沙箱进程的"能力上限"锁死。即使 ACL 里不小心给了多余权限,被禁用的 SID 和被删除的特权也会在权限评估时生效——双重保险。

专用沙箱账户——隔离的身份锚点

把上面三者串起来的是一个本地专用账户。这个账户:

  • 不在 Administrators 组里
  • 不在 Power Users 组里
  • 只属于一个自定义的低权限组(比如 CodexSandboxUsers
  • 密码由系统管理,用户不需要知道

Agent 进程以这个账户身份启动,拿到它的受限令牌,在 ACL 约束下操作文件。整个链条:账户 → SID → 受限令牌 → ACL → 资源

实践:用 PowerShell 模拟一个最小沙箱环境

下面是一个可以在 Windows 11 上直接运行的示例,模拟 Codex 沙箱的核心步骤:创建沙箱账户、配置 ACL、用受限令牌启动进程。运行前请确保你有管理员权限,且在测试机器上操作(不要在生产环境直接跑)。

步骤一:创建沙箱账户和自定义组

# 创建一个本地低权限组
net localgroup CodexSandboxUsers /add

# 创建沙箱账户,不设密码(由系统管理),不允许交互登录
net user CodexAgent /add /passwordchg:no /expires:never
net localgroup CodexSandboxUsers CodexAgent /add

# 禁止沙箱账户交互登录(防止有人用这个账户直接登桌面)
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList"
New-ItemProperty -Path $regPath -Name "CodexAgent" -Value 0 -PropertyType DWord -Force

步骤二:为目标项目目录配置 ACL

# 假设项目目录
$projectDir = "C:\Projects\my-app"

# 获取沙箱账户的 SID
$sandboxSid = (New-Object System.Security.Principal.NTAccount("CodexAgent")).Translate([System.Security.Principal.SecurityIdentifier])

# 获取当前 ACL
$acl = Get-Acl $projectDir

# 给沙箱账户授予 Modify 权限(读+写+执行,但不能改权限本身)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    $sandboxSid,
    "Modify",
    "ContainerInherit,ObjectInherit",
    "None",
    "Allow"
)
$acl.AddAccessRule($rule)
Set-Acl $projectDir $acl

# 确保沙箱账户无法访问 .ssh 目录(显式 Deny)
$sshDir = "$env:USERPROFILE\.ssh"
$sshAcl = Get-Acl $sshDir
$denyRule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    $sandboxSid,
    "FullControl",
    "ContainerInherit,ObjectInherit",
    "None",
    "Deny"
)
$sshAcl.AddAccessRule($denyRule)
Set-Acl $sshDir $sshAcl

步骤三:用受限令牌启动 Agent 进程

# 用 Start-Process 以沙箱账户身份启动(模拟受限令牌效果)
# 实际生产中会用 CreateRestrictedToken API 剪裁更多特权
# 这里用 PowerShell 模拟基本隔离

$agentScript = @"
Write-Host 'Agent running as:' 
whoami
Write-Host 'Attempting to read project file...'
Get-Content 'C:\Projects\my-app\README.md' | Select-Object -First 5
Write-Host 'Attempting to access .ssh...'
try {
    Get-Content '$env:USERPROFILE\.ssh\id_rsa' -ErrorAction Stop
    Write-Host 'DANGER: .ssh is accessible!'
} catch {
    Write-Host 'Good: .ssh access denied.'
}
"@

$agentScript | Out-File "C:\Projects\agent-runner.ps1" -Encoding UTF8

# 以沙箱账户身份启动(需要 SeIncreaseQuotaPrivilege,管理员默认持有)
Start-Process -FilePath "powershell.exe" `
    -ArgumentList "-ExecutionPolicy Bypass -File C:\Projects\agent-runner.ps1" `
    -Credential (New-Object PSCredential("CodexAgent", (New-Object System.Security.SecureString))) `
    -NoNewWindow -Wait

注意:上面的 Start-Process -Credential 在 PowerShell 中需要密码。真实生产场景中,OpenAI 使用 Win32 API CreateProcessAsUser + CreateRestrictedToken 组合,密码由 LSA secret 管理。这里为了可运行性做了简化。如果你想在本地完整模拟,可以用 C# 调用 CreateRestrictedToken,下面给出核心 P/Invoke 片段:

// 核心 Win32 调用示意(需嵌入完整 P/Invoke 声明)
// CreateRestrictedToken 参数:
//   baseToken        — 沙箱账户的进程令牌
//   disableSidCount  — 要禁用的 SID 数量
//   sidsToDisable    — 如 Administrators SID, Power Users SID
//   restrictSidCount — 要添加的仅限拒绝 SID
//   sidsToRestrict   — 如高权限组的 SID
//   privilegesToDelete — 如 SeDebugPrivilege, SeLoadDriverPrivilege

IntPtr restrictedToken;
bool ok = CreateRestrictedToken(
    baseToken,
    0,                    // flags
    disableSids.Length,
    disableSids,
    restrictSids.Length,
    restrictSids,
    privsToDelete.Length,
    privsToDelete,
    out restrictedToken
);

// 然后用 restrictedToken 启动 Agent 进程
CreateProcessAsUser(restrictedToken, ...);

设计权衡与落地建议

Codex 的方案不是银弹,它有几个明确的取舍:

1. Windows 专属,不可移植

SID、ACL、受限令牌全是 Windows NT 安全模型的产物。同样的思路在 Linux 上要换成 UID/GID + seccomp + AppArmor + namespaces。核心原则(专用身份 + 声明式权限 + 进程级剪裁)可以迁移,但具体 API 不能。

2. ACL 配置是运维负担

每个项目目录都要手动或脚本化地配置 ACL。如果项目多了,需要一个 ACL 管理工具来批量操作,否则迟早会漏配。建议写一个项目初始化脚本,在 git clone 之后自动跑 ACL 设置。

3. 受限令牌的调试成本高

进程拿受限令牌运行后,很多"明明 ACL 允许但还是报 Access Denied"的情况,根源是被禁用的 SID 或被删的特权在权限评估时优先级更高。排查时要用 whoami /all 对比令牌内容,或者用 AccessChk 工具逐项检查。

4. 网络隔离没覆盖

文章聚焦在本地文件和进程权限。Agent 如果能自由联网,ACL 再严也挡不住它把文件内容 POST 到外部服务器。生产部署需要加网络层隔离(Windows Firewall 规则或网络命名空间)。

落地检查清单

如果你要在自己的 Agent 系统里复用这套思路,逐项确认:

  • [ ] Agent 运行在专用低权限账户下,不在 Administrators / root 组
  • [ ] Agent 进程令牌通过 CreateRestrictedToken(或 Linux 对等机制)剪裁了高危特权
  • [ ] 项目目录 ACL 给 Agent SID 开了必要权限,敏感目录(密钥、凭据、其他项目)显式 Deny
  • [ ] Agent 进程的令牌里禁用了高权限组 SID(Deny-only)
  • [ ] 网络出站有防火墙规则限制(只允许必要的域名/IP)
  • [ ] 有自动化脚本在项目创建时配置 ACL,避免手工遗漏
  • [ ] 有日志记录 Agent 进程的令牌内容和权限检查失败事件

Codex 的做法证明了一件事:为 AI Agent 做安全隔离,不需要发明新范式,把操作系统已有的安全原语正确组合就够了。难点不在原理,在于把每个细节都配对、配全、配到不漏。


相关推荐