跳转到主要内容

没有任务的身份:为什么按代理发放的凭证仍然过度授权

任务范围的凭证解决的是令牌生命周期,而不是令牌范围。范围取决于规划器如何分解工作,这使得分解本身成为一种安全控制。

服务器机架的近景,带有跳线和状态 LED,代表代理系统中的分层访问控制

改进之处,以及它止步的地方

给 AI 代理一个随任务结束而失效的凭证,你就消除了访问管理中最古老的问题之一:长期有效的持有者令牌躺在配置文件里,数月内一直有效,在代理进程早已消失之后仍对攻击者有用。1Password 式的框架以及更广泛的面向代理的 IAM 提案都朝这个方向推进。凭证按任务铸造,绑定到会话,并在任务关闭时撤销。泄露令牌可被利用的窗口从数月缩短到数分钟。这是实实在在的收益。

但这一收益与大多数安全审查真正关心的问题正交:这个凭证在存活期间能触及什么?一个生命周期五分钟、却对 * 拥有 s3:GetObject 的任务范围令牌,仍然是一个通向整个存储桶的五分钟窗口。生命周期是窄的。范围不是。而范围并不是凭证签发系统自己决定的——它继承自更早的一个决策,由另一个组件、用另一套词汇做出。

范围是分解的产物

考虑编排器可能交给子代理的两条指令:

subtask: reconcile the account
subtask: read invoice 4417

第一条是合理且常见的请求。从访问控制的角度看,它也是对账单台账、支付提供商、客户记录以及任何对账逻辑所触及内容的读取权限请求。第二条是对单个文档的请求。两者都是“任务”。作为对意图的描述,两者都不比对方更正确。但它们产生的凭证广度天差地别,而签发系统——那个铸造短期令牌的系统——无从得知第一条是懒惰的分解,第二条是谨慎的分解。它看到的是一个任务标识符和一个请求的范围,如果范围与任务匹配,它就授予。

换句话说,规划器无论是否有人这样看待它,都在做访问控制。当它把“处理客户投诉”拆成“读取工单、读取订单、发起退款”时,它刚刚决定了集群需要哪些凭证。当它把同一个请求拆成“读取工单、读取订单、读取支付历史、读取物流记录、读取账户备注”时,它决定了一个更宽的范围。下游的 IAM 层只是形式;实质性的决策发生在上游,发生在一个大多数团队仍称之为编排的组件里。

High voltage power line and insulators against a clear blue sky

这正是按任务发放凭证的模型没有解决的部分。它修复了生命周期问题,因为生命周期是签发者控制的属性。它没有修复范围问题,因为范围是分解的属性,而分解是由语言模型对用户请求进行推理产生的,不是由策略引擎对最小权限进行推理产生的。

可审计性要求什么

如果范围是分解的下游,那么审计访问就意味着审计分解。一份记录 agent-7 requested scope X, granted scope X 的令牌签发日志,对审查者来说无法说明 X 是否合理。它记录了形式上的步骤,却省略了实质性的步骤。

由此得出的设计规则并不复杂,尽管它比听起来更具侵入性。每个凭证请求都应携带两个标识符:它所属的子任务,以及产生该子任务的父计划节点。如果请求的范围超出所声明子任务可能需要的范围,授予就应被拒绝——这里的“可能”指的是分解必须满足的策略,而不是签发者临时做出的判断。授予一旦发出,就应与促成它的分解一起记录,以便审查者重建推理过程:这个令牌存在,是因为计划节点 P 派生了子任务 S,而 S 的范围限定为发票 4417,且该令牌可以读取发票 4417。

{
  "plan_node": "P-118",
  "subtask": "S-118-3",
  "intent": "read invoice 4417",
  "requested_scope": ["billing:invoices:read:4417"],
  "granted_scope": ["billing:invoices:read:4417"],
  "parent_scope_ceiling": ["billing:invoices:read:*"]
}

parent_scope_ceiling 字段是最重要的。它是计划节点允许分配给其子节点的最大范围,并且在计划创建时设定,而不是在请求凭证时设定。这给审查者两件要检查的事:子任务是否留在了父节点的上限之内,以及父节点的上限本身是否由原始请求所证明合理。第二项检查才能抓住那种悄悄认定“对账”意味着“读取一切”的规划器。

重试循环才是范围蔓延的地方

要警惕的失败模式不是恶意代理,而是乐于助人的代理。代理遇到权限错误——它试图读取发票 4417,范围是 billing:invoices:read:4417,但发票位于另一个系统,或者对账还需要支付记录——于是它重试。如果重试逻辑被允许扩大范围![Close-up of rClose-up of rusty screws on a corroded metal surface showing decay

重试时的范围蔓延应是一等事件信号,而不是一行日志。具体来说:请求的范围严格大于原始子任务范围的重试应失败关闭,发出事件,并要求由不是撞墙的那个代理的组件重新分解计划。重新分解才是修复。扩大的授予才是失败。如果系统无法区分两者,它最终会授予扩大的范围,因为那是对处于截止期限压力下的代理阻力最小的路径。

诚实的权衡

这一切都不是免费的。要求每个凭证请求携带子任务标识符和计划节点上限,意味着规划器必须输出结构化的分解,而不是自由文本的任务描述。这意味着编排层必须维护一个带范围注解的计划图,而该图必须被版本化并可审计。这意味着一些合理的任务会失败,因为分解太粗,代理无法在其范围内自我纠正——而对这种失败的正确回应是更慢、人类可见的重新规划,而不是更宽的令牌。

这就是权衡:更窄的范围,代价是分解出错时更多的摩擦。另一种选择——按任务发放凭证、采用任务级范围,其中“任务”是规划器碰巧写下的任何东西——给出的是最小权限的表象,以及规划器随心所欲的实质。任务范围的凭证是一种改进。它们不是范围问题的解决方案,因为范围问题不是凭证问题。它是分解问题,而且它位于上一层。

该构建什么

  • 把规划器当作安全相关组件。它的输出就是访问控制决策。
  • 要求每个凭证请求都带有子任务和父计划标识符。
  • 强制执行在计划创建时而非凭证签发时设定的父范围上限。
  • 将分解与授予一起记录,使理由可被重建。
  • 对以更宽范围重试的情况失败关闭,并将失败路由到重新分解,而不是更宽的授予。

凭证的窄度只能与产生它的任务一样。如果任务以错误的粒度陈述,凭证也会如此。

封面照片:panumas nikhomkhai / Pexels。

分享XLinkedIn永久链接6 分钟阅读

相关阅读

这能回答的问题

Does task-scoped credential issuance solve least privilege for agents?

It solves the lifetime half. A short-lived token limits how long a leak is useful, but the token can still reach whatever the task was scoped to, and that scope is set by the planner's decomposition, not by the issuer.

Why is the planner a security-relevant component?

Because the planner decides how work is split, and the split determines what credentials each subagent needs. A coarse decomposition produces coarse grants regardless of how short the token lifetime is.

What should a credential request carry?

The originating subtask identifier and its parent plan node, plus a scope ceiling set when the plan was created. Grants that exceed the declared subtask should be rejected, and the decomposition should be logged with the grant.

How should retry-with-broader-scope be handled?

As a first-class incident signal. A retry requesting a scope larger than the original subtask should fail closed and route to re-decomposition by a component other than the agent that hit the permission error.