第 22 章 凭据与出口:Agent 手里的钥匙和门
钥匙不能给错,门不能大开
第 21 章管住了"Agent 能调用哪些工具"。但还有两样最关键的东西没管:凭据(Agent 手里握着的钥匙)和出口(Agent 数据出去的通道)。
凭据的问题是:把原始密钥交给 Agent,它就可能泄漏。出口的问题是:就算私有数据访问和不可信输入都在,只要 Agent 没法把偷到的数据送出去,攻击就失败了。
这一章讲四个模式,前两个管"钥匙",后两个管"门":
- Local-First Credential Broker(本地优先凭据代理):凭据留在本机,网络层注入,Agent 进程碰不到。
- Non-Custodial Spending Controls(非托管支出控制):Agent 能发起花钱的动作,但永远碰不到私钥。
- Egress Lockdown(出口封锁):默认拒绝出站,只有白名单能出。
- Zero-Knowledge Verified Egress(零知识出口):出站前证明"请求的内容是真的"。
模式一:本地优先凭据代理(Local-First Credential Broker)
问题
AI Agent 越来越需要调用带认证的第三方 API:发 Slack 消息、建 Linear issue、给 Stripe 客户扣款、写 GitHub 仓库。现在的主流做法是把原始凭据通过环境变量、.env 文件或逐工具配置交给 Agent。这在几个地方会崩:
- Token 从上下文泄漏。读环境变量的 Agent 可能把 token 回显进日志、提示词、错误追踪、共享对话记录。一次粗心的
os.environ打印就够泄漏一把长期有效的 API key。 - 没有 OAuth 刷新。静态 API key 永不过期,暴露了就能无限复用;OAuth 访问令牌需要刷新逻辑,Agent 不该为每个提供商自己写。
- 逐工具漂移。每个 CLI、每个框架、每个 skill 都自己搭一套凭据启动。结果同一个密钥散落在
~/.config/foo/、~/.bar/credentials、.env、shell 历史里。 - 托管凭据服务是拆东墙补西墙。把凭据路由经过 SaaS broker 能解决泄漏,但要求把 OAuth 刷新令牌发给第三方,制造新的审计面,还加了一个运行时网络依赖。
方案
引入一个本地的凭据 broker 进程来持有凭据。 Agent 通过 broker 跑的 loopback HTTPS 代理和提供商通信。broker 把出站请求和提供商注册表比对,挑出对的凭据,在请求离开主机前一刻把它注入进去。
核心组件:
- 加密本地保险库:存在用户机器上(比如
~/.credentials/或 OS keychain),持有访问令牌、刷新令牌、API key 和"属于哪个提供商"的元数据。从不碰远程服务。 - Loopback 代理:Agent 出站 HTTPS 流量经过的短生命周期进程。按请求主机名匹配提供商注册表,插入正确的
Authorization头。只改头,绝不改请求体。 - 刷新守护进程:盯访问令牌的过期时间,联系提供商自己的 token 端点刷新。刷新令牌直接从本地保险库到提供商,不经过任何第三方。
- 首次登录流程:浏览器 PKCE、OAuth device code、或一次性桥接(用户粘贴一次 API key)。首次登录后,Agent 再也不用看到用户的凭据输入过程。
- 提供商注册表:JSON 驱动的 manifest,把提供商主机名(
api.github.com、api.openai.com)映射到凭据类型、刷新端点、提供商特有的头。
class LocalCredentialBroker:
def __init__(self, vault_path):
self.vault = EncryptedVault(vault_path)
self.providers = load_provider_registry()
self.refresh_daemon = RefreshDaemon(self.vault, self.providers)
def proxy_request(self, req):
provider = self.providers.match(req.host)
if provider is None:
return req # 不认识的直通
cred = self.vault.get(provider.id)
if cred is None:
raise NotLoggedIn(provider.id)
if cred.expires_soon():
cred = self.refresh_daemon.refresh(cred)
req.headers["Authorization"] = provider.format_auth(cred)
return reqAgent 只需要知道它的 HTTPS_PROXY(或等价客户端配置)指向 127.0.0.1:<port>。从 Agent 的视角看,它的出站调用就是成功了,它从来没见过真实 token。
证据
- 证据等级:中(
medium)。 - 有价值发现:Cloud Security Alliance 的《Agentic AI Identity & Access Management》(2025-08)把"通过代理隔离凭据"列为 Agent 运行时推荐控制;OWASP 的《Securing Agentic Applications Guide》把"Agent 进程永不持有原始 API key"列为加固原则;2025-2026 出现了多个独立的开源实现(Python、Rust、服务端变体),说明是趋同设计而不是单一厂商模式。
- 未验证:共享开发机器上的长期行为;长任务里刷新失败是否真会引发宕机事故的运维数据。
怎么用
- 选保险库位置。默认用 OS 已经限制的每用户目录(Linux 用
~/.config/<broker>/,macOS 用~/Library/Application Support/<broker>/)。静态加密,密钥从 OS keychain 或用户口令派生。 - 定义提供商注册表。从 Agent 最常调的提供商开始。每条要主机名匹配、认证头格式、token 端点 URL。
- 按提供商选首次登录流程。OAuth2:桌面用浏览器 PKCE,无头用 device code。API key 提供商:一次性桥接页让用户粘贴。永远别让用户把密钥粘贴进 Agent 聊天。
- 把 broker 跑成长期存活的本地进程。刷新守护进程得活着,才能在 token 过期前刷新。
- 配置 Agent 用 loopback 代理。设
HTTPS_PROXY=https://127.0.0.1:<port>,装 broker 的本地 CA 让 Agent 的 HTTPS 客户端信任它。 - 审计这个接缝。broker 里记录每次凭据注入(提供商、请求路径、时间),绝不记录凭据值。把这些日志给用户看,不给 Agent 看。
要避开的坑:
- 别代理 broker 不认识的提供商流量。默认直通。一个吞掉所有出站 HTTPS 的 broker 会变成调试噩梦。
- 别把凭据存在全局注册表。用每用户保险库。同一主机上多用户共享一个保险库,重新引入这个模式要解决的泄漏问题。
- 别把 broker 自己的管理 API 放到公网端口。只走 loopback。
- 别忘了负载下的刷新。刷新令牌要退避重试;刷新失败要报成可行动的错误,而不是静默的 401 让 Agent 无限重试。
取舍
- 好处:原始凭据永不进入 Agent 的进程环境、对话上下文或崩溃转储;OAuth2 刷新对无头工作负载(cron、CI、后台 Agent)可用,不需要键盘前的人;无 SaaS 依赖,凭据除了给它们对应的提供商外从不离开用户机器;加一个新提供商是一条 JSON,不是改每个调用它的 Agent 的代码。
- 代价:给用户设置加了个本地守护进程和要信任的本地 CA;多用户主机要小心保险库权限,不适合共享开发机器场景;Agent 的 HTTP 客户端必须尊重代理配置,有些 SDK 硬编码自己的客户端要适配器;多租户 Agent 舰队(一台机器服务多用户)更适合托管网关,这个模式天生单用户。
模式二:非托管支出控制(Non-Custodial Spending Controls)
问题
能发起钱包动作的 AI Agent,可能因为提示词漂移、bug 循环、被攻破的提示词,发出不安全的交易。如果花钱审批直接放在 Agent 提示词或应用逻辑里,安全约束很容易被绕过。 这是"致命三要素"威胁模型的一个具体实例:钱包访问 + 不可信输入 + 对外通信,凑一起就开了利用路径。
方案
在 Agent 和交易签名之间插一个显式策略强制层。 Agent 提交交易意图,策略层对照规则验证,只有批准的意图才被转发给签名者。
核心机制:
- 意图优先工作流:Agent 永远不直接签名。
- 非托管边界:策略服务验证并返回授权,但从不存储或管理私钥。
- 失败关闭:策略检查不可用时,交易审批被拒绝。
- 双闸控制:签名前一个策略评估步骤 + 一个单独的授权/时机检查。
- 工具层集成:Agent 正常调钱包工具,策略层包住底层钱包库,对 Agent 透明。
怎么用
- 把动作空间建模成交易类型和目的地的允许列表。
- 加按资产、按端点的支出预算。
- 强制频率约束(频次/限流)和每日/每小时支出上限。
- 对关键对手方要求允许/阻止列表。
- 对每个拒绝/允许决策发出结构化审计日志。
取舍
- 好处:防止 Agent 运行时错误直接滥用签名凭据;改策略不用碰提示词逻辑;治理和事故复盘有清晰审计轨迹。
- 代价:每笔交易加延迟;策略服务高可用是正常运行的要求;配错的策略会挡住合法工作;比内嵌提示词检查运维复杂。
模式三:出口封锁(Egress Lockdown)
问题
就算有私有数据访问和不可信输入,只要 Agent 没法把偷来的数据送出去,攻击就失败。 这个模式实现了 Bell-LaPadula 模型的"不下写"性质:高权限主体不能写到低信任目的地。很多真实世界的修复就是直接移除或过滤出站通道。
方案
给 Agent 工具实现一个出口防火墙:
- 只允许特定域名、方法、或载荷大小。
- 在允许的出站调用里,剥掉或哈希内容。
- 禁止动态链接生成(比如
attacker.example/exfil?q=REDACTED)。 - 外部通信必不可少时,把它放进一个看不见私有数据的独立"哑" worker。
# Docker 文件示例
RUN iptables -P OUTPUT DROP # 默认拒绝
RUN iptables -A OUTPUT -d api.mycompany.internal -j ACCEPT
# 要 L7 感知的过滤:eBPF/XDP(Linux 4.19+)或 Cilium证据
- 证据等级:成熟(
established)。 - 多个厂商事故报告(Willison 引用的):Microsoft 365 Copilot、GitHub MCP、GitLab Duo Chatbot 的修复,第一补丁全是禁用出口路径。
怎么用
- 把 Agent 放进带出站规则的沙箱 VM 或容器里。
- 需要的 API 通过内部代理提供;审计那个代理的请求 schema。
- 用 seccomp profile 或 AppArmor 策略直接拦网络 syscall。
- 记录所有 DROP 事件,供取证跟进。
取舍
- 好处:大幅减少高影响泄漏;容易推理。
- 代价:会破坏合法集成;必要的调用要建代理桩。
模式四:零知识出口(Zero-Knowledge Verified Agent Egress)
问题
Agent 可能被诱骗发出一个网络层看起来正常、但带着虚假声明的出站请求:付给错误的收款人、参数被篡改的 API 调用、参数和用户批准的不一致的工具调用。域名允许列表或出口防火墙会因为"目的地是被允许的"就放行,它从不检查请求的内容是不是真的。在受监管或高价值流程里,团队需要每个出站调用在离开前证明它匹配一个可信的真相源,而又不把那个真相源暴露给 Agent 或目的地。
方案
在每个出站请求前面放一个验证层。 调用离开前,Agent 对它的声明被对照真相源检查,一个零知识证明确认声明成立而不泄露底层数据。证明验证通过,调用才出去。
这层包住 Agent 的 HTTP 路径和 MCP 交接:
- 拦截:挂钩 Agent 已经用的 HTTP 库和 MCP 工具调用,让每个出站请求都经过。
- 验证:对照真相源检查请求的声明,生成证明它们匹配的零知识证明。证明在持有真相源的后端跑,Agent 和目的地都看不到它。
- 强制:证明验证不过的一律拦,不在可信允许列表的端点也拦。记录每个出站调用供审计。
on_outbound_request(req):
proof = prove_against_source_of_truth(req.claims) # 零知识
if not verify(proof): return BLOCK
if req.endpoint not in allow_list: return BLOCK
log(req, proof)
return ALLOW这和出口封锁互补:出口封锁在网络层控制 Agent"能把数据发到哪",零知识出口在数据发出前验证 Agent"发的数据是不是真的"。
证据
- 证据等级:中(
medium)。 - 有价值发现:NIST SP 800-207(零信任架构)把"授予访问前逐请求验证"立为核心原则,这个模式把它用到 Agent 的出站调用;零知识证明让验证者确认声明而不看到背后的秘密(Goldwasser、Micali & Rackoff,1989),所以真相源对 Agent 和目的地都保持私密;Agent 安全文献把工具滥用和篡改的工具参数归为网络层出口控制覆盖不到的独立威胁类(见 arXiv:2510.06445 和 OWASP 的 Agentic AI 威胁分类);在 HTTP 和 MCP 层挂钩,不管哪个框架产生的出站调用都能抓到。
- 未验证:证明生成的延迟、高调用量下跑验证后端的运维成本,需要更多生产数据。
怎么用
- 包住任何会发出站 HTTP 或 MCP 工具调用的 Agent 框架(LangChain、CrewAI、MCP server、自定义 SDK),让每个出站请求都过验证层。
- 为重要的流程定义真相源(批准的收款人、订单详情、策略限制)和可信端点允许列表。
- 选证明机制:零知识证明让真相源对 Agent 和目的地都保持私密;隐私不是约束时,验证者签名的证明(signed attestation)能达到同样的门禁。
- 出站调用都路由过这层;证明失败或端点不在允许列表,当硬阻止。
- 保留签名日志供审计和事故复盘。
取舍
- 好处:抓住"看起来真但实际假"的请求,网络允许列表抓不住这种;零知识证明让真相源保持私密;通过 HTTP 和 MCP 挂钩与框架无关;每次调用都有日志。
- 代价:证明生成每次调用加延迟;验证跑在后端,不是完全离线;要为每个流程定义并维护真相源。
四个模式怎么选
| 场景 | 推荐模式 |
|---|---|
| Agent 调多个认证 SaaS,想别泄漏凭据 | 本地优先凭据代理 |
| Agent 能发起钱包/花钱动作 | 非托管支出控制 |
| 想堵住数据外传 | 出口封锁 |
| 出站请求内容也要可信 | 零知识出口 |
四个模式是凭据与出口的完整防线:凭据代理管"钥匙不交到 Agent 手里",支出控制管"能花钱但不能碰私钥",出口封锁管"数据出不去",零知识出口管"出去的必须是真的"。钥匙、钱包、出口、内容,四道闸都设上,Agent 才真正被关在笼子里。
实践清单
- 凭据放本地加密保险库,通过 loopback 代理注入,Agent 进程碰不到原始 key
- OAuth 刷新走本地守护进程,刷新令牌只从保险库到提供商,不经第三方
- 首次登录用 PKCE / device code / 一次性桥接,别让用户把密钥粘贴进聊天
- 钱包/花钱动作:Agent 只提交意图,策略层验证后才到签名者,私钥永不进 Agent
- 支出控制:交易类型允许列表 + 预算 + 频率约束 + 每日/每小时上限
- 出口防火墙默认拒绝出站,只放白名单域名/方法/载荷
- 必要的外部通信放"看不见私有数据"的哑 worker
- 出站请求过验证层:零知识证明/签名证明对照真相源,不过就拦
- 记录凭据注入、允许/拒绝、DROP、出站调用的审计日志,绝不记录凭据值
本章小结
- 本地优先凭据代理:凭据留本机,网络层注入,OAuth 刷新无头可用,Agent 不见真 token。
- 非托管支出控制:意图优先 + 非托管边界,Agent 能花钱但永远碰不到私钥。
- 出口封锁:默认拒绝出站,只放白名单,堵住数据外传这条路。
- 零知识出口:出站前证明内容是真的,网络层看着正常但声明是假的一样拦。
- 凭据和出口管好了,"致命三要素"里"能对外通信"这一环就被钉死。
下一章讲多智能体信任:多个 Agent 之间怎么互相信任、怎么审计。零信任网格、灵魂绑定身份、间接证据信任链、防篡改审计。