凭据如何投递进沙箱

13 分钟读完

S13
深入解析 · 智能体安全

智能体从没逃出沙箱。它也不需要逃。

你们团队做过的每一个沙箱决策,回答的都是同一个问题——智能体生成的代码能不能跑出这个箱子?——而没有一个回答了真正决定你爆炸半径的那个问题:在智能体启动之前,平台往箱子里放了什么?一份被投递进沙箱的凭据,任何能发起 HTTP 请求或读取文件的工具都读得到,也就是说,每个智能体都有的那两个工具都读得到。隔离与权限是两条正交的性质,加固其中一条对另一条毫无作用;而那些托管运行时卖给你的是第一条,同时让第二条默认落在它们能蒙过去的最宽作用域上。

STEP 1

两个问题,而设计阶段只有一个会被提出来。

逃逸那个问题已经被充分理解,也答得相当不错。对智能体生成的代码来说,共享内核的容器已不再站得住脚;层级是 microVM、用户态拦截与仅远程执行;而你按一次突破会造成多大损害来选——这些沙箱与隔离模式都已认真梳理过。团队会刻意做这个决定、把它写下来,而且通常说得出自己在哪一层。

持有那个问题几乎从不出现在同一场会议上:假定智能体一直待在箱子里,那它在里头已经能做什么?而这个答案是由别人挑的一份凭据定下的——运行时厂商、平台团队,或者一个从快速上手指南里抄来的 Terraform 模块——并且被自动投递进沙箱,只为了让智能体的 SDK 调用不必配置就能跑通。

这里有一条能把两者分开的诊断。做一次反事实推演:假设你的隔离是完美的,内核摸不到,突破不可能。现在问,一次成功的提示注入能达成什么。如果答案仍然是「读到这个账号里的每一段会话」,那沙箱从来就不是你真正倚靠的那道控制,加固它什么也买不来。这不是假设。这正是 Zenity Labs 于 2026 年 10 月 8 日针对 Amazon Bedrock AgentCore 披露的 AgentCorruption 那条链的形状:Firecracker microVM 的边界全程守住了,而整场攻陷跑的全是运行时放进箱子里的那些凭据。

这份混淆有一部分出在用词上。「已沙箱化」既被用来指被圈住,也被用来指无特权,而厂商很乐意让你在他们只建成了第一样时听成第二样。microVM 是对「圈住」的一个极好的答案,对「特权」则什么也没说。当一份产品说明写着「每个会话跑在自己独立的 microVM 里」,正确的追问不是关于虚拟机监控器的。它是:那个 microVM 持有什么身份?

STEP 2

四条投递通道,按「读它需要什么原语」来排序。

凭据抵达一个工作负载的方式只有少数几种,而有用的排序不是每一种感觉上有多机密,而是读到它需要智能体具备哪种能力。这么一排,清单就塌缩了:有 shell 工具的智能体四条全读得到,只有 HTTP 取数工具的读得到第一条,而两样都没有的,仍然经由自己的 SDK 把四条的好处全享了。

  • 实例元数据服务。位于 169.254.169.254 的一个链路本地 HTTP 端点,会把工作负载的角色凭据交出来。IMDSv2 把门槛抬高了:你必须先向 /latest/api/token 发一个 PUT,带上必填的 X-aws-ec2-metadata-token-ttl-seconds 头——AWS 自己的示例用的是 21600,也就是六小时——并在每次读取时出示拿回来的令牌。这挫败了一次普通的服务端请求伪造,而那正是它被设计来挡的东西。它并不会给一个能发 PUT 的工具添任何麻烦;而随之配套的那个跳数限制控制,在常见的容器网络形态下不得不从最严的那档往上调,所以实际情况是:最需要它的那个平台,正是已经把它调松了的那个平台。
  • 环境变量。env 读得到,智能体派生的每一个子进程都会继承,还会被抄进崩溃转储、啰嗦的错误路径,以及任何会打印自身环境的诊断里。这条通道自身没有过期语义,所以落在这儿的,往往就是那些「长命到足以活过一次重启」的东西。
  • 挂载的令牌文件。Kubernetes 的投影式服务账号令牌是常见通道里最好的一条,因为它带着其余几条所缺的性质:一个把令牌绑定到唯一预期接收方的 audience,以及一个默认 3600 秒、且不得小于 600 秒的 expirationSeconds——kubelet 会在寿命的 80% 处、或令牌超过 24 小时时轮换,这也正是工作负载必须重新加载那个文件、而不能读一次就算的原因。这些都是实打实的改进。而它仍然是一个智能体 cat 得到的文件。
  • SDK 凭据链。最危险的一条通道,恰恰因为它不是一条智能体还得去找的通道。智能体生成的代码调用云 SDK,SDK 默认的提供方链把上面几条中存在的那一条解析出来,调用就成功了。你的链路记录里不会有任何一次凭据读取,因为本来就没有一次——有的只是一个客户端构造函数。

