作者通过审计日志实测 AI 代理以用户身份认证后实际持有的令牌权限范围,发现认证机制远比表面看起来复杂,代理可能继承超出预期的访问能力。
我测量了一个 AI Agent 在模拟我身份认证时实际持有什么。认证原来是一枚 Token,其 scope 字符串赫然写着 user_impersonation。授权几乎全部来自标准权限检查无法看到的组成员资格。
两个生产环境在一小时内到达的权限范围相差两个数量级,审计覆盖范围与凭证能力成反比。Agent 能够从安全数据库删除数据的那个环境,恰恰是唯一没有审计跟踪的环境。
当我测量我们自己运行的无人值守 Agent 所使用的机器身份时——构建正确、按资源划分作用域——它在与我相同的地方出现了问题。
"能够从安全数据库删除数据的那个环境,恰恰是唯一没有审计跟踪的环境。"
我给一个编码 Agent 提供了我自己的 Azure 凭证,这是标准做法,然后调查了它能访问到什么。答案:在一个生产环境上有 2 项权限,在另一个上有 107 项。那个能够从安全数据库删除数据的环境,审计恰好是关闭的。这一切都不是配置错误。当唯一持有该凭证的是一个真人时,这一切都是合理的。
我们工作仓库中的 11 个 Agent 技能连接数据库。它们都使用相同的方式认证:
sqlcmd -S <server> -d <db> --authentication-method ActiveDirectoryAzCli -C -Q "<query>"
这个 flag 告诉 sqlcmd 向 Azure CLI 请求其缓存的 Token 并呈现它。没有连接字符串,没有存储密码,也没有属于 Agent 自身的凭证。当 Agent 运行查询时,数据库看到的是我。
工程师们将这描述为 Agent"还没有身份"。它其实有。它用的是我的。
"工程师们将这描述为 Agent'还没有身份'。它其实有。它用的是我的。"
使用借来的凭证是一种已知的不良实践。它会导致三种常见结果:归属崩溃、Agent 继承人类积累的权限、以及撤销 Agent 同时撤销人类。我希望将这些预测与一个实际运行的系统进行对照。结果表明,这三种结果中有两种比表面看起来要奇怪得多。
首先,我检查了数据库对传入连接的记录:
SELECT login_name, program_name, host_name, client_interface_name
FROM sys.dm_exec_sessions
WHERE session_id = @@SPID;
login_name: naseeb.ahmed@<redacted>
program_name: sqlcmd
host_name: <my workstation>
client_interface_name: go-mssqldb
这些身份字段反映的是我的登录、我的机器和工具。没有一个字段会因为是我输入的查询还是 Agent 决定运行的查询而改变。因为不存在第四个字段供 Agent 声明自己,Agent 操作和人类操作在设计上是一致的。
下游的每一个防护栏都依赖于这三个相同的值。没有任何下游控制可以对 Agent 有条件:不是速率限制,不是审批步骤,也不是对机器发起语句使用不同保留策略。
"记录对凭证是准确的,但对意图保持沉默。"
这也造成了一个反向问题。如果有人在六个月后质疑我输入的一个查询,我无法证明那是我自己的,而不是某个自动化工具执行的。记录对凭证是准确的,但对意图保持沉默。
我们的认证方法很简单:没有人真正设计过它。开发者用目录账户和第二因素登录一次,CLI 缓存结果,仓库中的每个技能拾取它,Agent 认证只是搭乘人类当天早些时候完成的事情。这避免了存储密码,也不需要在确定 Agent 有用之前先配置一个身份。
解码 CLI 为数据库连接提供的访问 Token,揭示了以下声明:
aud: https://database.windows.net/
appid: <Azure CLI public client ID>
appidacr: 0
scp: user_impersonation
exp - iat: 84 minutes
groups: 37 entries
amr: ['pwd', 'mfa']
其中两个声明很突出:
Scope 是 user_impersonation。RFC 8693 将模拟定义为一个主体接收另一个主体的所有权利,同时保持与其不可区分。我的会话记录符合这一定义。
amr(认证方法)声明带有 mfa。Agent 执行的每个操作都附带一个人类满足了第二因素的证明,即使该因素是在数小时前为不相关的任务完成的。
84 分钟的 lifetime 看起来像是一个安全边界,但它不是。CLI 使用刷新 Token 自动刷新,所以无人值守运行不会在 84 分钟后停止。它只在刷新 Token 过期或有人停用用户账户时停止。
在租户中列出每个订阅的角色分配,只返回一行:非生产订阅上的 Reader。
然而,组成员资格讲述了不同的故事:
与访问相关的组镜像了组织架构:
<environment> - SQL Database Viewer<environment> - SQL Elevated<environment> - Resource Contributor<environment> - SQL Database Manager<environment> - Resource Viewer<environment> - Readers组携带了所有实际授予数据访问权限的内容,并位于数据平面,控制平面查询无法看到它们。安全审查员通过标准角色分配检查 Agent 的权限,只能得到一行和一个虚假的小爆炸半径感觉。
Token 的 groups 声明显示 37 个条目,而目录的直接成员查询显示 34 个。差异来自嵌套组。两个权威的组成员资格答案不一致,数据库使用的是较大的数字。
其中一些成员资格是即时管理(JIT)的。查询哪些是临时的会导致错误:
Forbidden: PermissionScopeNotGranted
PrivilegedEligibilitySchedule.Read.AzureADGroup, PrivilegedAccess.Read.AzureADGroup
能够从安全数据库删除行的凭证无法读取其权限是常设的还是临时的。根据供应商关于 JIT 组激活的文档,已经缓存了成员资格的应用程序可能在停用后继续遵守它。窗口约束目录记录,但不会触及已发出的 Token。
使用 fn_my_permissions 检查连接主体在生产数据库服务器上实际拥有的权限,结果显示:
可见数据库:1,382 个(692 个业务层,687 个安全层)
业务层权限:CONNECT, SELECT
安全层权限:mssql: login error: Login failed for user '<token-identified principal>'
Token 持有两个权限,安全层直接拒绝连接。读取访问在必要的地方得到执行,没有通往敏感层的路径。Agent 在这里继承了一个紧凑、正确的范围。
然而,使用相同 Token 查询我们的 lakehouse 生产分析端点,揭示了:
权限:107 个(跨越读取、写入、模式定义和数据库管理)
一个身份在一小时内访问了两个生产环境,在一个上有 2 项权限,在另一个上有 107 项。如此宽的授权不再是工作描述;而是一个分配给偶尔查看分析的人类完整角色,现在交给了一个无人值守运行数千条语句的工具。
供应商文档指出,分析端点对底层表是只读的,修改通过不同的引擎传递。在该端点上配置的安全规则通过它来管理访问,但不跟随数据到另一个引擎访问时。有效访问取决于路径,而不是主体。
"代码强制执行一个限制,另一个只是文件中的一个句子。Agent 读取两者,不验证任何一个,并基于与其实际权限相矛盾的指令集操作。"
此外,Agent 在访问该端点之前读取的指令文件声明表面是只读的。Agent 呈现的凭证授予 107 项权限。代码强制执行一个限制,另一个只是文件中的一个句子。Agent 读取两者,不验证任何一个,并基于与其实际权限相矛盾的指令集操作。
在非生产环境中,范围完全开放:
业务层:CONNECT, SELECT, INSERT, UPDATE, DELETE
安全层:CONNECT, SELECT, INSERT, UPDATE, DELETE
写入和删除权限适用于两个层。在大多数软件系统中,非生产环境允许团队成员按设计破坏东西。
检查数据库级审计配置返回六个已启用的规范:
SELECT name, is_state_enabled FROM sys.database_audit_specifications;
虽然数据库报告审计已启用,但查询监控接收器返回的是空结果而不是权限错误。没有任何东西写入日志。
审计配置位于服务器资源上,而不是数据库内部。查询管理 API 揭示一个环境在服务器级别启用了审计,并附加了语句级操作组:
第二个环境在服务器级别完全禁用了审计,没有附加目标。六个数据库规范对象是过去环境副本的残留物。
内部数据库检查对零活动的环境报告"已启用"。
审计覆盖范围与借来的凭证能力成反比。生产环境有审计,而非生产环境为了节省存储和处理成本而未被监控。
这个逻辑在 Agent 替代人类用户时失效。Agent 在不通知任一系统的情况下消除了两个配置背后的核心假设。
此外,目录认证下的失败登录不会到达 SQL 审计日志,因为凭证在接触数据库之前就已验证。保护安全层的拒绝在日志中保持不可见。审计在重负载下也是尽力而为的,语句在超过 4,000 个字符后被截断。
我们的仓库还包含一个运行在 15 分钟定时触发器上的无人值守 Agent。它的身份对每个应用组使用用户分配的托管标识,具有明确命名的角色授权:
这个结构代表了一个正确配置的机器身份。然而,在连接数据库时,没有使用托管身份。相反,它通过在部署时设置的標準 SQL 登录和密码进行认证。
数据库无法区分不同的调用者。机器身份在数据路径开始处停止。在数据库处,自动化的 Agent 和人类用户一样无法归属。
两种模型在同一个地方失败。给每个 Agent 自己的服务账号会改变日志中的名称,但不解决归属问题。
收紧权限和在非生产环境中开启审计限制了潜在损害,但没有修复根本原因的归属问题。系统仍然为自动化操作记录人类凭证。
解决这些问题需要五个核心更改:
在凭证级别分离 actor 和 subject。 RFC 8693 区分了模拟 Token(仅携带 subject)和委托 Token(同时携带 subject 和 acting party)。Agent 凭证应派生自人类凭证并携带两个身份,以便会话日志可以同时记录两者。
将有效权限强制为交集。 Agent 只应减少访问权限,永远不应扩大。
按操作而非按主体划分作用域。 权限应适用于特定操作,而不是授予广泛的、身份范围内的角色。
将审计覆盖与权限级别绑定,而不是环境。 如果凭证在数据库上持有写入或删除访问权限,语句日志记录必须跟随凭证,而不论环境如何。
提供独立的撤销机制。 切断 Agent 不应需要停用主要的人类用户账户。
模型上下文协议(MCP)授权规范明确禁止服务器转发客户端 Token,以确保下游服务可以验证调用者身份。命令行工具需要采用相同的标准。
这些发现代表了在两天内跨单个系统进行的测量: