密钥委托问题:AI Agent 时代的凭证管理困境

用 Claude Code 排查一个环境变量问题时,它会读取你的 .zshrc。配置 MCP 服务时,它会读取 .cursor/mcp.json。调试 npm 安装时,它会读取 .npmrc。每一次读取操作的完整文件内容都被记录进了会话日志,其中的密钥自然也一并保留。

这个过程不需要你做任何错误操作。你请求帮助,AI 助手读取相关文件,文件内容进入 session log。这是所有具备文件读取能力且保留会话记录的 AI 编程工具的共同特征,是架构层面的行为。

但比起 session log 中出现了几个明文密钥,更值得关注的是一个更深层的问题:我们还没有一套合适的机制,来向这类半自治的 agent 委托密钥访问权限。密钥存储本身已经有了成熟方案;真正困难的是 secret delegation,即如何在 agent 需要凭证时,以合理的权限边界和生命周期把凭证交给它。

一类新的密钥消费者

密钥管理并非新问题。在 AI 编程工具出现之前,行业在两个端点上已经有了可用的方案。

一端是本地交互式使用。开发者坐在自己的机器前,密钥管理工具通过 Touch ID 或 Apple Watch 做生物识别确认,批准后注入环境变量。人类在场,批准链路短,体验也合理。1Password 的 op run、Doppler 的 doppler run,走的都是这条路径。

另一端是无人值守的机器身份。CI 管道、cron job、服务器进程,没有人类在场,也不需要。密钥从平台级 secrets 存储注入(GitHub Actions secrets、AWS IAM role、Vault 的 AppRole),有自动轮换,有审计日志,有明确的权限边界。这一端的关键假设是:消费者的身份和行为模式是预先确定的,权限可以在部署时静态配置。

AI 编程 agent 落在这两端之间。它能读取文件、维持上下文、反复调用工具、生成代码和命令,行为模式比 CI 脚本丰富得多。但它又不是一个坐在屏幕前的人,无法随时响应生物识别弹窗。更关键的是,它的执行环境跨越了上述两端的假设边界:在本地交互式场景中,它有时需要在用户不逐一批准的情况下连续执行多步操作;在远程 SSH 场景中,桌面认证链路完全断开;在 CI 中,它比传统脚本拥有更大的上下文和更灵活的行为空间。

这个中间地带正是当前产品覆盖最薄弱的地方。

Session log:搜索空间压缩与权限降级

在讨论委托机制之前,有必要先理解 session log 为什么让问题更紧迫。我在自己的开发机上做了一组实验。

在一台活跃的开发机上,密钥散布在不同路径、不同格式的配置文件中。攻击者要逐一收集它们,需要知道去哪些路径找、用什么格式解析、如何区分有效凭证和占位符。Session log 改变了这个方程:AI 助手在日常工作中读取过的所有文件内容,按时间顺序排列在一个 JSONL 文件中。攻击者拿到一个 session 文件后,在一个格式统一的文件中做正则匹配就够了。Session log 成为了一个非预期的密钥索引。

我在 Claude Code 的 transcript 文件中确认了来自 .zshrc.cursor/mcp.json.npmrc 的真实密钥,包括 npm token、Tavily API 密钥和 1Password 服务账号 token。在 Codex/OpenCode 的 session 目录中同样确认了 Tavily、1Password、Mistral 和 HuggingFace 的真实密钥。这些密钥进入 session log 的路径都是正常的文件读取操作。

更值得关注的是权限降级。我用 stat 测量了源文件和 session log 的实际权限:

文件 权限
~/.cursor/mcp.json 600(仅所有者可读写)
~/.npmrc 600
~/.claude/transcripts/*.jsonl 644(所有者可读写,其他用户可读)
~/.codex/sessions/*.jsonl 644

存储在 ~/.npmrc(权限 600)中的真实 npm token,出现在了 Claude transcript 文件(权限 644)中。存储在 ~/.cursor/mcp.json(权限 600)中的真实 Tavily API 密钥,出现在了多个 Claude transcript 文件(权限 644)中。从 600644,意味着原本只有文件所有者才能读取的密钥,现在同一台机器上的任何用户都能通过 session log 读到。在共享开发服务器、多用户 CI 环境或容器编排场景中,这个权限差异的实际影响会被放大。

以上测量仅限于本地文件系统权限。各 AI 工具厂商是否以及如何将 session log 同步到云端、存储策略和保留期限如何,取决于具体厂商的隐私政策和用户设置,我没有直接测量,因此不做断言。

Blast radius 远超 API 账单

理解了 session log 如何集中和降级密钥之后,下一个问题是:一旦这些密钥被获取,实际影响有多大?

一个模型服务的 API key 可能允许读取调用历史、访问日志、触达共用身份体系的其他服务。泄露的后果从资源滥用升级为系统边界探测。CI token 的价值在于它是否能改构建步骤、替换依赖、注入产物、触发自动发布。2025 年的 tj-actions/changed-files 事件就是这个类别:攻击者利用构建链中的信任关系收集和外泄 secrets。npm token、GitHub automation token 一旦和自动发布流程绑定,攻击者拿到的是一个分发通道。Shai-Hulud 利用被盗 npm token 自我复制,说明攻击者真正想要的是你在软件分发链上的位置。

这组实验里最值得警惕的例子是 1Password 的 service account token。普通 API key 泄露意味着某个系统被误用,secrets manager 的 bootstrap credential 泄露意味着攻击者开始接近整组凭据。局部问题升级为账户级问题。

GitGuardian 2026 年度报告提供了一些行业背景:2025 年公开 GitHub 上新增了 2865 万个硬编码密钥,AI 辅助的 commit 密钥泄露率约为人工 commit 的两倍。MCP 配置文件中发现了 24,008 个明文密钥,其中 2,117 个仍然有效。

产品缺口:两端可用,中间断层

如果把密钥消费者按自主程度排列,从人类坐在屏幕前到无人值守的 CI 管道,会发现现有产品在两端的覆盖是合理的。

本地交互式场景中,1Password 通过 macOS Keychain 和 Secure Enclave 实现生物识别批准。Secret zero 是一个硬件断言,绑定在用户的物理在场上。只要开发者坐在机器前,这条链路运转良好。

无人值守的 CI/CD 场景中,密钥从平台 secrets 存储注入,身份由 OIDC provider 或 IAM role 锚定。密钥有自动轮换,有审计日志,有权限边界。secret zero 是平台基础设施本身的信任锚。这一端的方案也是成熟的。

断层出现在远程/半自治的 agentic execution 场景。通过 SSH 连接到远程机器运行 AI 助手时,桌面认证链路断开。Touch ID 弹窗无法到达用户。重复完整登录的摩擦很高。此时 service account token 变成了诱人的替代方案,但 service account token 一旦放在 agent 可读的文件中(比如 .zshrc),就回到了 session log 泄露的老问题。这一次泄露的是 master credential。

这个断层是系统性的。它源于一个还没有被现有产品充分建模的身份类型:一个行为丰富、上下文密集、执行模式动态的半自治消费者。当前的密钥管理工具要么假设有人类在场做交互式批准,要么假设消费者的行为模式可以在部署时静态确定。AI 编程 agent 两个假设都不满足。

Secret zero:委托链的操作瓶颈

任何密钥管理系统都需要一个初始凭证来证明请求者的身份。1Password 需要 service account token,Doppler 需要 project token,HashiCorp Vault 需要 role ID 加 secret ID。密钥管理工具把 50 个明文密钥收拢成了 1 个 bootstrap credential,但这 1 个仍然需要存放在某个地方。如果那个地方是 AI 工具可以读取的文件,session log 中出现的将是一个能解锁所有密钥的 master token。问题上移了一层,暴露面反而更集中。

所以 secret zero 无法被彻底消除。能做的是约束它的属性:缩短生命周期,缩小作用域,把它绑定到具体的会话、设备或身份。不同场景对这些属性的约束方式截然不同,这也是为什么下面需要按场景分别讨论。

短期应对:按场景的注入方案

在行业给出真正的 agent delegation 层之前,以下是当前各场景下可用的做法。

本地交互式开发。 开发者坐在自己的机器前,AI 助手在本地运行。这个场景的关键优势是存在一个物理在场的人类,可以完成交互式身份验证。.env 文件中只写引用,运行时通过 op run 注入真实值:

# .env 中只有引用,AI 助手读到的也只是引用字符串
OPENAI_API_KEY="op://Development/OpenAI-API/key"
ANTHROPIC_API_KEY="op://Development/Claude-API/key"

# 启动时注入真实值,密钥只在目标进程的环境变量中存在
op run --env-file .env -- claude

第一次调用时 1Password 弹出 Touch ID 确认,之后在应用未锁定期间不再重复。密钥不写入磁盘文件,AI 助手读不到 .env 中的明文,session log 中只会出现 op://... 引用字符串。这里的 secret zero 是 1Password 桌面应用的登录态,绑定在 macOS Keychain 和 Secure Enclave 上,受 Touch ID 保护,是一个不可复制、不可远程获取的硬件断言。Doppler(doppler run --)和 Infisical(infisical run --)提供类似的运行时注入能力。

远程 SSH 开发。 通过 SSH 连接到远程机器运行 AI 助手。这个场景失去了本地场景的关键优势:没有图形界面,Touch ID 弹窗无法到达用户。一个常见做法是在远程机器的 .zshrc 或 systemd environment 中写入 OP_SERVICE_ACCOUNT_TOKEN。这能工作,但需要意识到其含义:一个长期有效的、可以读取指定 vault 的 token 被放置在 agent 可读的位置。

更合适的方式是利用 SSH 会话本身作为授权通道:

# 在本地取得密钥,通过 SSH 环境传递,会话结束即消失
ssh -A remote-host \
  "OPENAI_API_KEY='$(op read op://dev/dev-api-keys/openai_api_key)' claude"

密钥生命周期约束到单次 SSH 会话,远程磁盘上不残留凭证。代价是每次建立会话时需要在本地完成一次 1Password 认证。如果基础设施约束要求远程机器上有常驻凭证,至少应做到:service account token 的权限限定到最小必要的 vault 和 item;token 有明确的过期时间;token 存储在只有目标进程能读取的位置(如 systemd 的 EnvironmentFile,权限 600),而非 shell profile 中。

无人值守 / CI。 没有人类在场,不能弹 Touch ID,也不能依赖交互式 SSH 会话。为每个自动化任务创建独立的 machine identity,密钥从 CI 平台的 secrets 存储注入:

# GitHub Actions:密钥从 repo secrets 注入
- name: Run analysis
  env:
    ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  run: claude --dangerously-skip-permissions -p "analyze codebase"

关键约束:每个 identity 只能访问完成任务所必需的资源,密钥有自动轮换机制,审计日志记录每次访问。这里的 secret zero 是 CI 平台本身的信任锚。它同样不能被彻底消除,但暴露面远小于配置文件里的长期 token。

检测与轮换

无论采用哪种方案,都需要处理已经泄露的密钥。用 Gitleaks 作为 pre-commit hook 拦截密钥进入 git 历史,用 TruffleHog 对已有历史做深度扫描并验证泄露的密钥是否仍然有效。如果你的 session log 中已经包含真实密钥,仅删除日志文件不够,应假设这些密钥已经暴露,立即轮换。

委托层仍然缺失

以上方案可以在各自场景内降低暴露面,但它们本质上都是在现有产品能力的边界内做局部优化。远程 SSH 场景中的 SSH 环境变量传递是一个可用的 workaround,但它的可用性随着 agent 会话时长和切换频率的增加而下降。CI 场景中的 platform secrets 方案成熟,但它的前提是消费者的行为模式可预测、可静态配置,这个前提在 agentic 工作负载中会越来越勉强。

行业仍然需要一个面向半自治 agent 的密钥委托层。它的核心属性应该包括:密钥的作用域与 agent 的当前任务绑定,而非与整个 vault 或整台机器绑定;密钥的生命周期与 agent 会话绑定,会话结束后自动失效;agent 对密钥的使用有审计日志,且日志本身不包含密钥明文;委托过程可以由人类预先批准一次,之后在约定的权限边界内自动执行,而非每次弹窗或完全无监管。

在这个层出现之前,一条操作原则仍然适用:如果 AI 助手能读到一个文件,就应该假设这个文件的内容已经进入了 session log。由此推导:任何你不希望出现在 session log 中的信息,都不应该以明文形式存在于 AI 工具可访问的文件系统路径中。这包括你用来访问密钥管理工具的 bootstrap credential 本身。


Comments