值得内化的那个后果是:没有哪份工具目录是你能靠精挑细选来消掉这件事的。取数工具就是一个元数据服务客户端。shell 工具就是一个文件系统读取器。代码执行则两者都是。这些并不是你本可以裁掉的附带能力——它们正是智能体存在的意义所在;这正是环境权限才是对的框架、而工具级白名单不是的原因。

STEP 3

授权打不了补丁,而那条修复时间线正好证明了这点。

让 AgentCorruption 成为一条链、而不是一件奇闻的,不是那次元数据读取。而是那次读取所返回之物的作用域。按 Zenity 的说法,默认执行角色让单个智能体的凭据得以发现其他智能体的标识、调用那些智能体、拉取它们的容器镜像、读取私密会话,并从 AWS Secrets Manager 取出密钥——范围覆盖同一个 AWS 账号与区域里的每一个 AgentCore 智能体。一个提示词、发给一个对公的智能体,就触达了其余全部。

现在来读日期,因为这个次序就是那堂课。Zenity 自 2025 年 12 月 25 日起上报。AWS 自 2026 年 2 月 14 日起,让新部署的智能体默认仅用 IMDSv2——传输层面的加固大约花了七周。权限则一直拖到九月底:2026 年 9 月 29 日,在发表前做最后一次核查时,Zenity 发现跨智能体调用、会话读取与 Secrets Manager 访问已从默认角色上移除,其他权限也被收窄。九个月,而且没有签发任何 CVE。

这份不对称是结构性的,不是疏忽。代码缺陷有补丁;一项权限没有。对一项过宽授权,唯一的修复就是把它撤回,而撤回对每一个「自家智能体悄悄依赖着它」的客户都是一次破坏性变更——这正是把七周变成九个月的那道评审周期。对你来说,有三件事随之而来。

  • 默认角色的作用域是一个在你到场之前就做好的产品决定,而且它变更时不带版本号。它该和其他「由别人代你掌控的设定」排在同一张清单上;未钉住的厂商默认值是这件事的通论,而一个执行角色是那张清单上后果最重的一条。
  • 没有 CVE 不等于没有暴露。授权不由漏洞标识来追踪,所以这一整类问题对你早已在跑的扫描与 SBOM 机器而言是不可见的。你流水线里不会有任何东西来告诉你:你运行时的默认角色太宽了。
  • 「账号加区域」才是真正的单位,而它不是你用来思考的那个单位。你按智能体来想;权限按账号来想。这道落差就是此处爆炸半径的全部,也正是智能体的多租户一再发现「人们买到的隔离圈错了对象」的原因。
STEP 4

短命是便宜的那一半。作用域才是要紧的那一半。

去问一个平台团队他们怎么保护智能体凭据,答案几乎总是关于寿命的:令牌是短命的、会轮换、什么都不留。全都没错,全都比做作用域容易得多,也全都瞄错了威胁模型。面向智能体的受限凭据把那三条性质放在一起论证;此处要补上的,是为什么偏偏是智能体把寿命这条论证给破了。

短 TTL 防的是一个离线的对手:某人偷走一份凭据、带到别处、过一阵再回来用。整套做法本就是为那个威胁建起来的,而时钟对它确实是一道真实的防御。一个被注入的智能体不是那个对手。它是一个在窗口之内持有凭据的在线进程,带着一个每秒能发出成百上千次调用的工具循环。TTL 本要防的那一步「外带」并不会发生,因为没有什么要外带的——攻击者的代码早就跑在凭据旁边了。

给它配上数字。一个十分钟的令牌,被一个 400 毫秒完成一次 API 调用的智能体使用,在它过期之前授权的动作数量量级在一千次顺序调用上下,并发则更多。对着这个,十分钟令牌与一小时令牌之间的差别是噪声。而「一个只能调一个端点的角色」与「一个能枚举整个账号的角色」之间的差别就是全部。这正是凭据寿命所围绕的那份不对称,而它把通常的加固次序翻了过来:对智能体而言,先做作用域、再做寿命——这恰恰与「哪个容易」相反。

一个你在会上就能跑的快速自检。拿你寿命最短的那份智能体凭据,问一句:它所能造成的损害里,有多大比例是在一个 TTL 之内就完成得了的。如果诚实的答案是「全部」,那你的轮换叙事就是一件合规产物、而不是一道控制,而真正的活儿在挂到那份凭据上的策略里,不在它上头的那只钟上。

STEP 5

把权限放到箱子外面,把身份留在里面。

