某个平台团队向十几位工程师推出了 Claude Code。为了让它真正可用,他们把它接入了几台 MCP 服务器:一台读取 monorepo,一台查询报表只读副本,一台负责创建工单。不到一周,某个代理就已经在读取源码、操作数据库、调用内部工具——而没有人能回答审计人员迟早会提出的那个问题:到底是哪个代理,对哪个资源,做了什么,并且您能否证明这条记录事后没有被篡改?
这并非假想中的缺口。独立的行业研究(CSA/Token,n=418)发现,约 82% 的组织存在自己并不知情的 AI 代理在运行,而仅有约 21% 的组织对它们保有实时清单。Claude Code 与 MCP 正是那种落地极快、迅速跑在审计能力之前的能力。好消息是:一条经得起辩护的追踪链完全可以在您自有的边界之内实现,没有任何载荷或密钥需要离开它。
为什么显而易见的部署方式无法通过审计
通常的首次部署会共用一份凭据。每一个 Claude Code 实例都以同一个服务账户——mcp-runner、ci-bot 或某个泛泛的名字——向 MCP 层及下游数据库进行认证。它能跑通,却悄无声息地摧毁了归因能力。
当十个代理都以同一个主体的身份行动时,数据库审计日志只会显示来自 mcp-runner 的十次写入,仅此而已。您无法判断是哪位工程师的会话发出了那条 UPDATE、它来自交互式的 Claude Code 提示还是某个无人值守的任务,也无法判断当时是哪个 MCP 工具处在调用路径上。独立研究(Optro)显示,能够把某次代理操作追溯到具体个人的组织占比约为 28%。共享服务账户是归因坍缩的一个结构性原因:审计日志能显示发生了什么,却永远无法显示是哪个代理做的。
第二个失败点是完整性。即便是按操作记录日志的团队,通常也是写入一个可变的存储。如果日志就放在攻击者(或某个存在缺陷的代理)能够触及的同一系统里,那么”我们有日志”并不等同于”我们有证据”。审计人员会把两者严格区分开来。
按代理划分的身份是基石
下游的一切都依赖归因,所以要先把身份这件事做对。不要使用单一的共享令牌,而要为每个代理——理想情况下是每个会话——签发独立、短期有效的身份。该身份会随请求一同传入 MCP 服务器,并进入代理所触及的任何资源,于是数据库端记录的主体就是这个代理,而不是一个泛泛的 runner。
这正是让最小权限变得有意义的关键。报表代理可以被钉死为对报表只读副本只读;发布代理则只对它自己拥有的工件具备写权限。如此一来,单次观测到的、越出该范围的写入,就成了一个精确、可归因的信号,而不是淹没在共享账户里的噪声。用策略来表达,这一意图非常直白:
agent "reporting-assistant" {
# Claude Code session via the reporting MCP server
resource "prod-postgres/reporting_replica" {
access = "read" # SELECT only
deny = ["write", "ddl"]
}
resource "s3://billing-exports" {
access = "read"
}
on_violation {
action = "block_and_alert" # refuse at access time, not just log
}
}
重点不在语法本身——而在于策略指明了代理、指明了资源,并把读与读/写区分开来。这一区分就是全部要害所在。
把 MCP 注解当作不可信信号来对待
MCP 工具可以通过 readOnlyHint、destructiveHint 等注解来描述自身。它们对初步分类确实有用——一个声明自己具有破坏性的工具理应配上更收紧的策略。但 MCP 规范明确指出,这些只是提示,客户端绝不能依赖它们来做安全决策。它们来源于服务器,而服务器恰恰正是您要审计的对象。一个工具可以声明 readOnlyHint: true,却依然发起写入——无论是出于缺陷、配置错误,还是蓄意规避。
因此正确的姿态是交叉印证,而非信任。把注解当作一个主张,然后用工具无法控制的某一层所提供的事实真相来核对它:
- 数据库审计日志(例如 PostgreSQL pgAudit)会告诉您一条
SELECT或UPDATE是否真的执行了。 - OpenTelemetry 来自 MCP 服务器及下游服务的 span 会展示调用图谱以及实际执行的操作。
- eBPF 内核信号是防规避的兜底防线:无论工具在用户空间里声称了什么,对文件或套接字的写入系统调用在内核层都是可观测的。
当注解声称只读、而内核却看到了一次写入时,这个矛盾就是头条级的发现——一个 readOnlyHint 的工具向 prod-postgres 写入了数据,正是那种值得把人叫醒的最小权限漂移。策略所允许的与采集器所观测到的之间的差异,才是真正风险所在之处。
一份审计人员会接受的账本
只有当追踪链可防篡改时,它才算是证据。把每个事件写入一个仅可追加、以哈希链相连的账本:每一条记录都包含前一条记录的哈希值,因此下游任何编辑或删除都会破坏链条,从而可被检测。每一行都把操作归因到具体的代理、指明资源、记录是读还是读/写,并附带一个置信级别——当身份与结果都得到交叉印证时为 attributed,当某些环节不得不靠推断时为 approximate。置信度被如实呈现;近似匹配绝不会被包装成已被证实的匹配。
ts=2026-06-08T09:14:02Z agent=reporting-assistant@s3f1 tool=sql-read resource=prod-postgres/reporting_replica op=R outcome=allow conf=attributed prev=8a1c…
ts=2026-06-08T09:14:05Z agent=reporting-assistant@s3f1 tool=export-writer resource=s3://billing-exports op=R outcome=allow conf=attributed prev=2f90…
ts=2026-06-08T09:17:48Z agent=data-export-job@b22e tool=sql-write resource=prod-postgres/customers op=RW outcome=DENY conf=attributed prev=c7d3… policy=read_only_violation
第三行正是审计人员所关心的:对一张该代理并无写权限的表的写入尝试,在访问发生时即被拒绝,归因到一个具名代理,并锚定在链条之中。由于策略是在访问发生时强制执行的——而不仅仅是事后记录——所以这次拒绝是一项控制措施,而非一份事后复盘。
还有两项特性让这条追踪链能够立得住。特权视图本身也会被审计:查看访问关系图这一行为会被记录下来,于是”谁看了什么”也是可回答的。而且证据导出本身可防篡改——您交给审计人员的,是一段经过签名、他们可以独立核验的链条切片,而不是一份只能凭信任接受的 CSV。
| 特性 | 共享服务账户 | 按代理划分的身份 + 哈希链账本 |
|---|---|---|
| 归因 | 所有代理共用一个主体 | 操作绑定到具体的代理/会话 |
| 读 vs 写 | 混为一谈 | 加以区分并接受策略校验 |
| 完整性 | 可变日志 | 仅可追加,一经编辑链条即断裂 |
| 可供审计的导出 | 凭信任接受的 CSV | 经签名、可独立核验的切片 |
为什么自托管在这里至关重要
这一切都无需任何东西离开您的网络即可运转。采集器是在旁观察——日志、OpenTelemetry,以及作为内核级兜底的 eBPF——而不是坐在代理的数据路径之上,因此即使它发生故障,也绝不会拖垮代理或其背后的生产请求。账本存储的是访问关系,而非载荷:它记录的是 reporting-assistant 读取了 reporting_replica,而不是它返回的那些数据行。可能携带密钥或 PII 的输入,会在写入任何内容之前先经过脱敏与密钥扫描。对于气隙隔离、受 GDPR 约束或受数据驻留限制的环境而言,这就是一条您能够辩护的审计追踪链与一条您无法辩护的审计追踪链之间的差别:关于您 Claude Code 与 MCP 使用情况的任何信息都不会回传,因为这套系统从一开始就根本看不到数据。
Claude Code 与 MCP 值得部署。它们只是需要一条审计追踪链,构建方式与您为任何其他特权自动化所构建的方式如出一辙——身份优先、结果交叉印证、证据被锚定。如果您想了解访问关系图与可防篡改账本是如何契合在一起的,安全模型与产品概览会在各自方面讲得更深入。