最小权限是安全领域最古老、最可靠的理念之一。给一个身份恰好它所需要的访问权限,不多不少,那么任何一次失陷的影响范围都会被控制得很小。对于人类账户和长期存在的服务账户而言,这套模型运作得相当不错:角色会被审查,授权会被限定范围,访问认证会按季度周期进行。
AI 代理悄然打破了这套模型。一个代理被授予一份凭据、一个工具、一条 MCP 服务器连接,从那一刻起,它的行为就是动态的。它在运行时自行决定读取哪些资源、写入哪些资源、接下来调用哪个工具。您在第一天写下的授权描述的是一个上限,而非代理实际所做之事。而且由于代理高效能产,这个上限往往设得很宽松:宽泛的数据库角色、“以防万一”的写入权限、一个被半打工作流共享的服务账户。其结果就是这样一个环境——被允许的访问与实际使用的访问每天都在悄然漂移、渐行渐远。
独立的行业研究揭示了这一盲区的规模:约 82% 的组织报告称存在自己并不知情的、正在运行的 AI 代理,而只有约 21% 的组织维护着对这些代理的实时清单(CSA/Token Security,n=418)。您无法对一个看不见的环境强制执行最小权限。
定义最小权限漂移
让我把这件事精确地命名出来。最小权限漂移,就是代理“被允许的访问”与“被观测到使用的访问”之间不断扩大的差距。 它有两个失效方向,而其中只有一个是显而易见的。
显而易见的方向是权限未充分使用:一个代理持有对某张表的写入权限,却从未向其写入过任何内容。这是死权限,是您承担了却毫无收益的风险。而危险的方向,是它所暗示的反向信号:当观测集合中出现了某个授予集合从未刻意授予的内容时,就意味着有一个无人审查过的操作正在发生。一个导出任务突然向一个它一向只读取的存储桶写入数据;一个被策略限定在某一个 schema 内的代理却横跨到了另一个 schema。这些都不是什么稀奇之事;它们正是用粗粒度授权拼接起来的代理们的日常面貌。
之所以这件事很难,之所以它是代理特有而非人类特有的问题,是因为这种行为是被生成的,而非被配置的。一个权限过高的人类,大多数情况下并不会去行使那些权限。而一个权限过高的代理,则会行使任何有助于它完成眼前任务的能力,包括无人预料到的路径。静态的策略审查跟不上每次运行都在变化的行为。
把漂移转化为可审查的信号
漂移只有在不可见时才是危险的。要做的工作就是把它变成一个人类可以审查的信号,而这需要两件事协同运作:对代理实际触及内容的持续观测,以及对它们曾被允许触及内容的稳定记录。
观测必须是读取优先的。一个从日志、OpenTelemetry 追踪和 eBPF 内核信号进行观测的采集器,处在代理数据路径之外。它不是代理,它不拦截调用,而且即便它发生故障,也是在安全意义上“失效即放行”——代理照常工作,您失去的是可见性而非可用性。这种不对称性至关重要:一个会拖垮生产环境的安全控件,正是会被团队悄悄禁用的那种控件。其中 eBPF 这一层尤其充当了内核级的事实基准,是代理无法绕开的部分,这也正是为什么诸如 MCP 工具注解(readOnlyHint、destructiveHint)这类协议层提示要拿来与它互证,而非直接信任。MCP 规范本身就声明这些注解是不可信的;内核信号才是让互证成真的东西。
观测产出的,是一张访问地图:对于每个代理,它触及了哪些资源,以及它是读取(R)还是读/写(RW)。这张地图存储的是访问关系,而非载荷、密钥或 PII。有意思的部分在于差异比对:
| 代理 | 资源 | 已授予 | 已观测 | 漂移 |
|---|---|---|---|---|
| data-export-job | prod-postgres | R | R | 无 |
| data-export-job | s3://billing-exports | R | RW | 未经审查的写入 |
| report-builder | analytics-db | R | (未使用) | 死权限 |
真正要紧的是中间那一行。策略授予的是对导出存储桶的读取权限;采集器却观测到了一次写入。这一行就是最小权限漂移本应揭示的首要风险:一项无人审查过却被行使的权限,而且它被归因到一个具体的代理,而非一个共享的服务账户——因为正是按代理粒度的身份,才让这种归因和审计在根本上成为可能。
在访问时强制执行,而不只是记录
检测告诉您漂移发生了。要把闭环合上,您希望那次未经审查的写入成为一条被拒绝的路径,而不是一条被记录的路径。这正是在访问时评估的 policy-as-code 发挥作用之处。同一份标记出漂移的差异比对,会变成阻止漂移的规则。
不妨考虑把导出任务在生产数据库上钉定为只读,并彻底拒绝写入,同时让违规行为阻断并告警,而不是悄无声息地放行:
agent "data-export-job" {
# Read-only on the operational database. No writes, ever.
access "prod-postgres" {
mode = "read"
deny = ["write", "delete", "ddl"]
}
# The export target the job is *supposed* to use.
access "s3://billing-exports" {
mode = "read"
}
on_violation {
action = "block" # deny the call at access time
alert = "security-oncall"
audit = "append" # write to the tamper-evident ledger
}
}
让我们具体地走一遍前后对比。
之前。 导出任务持有一个宽泛的角色,向 s3://billing-exports 发起一次写入。没有任何东西能阻止它。操作成功了,融入了正常流量之中,然后在数天之后(如果还能被注意到的话)才作为访问地图中的一个异常浮现出来。被允许与被观测之间的差距扩大了,而唯一留下的痕迹是一行无人阅读的日志。
之后。 同样的写入到来。策略在访问时被评估,发现这是对一个被限定为 read 的资源发起的 write,于是在操作落地之前返回一次拒绝。违规行为阻断了该调用,向值班轮值人员发出告警,并向那本仅追加、哈希链式的审计账本追加一条记录。这次未经审查的写入,永远不会变成一次未经审查的变更。漂移在当下就被即时转化,重新回到一条最小权限的路径上。
有两个属性让这一切保持诚实。其一,对访问地图的每一次特权查看本身都会被审计——谁查看了什么——因为这张地图很敏感,而一个无法对自己的操作者负责的安全工具是不可信的。其二,置信度被明白地展示出来:一项由内核级证据归因到某代理的操作,会与一项被近似推断出来的操作以不同方式标记。您永远不会被要求基于一个被捏造的确定性去采取行动。
要点总结
最小权限并没有在 AI 代理上失灵;失灵的是审查周期。授权是粗粒度的,行为是动态的,而按季度进行的认证无法追踪一个每次运行都在变化的访问面。解决之道不是在关键路径上塞进一个更重的代理。它是持续的、读取优先的观测——产出一份“授予 vs 观测”的差异比对,再加上 policy-as-code 在访问时强制执行那条被修正过的边界,从而让一个权限过高的代理早在酿成事故之前,就作为一个可审查的信号被捕获。
如果您想了解采集器、访问地图与访问时强制执行是如何在不横亘于您代理数据路径的前提下协同运作的,架构页面会逐步讲解这套设计,而产品页面则展示了“授予 vs 观测”视图在一个真实环境上的样子。