结构性的修法是一次单一的倒置:沙箱该持有的是一份「这是哪一次运行」的证明,而不该持有任何构成「这次运行可以做什么」的证明。授权本体住在沙箱之外的一个中介里,它读取运行身份、施加策略,并铸出一份窄到「偷走也没意思」的凭据。智能体的能力毫无变化;变的是,把沙箱里的一切都读光,拿到的是一张提问的票,而不是一个答案。

  • 会贴凭据的出站代理。把工具流量路由经一个持有真密钥、并在出站途中把它们贴上去的代理。沙箱从不见密钥,这也同时意味着一份泄露的链路记录、一次崩溃转储、一条啰嗦的错误都不可能含有密钥。你大概本来就在为出站控制建这个代理了;注入凭据是它该兼的第二份差。
  • 给你确实留在里面的那样东西做受众绑定。运行身份应当只被那个中介接受、而不被其他任何东西接受,这正是投影式令牌上 audience 字段的用处,也是运行时鉴证所形式化的东西——见智能体身份与鉴证。一个只开一道门的令牌,是你付得起「被读到」这个代价的令牌。
  • 每个工具一个身份,不是每个运行时一个。AgentCorruption 那些权限之所以触达得到,是因为单一一个角色服务了账号里的每一个智能体。按工具、按目的地的身份让中介的策略根本上成为可表达的;而一个共享角色则按构造就把每条策略变成了谎言。
  • 让做决定的是策略,而不是凭据的形状。一旦中介进了通路,有意思的问题就挪到了「对这次运行、这个目的地、这些参数,它会授权什么」——这正是面向智能体的策略即代码所围绕的那个决策点,也是一次拒绝能被写得清楚明白的地方。
  • 在网络层把元数据端点挡掉。不是靠跳数限制——平台会需要把它调高——而是在沙箱的网络命名空间里对那个链路本地地址下一条显式拒绝。这是一行规则,而它直接把 STEP 2 里的第一条通道整条去掉。
  • 引用密钥,永不放置密钥。沙箱里的配置应当点出它需要什么、让中介去解析,依面向智能体的密钥管理所述。一个名字不是一份凭据,而这个区别就是全部的收益。

请诚实地给代价定价,因为它们都是真的。你给每次工具调用加了一跳网络,这对延迟敏感的循环是要紧的。你现在得把那个共享角色一直隐含表达着的策略写出来,而弄清它当初表达的到底是什么,就是这活儿的大半。而那个中介进了关键通路,这迫使你做一个决定——该刻意做,而不该在一次故障中才发现——失败拦截与失败放行就是那个问题,而对一个凭据中介,答案几乎总是拦截。

STEP 6

从箱子里面量它,然后报一个数。

这套论证在有人真去跑那个原语之前都停在抽象里,而跑它要二十分钟。在你自己的非生产环境里给一个智能体一个 shell 工具和一条指令:把从这里触达得到的每一份凭据读出来、并打印它的身份。你会拿回一个角色 ARN、一个服务账号,或者两者都有。那就是唯一要紧的那次测量的输入。

接着把那个身份实际能做什么枚举出来——用云自己的策略求值,而不是用文档,因为文档描述的是意图、求值器描述的是授权。把不同的动作数与不同的资源数点出来。一个数字,平实地说出来:这个沙箱里的一次注入提示,授权 N 项动作、横跨 M 个资源。这个题目里再没有别的句子能像它那样落到一位高管心里。

  • 跑两遍——对着平台默认值跑一遍,对着你自己的角色跑一遍。两者之间的落差会告诉你,你们团队到底有没有做过任何作用域收窄,而团队对这个答案常常是猜错的。
  • 每次运行时升级之后都重跑。授权会随厂商的挪动而挪动,两个方向都会,而且是无声的。把它挂到你用于模型或运行时版本变更的那同一道关卡上。
  • 专门去数跨智能体的触达。「这个智能体看得到另一个智能体的会话、记忆或镜像吗」是那个把一次攻陷变成全部攻陷的问题,而它正是没人问的那个问题——因为智能体感觉上是彼此分开的东西。
  • 对那次读取告警,既然你阻止不了它。一次来自沙箱的元数据取数或令牌文件读取,而其工作负载并没有任何正当理由去做这件事——这是一条信号强、量又小的检测,是这个面上少有的几条之一,值得接进检测智能体被攻陷里。
  • 把结果当作下一个智能体的设计输入,而不是一条待修的发现。这个数字是你投递通道的一条性质。你要是想要一个不同的数字,就去换通道,也就是 STEP 5——写一条更严的提示词是到不了那儿的。

这周做两件事。跑那个二十分钟的演练,并把「N 项动作、横跨 M 个资源」那句话写下来,因为其余一切都得靠它才拿得到预算。然后在你的智能体沙箱上,给 169.254.169.254 加上网络层的拒绝——这是一条规则,如果你的工作负载已经用的是文件投递或中介投递的身份,它不花你任何代价;而它关掉的,正是一个既没有 shell、也没有文件系统访问的智能体仍然触达得到的那条通道。会贴凭据的代理与按工具的身份才是对的终点,但它们是一个季度的活儿,而把演练结果摆上桌,要为它们拿到预算容易得多。