Layer 4 通过 CloudTrail 查询 UA 标签追踪 Agent 实际调用,配合 GuardDuty 检测异常会话,闭环整个审计链路。
层 1–3 缩小爆炸半径,但无法消除意外。层 4 闭合回路:让 Agent 的行为成为你可观测的东西,而不是靠猜。
CloudTrail 到 Athena / 日志池——通过第 2 部分的 UA 标签可查询。这就是「Agent 实际触碰了什么」的回答。
让 GuardDuty 先扛起凭证复用这一类检测——它的专用凭证泄露检测 findings 专门针对 EC2 实例配置文件凭证,而且它的异常检测 findings 仍可能捕获被盗笔记本会话从新地点重放。不要去重写它覆盖的功能;只需要知道笔记本会话这一 case 走的是噪音更大的异常检测,这就是层 1 短会话窗口为什么重要的又一个原因。
优先对专用角色和源身份做异常检测,UA 标签次之——查询角色 ARN 加上 CloudTrail 的 userIdentity.sessionContext.sourceIdentity;对拒绝操作、工作时间外的突发调用、新 Region、或带 Agent 角色发起但无预期 App ID 的调用进行告警。委托人和源身份是持久归因——调用方声明的,但在 assume 时固定并贯穿角色链;标签是辅助证据。期待缺失标签告警会比较多,直到你确认车队中哪些客户端实际参与了第 2 部分。
对 SCP 拒绝告警——效果出乎意料的好。大多数服务报告 AccessDenied,而 Amazon EC2 通常使用 Client.UnauthorizedOperation;错误信息也要匹配。同时覆盖显式拒绝消息和允许列表场景(即没有 SCP 允许该操作的情况)。Agent 触发其中之一,要么是在探索,要么是在试探边界。两者都值得一看。
运行 CloudFormation 漂移检测——SCP 和 UA 标签都检测不到配置漂移。漂移检测能发现与模板的不匹配;CloudTrail 和 UA 标签可以帮助归因导致漂移的 API 调用。

从 Athena 表(基于 CloudTrail bucket)开始跑两个查询:
-- 以 Agent 身份发出的调用(会话已存在)
SELECT eventtime,
eventsource,
eventname,
useridentity.sessioncontext.sourceidentity
AS source_identity,
useragent
FROM cloudtrail_logs
WHERE useridentity.sessioncontext.sessionissuer.arn
LIKE '%:role/Agent%'
AND eventtime >= '2026-09-01T00:00:00Z'
ORDER BY eventtime DESC
LIMIT 100;
-- 对 Agent 角色的 Assume(引导 / 层 1 缺口)
SELECT eventtime,
useridentity.arn AS caller_arn,
json_extract_scalar(requestparameters, '$.roleArn')
AS assumed_role_arn,
json_extract_scalar(requestparameters, '$.sourceIdentity')
AS source_identity,
useragent
FROM cloudtrail_logs
WHERE eventname = 'AssumeRole'
AND json_extract_scalar(requestparameters, '$.roleArn')
LIKE '%:role/Agent%'
AND eventtime >= '2026-09-01T00:00:00Z'
ORDER BY eventtime DESC;
以下是该查询在实际账户上的输出样本,裁剪到了你关心的字段(四行数据都是 2026-09-18;event source 就是事件名加上 .amazonaws.com——s3、iam、dynamodb、lambda):
eventtime | eventname | source_identity | useragent
02:11:41Z | GetObject | claude-code-1 | app/ai-agent_eng
02:11:42Z | ListRoles | claude-code-1 | app/ai-agent_eng
03:58:17Z | DescribeTable | (empty) | app/ai-agent_eng
09:15:33Z | InvokeFunction | claude-code-1 | app/ai-agent_eng …
| | | CloudShell
该样本包含 GetObject 和 InvokeFunction,这些是 CloudTrail 数据事件。默认的管理事件 trail 不会显示它们。在你真正关心的 S3 bucket 和 Lambda 函数上启用数据事件(Bedrock agent / 异步 API 也是数据事件——普通的 Converse / InvokeModel 已经作为管理事件到达),否则「它触碰了什么」的回答只有控制平面。
从中可以读出几点:
source_identity 有值 + 标签存在(第 1–2 行):预期的 Agent 路径——经第 1 部分加固的 assume,经第 2 部分加了标签。正常。
source_identity 为空但有标签(第 3 行):调用带了 App ID 但没有 source identity——assume 路径没有走第 1 部分的加固桥。这是自身层 1 的缺口,值得跟进。
标签存在但调用客户端是 CloudShell,不是 Agent(第 4 行):App ID 带进来了,但实际调用方是别的东西——检查你的 MDM 配置是否泄露到了非 Agent 工具,或者 Agent 在包装另一个工具链。User-Agent 显示 App ID 前缀加上真实客户端。
对于引导故事,跑第二个查询并读取该事件上的 sourceIdentity / userAgent。经加固的 assume 成功时,source identity 会出现在 requestParameters.sourceIdentity 中;那里为空意味着调用方没有设置,这是层 1 的缺口。
在这个查询可信之前有两个坑,且两个都在我实际环境中踩过。
第一,Schema。标准的 Athena cloudtrail_logs DDL 没有在 session-context 结构体内声明 source-identity 字段——先在那里加上 sourceidentity:STRING,否则该列无法解析。对于 assumed-role 调用,事件上的委托人 ARN 是会话 ARN,不是角色:你追查的角色在 session issuer 字段的下一层,以上 WHERE 子句匹配的就是它。调整表名和时间范围到你的环境——分区表需要 region/year/month/day 过滤器。
第二,模式。LIKE '%:role/Agent%' 是贪婪的——它会匹配名字中带 "Agent" 的每个角色,所以在多角色车队上你会拉进不打算归因的行。把模式收窄到你实际的角色名,或者为 Agent 角色保留专用前缀,否则你会追着幽灵行跑。
然后读缺口。source identity 为空的行不是走第 1 部分加固 assume 路径的,缺失 App ID 的行来自不参与第 2 部分的客户端。这些缺口通常是故事所在。
接下来:把四层整合在一起——模型、推出顺序,以及仍然需要你自己负责的部分。
Amazon Athena — Create a table for CloudTrail logs (stock DDL, partitions)
AWS CloudTrail — userIdentity element
AWS CloudTrail — Enrich CloudTrail events with IAM global condition keys (CloudTrail Lake event data stores only)
AWS CloudFormation — Detect drift on an entire stack
Amazon EC2 — Error codes (Client.UnauthorizedOperation)