# 适用于自主编程工具的 macOS Keychain

macOS Keychain 擅长在凭据无人使用时保护它。但这不代表它能为自主编程工具提供完整的安全边界。进程一旦能够取回 API 令牌或私钥，通常也就能复制、打印或向外发送它，甚至能在操作员以为任务已经结束后继续使用。

与传统桌面应用相比，这一点对代理更重要。人会选择菜单命令，并看到即时结果。代理则会解释文本、调用工具、跟进输出，并可能持续运行数小时。Keychain 存储可以回答：「谁可以读取这个秘密？」代理式执行回答的是更难的问题：「这个特定进程现在能否执行这个特定操作，同时永远拿不到秘密？」PAM 会话认证回答的是另一个相关但不同的问题，即谁打开了登录会话。把这三种控制当成一回事，会留下演示时不易察觉、事后却很难查清的漏洞。

## Keychain 保护存储的凭据，而非每次使用

Keychain 会加密存储的秘密材料，并通过 macOS 安全服务调解访问。与 `.env` 文件、shell 历史记录、代码仓库或代理配置文件相比，它显然更适合存放令牌。项目可以带有访问控制，系统也能在释放项目之前要求用户在场。这些措施确实能抵御随手窃取文件和未经授权的读取。

边界在秘密被披露时结束。如果编程工具调用一个以文本返回令牌的辅助程序，令牌现在就存在于辅助程序、进程间通信路径和接收进程中。它可能出现在调试消息、异常对象、记录稿、崩溃报告、交换空间、复制的环境、子进程或恶意外发请求里。Keychain 无法收回这些字节。之后锁定钥匙串，也不会让已经复制到内存中的不记名令牌失效。

Apple 的 Keychain Services 文档描述了密码、密钥和证书的存储与取回。这个表述既准确又有用：取回本来就是受支持的结果。开发者有时把 Keychain 说成能把每个秘密都变成不可导出的签名密钥。事实并非如此。某些加密密钥可以带访问控制创建，并通过安全 API 使用而不导出私有材料，但普通 API 令牌最终仍要出现在某个 HTTP 请求头中。安全问题在于，哪个可信组件构建并发送该请求。

可以用测试账户而非生产令牌暴露这个问题。存入一个可随时丢弃的值，让代理平常使用的执行路径取回它，然后查看进程能怎样处理返回的字节：

```sh
security find-generic-password -a agent-test -s example-api -w
```

输出形式是秘密，后跟一个换行符：

```text
test_token_7f3a...
```

如果代理或受代理控制的 shell 能成功运行这条命令，存储层就已经作出了决定。终端中的遮盖只改变人能看到什么，并不能阻止进程重定向输出、编码内容或把它放进请求。

## 获准读取的进程可能意外导出秘密

危险主体往往不是陌生攻击者，而是你特意允许工作的编程工具。自主进程要发挥作用，就需要广泛访问文件、构建工具、软件包命令和网络客户端。再给同一进程可读凭据，提示注入、遭入侵的依赖项或一个错误命令就可能把普通访问变成凭据导出。

代码签名要求可以缩小允许访问 Keychain 项目的应用范围。当威胁来自另一个未签名或签名不同的进程时，它们确实有帮助。但如果获批应用可以扩展、启动 shell、加载插件、接受不受信任的仓库指令，或暴露工具协议，作用就小得多。执行受攻击者影响行为的，可能正是那个已签名二进制文件。

这里还存在委托问题。假设获批桌面辅助程序读取令牌，并通过标准输入交给代理。Keychain 看到的是获批辅助程序，而不是最终使用者。再假设辅助程序把令牌放入环境变量供某条命令使用，所有继承该环境的子进程都可能收到它。最初的访问决定并未说明原本打算执行哪个下游操作。

有效的审查应把秘密当作数据追踪，而不是只看架构图中的一个方框。要回答四个具体问题：

- 哪个进程最先收到明文？
- 是否有子进程、插件、shell 命令或记录稿能收到副本？
- 接收者能否选择目标、HTTP 方法、路径或 SSH 主机？
- 什么事件会终止接收者的权限，又是否会清除已经披露的材料？

团队常常只回答第一个问题，跳过另外三个。这样的设计静止时看似受到保护，代理一启动，行为却和长期明文凭据相差无几。

## PAM 证明登录事件，而非代理意图

可插拔认证模块 PAM 让服务在登录边界应用认证策略。在 macOS 上，`sudo`、`login` 和远程登录路径等服务使用具名 PAM 配置。PAM 栈可以认证用户、检查账户、建立凭据，以及打开或关闭会话。它适合决定某人能否开始一个有特权的操作系统会话。

PAM 通常不会调解已运行代理发出的每个 HTTP API 调用。认证完成后，进程使用它获得的操作系统身份和能力执行操作。如果进程能读取令牌，PAM 不会把之前的 Touch ID 手势或密码输入绑定到之后的某一个请求上。它也不了解 `POST /releases` 比 `GET /status` 更敏感，除非另一个组件实现了这种应用层区分。

「会话」这个词容易引起混淆，因为它指代几种互不相关的生命周期。PAM 会话可能围绕一次登录或 `sudo` 活动。终端会话可能一直留在窗口里。代理会话可能指一次进程运行、一次对话或恢复的任务。API 会话也可能是不记名令牌的有效期。结束其中一个，不一定会结束其他会话。

一个具体故障是这样发生的：操作员认证后启动 shell，再启动代理并批准 Keychain 访问。代理读取部署令牌。数小时后，操作员关闭可见对话，但一个子进程仍然存活，环境中还留着令牌。PAM 正确认证了最初的会话，Keychain 也正确批准了读取。两者都无法阻止继续使用，因为都不负责操作的生命周期。

当决定的是某个操作系统账户能否建立会话时，应使用 PAM。不要把 PAM 认证当作证据，声称该会话内每个操作都带有新鲜的人类意图。这种说法要求 PAM 提供它并不具备的语义。

## 认证和授权回答不同的问题

认证确认主体是谁，或者至少确认它提交了哪个凭据。授权决定该主体在特定上下文中可以做什么。秘密存储会保管材料，直到获准读取或执行加密操作。这些控制可以互相配合，却不能彼此替代。

对于自主工具，要精确命名主体。「用户」太模糊。键盘前的操作员、已签名代理可执行文件、新生成的代理进程、MCP 服务器、shell 子进程和远程 API 账户都是不同主体。如果一次批准同时适用于全部主体，设计就该明说。

日常工作中最实用的单位通常是一次进程运行。操作员可以授权一个已识别的代理进程直到它退出，然后要求新运行重新作出决定。这个边界能防止静默重启继承旧批准。对于高度敏感的凭据，授权还应更窄：即使运行已获批准，也要每次使用时确认。

逐次批准并不会自动带来安全。只写着「允许网络访问？」的对话框几乎不给操作员任何判断依据。批准界面应标明请求进程，并显示会改变风险的操作细节，比如凭据别名、HTTP 的方法与目标，或 SSH 的用户与主机。它还应避免批准疲劳。如果每次无害读取和生产写入都会触发同样的打断，人们就会对两者都一路点击通过。

进程身份也不能只看路径或显示名称，因为两者都可以复制。在 macOS 上，代码签名要求可以把决定绑定到签名机构和指定要求，进程标识符则用于区分不同运行实例。应向操作员展示系统实际验证过的签名事实，不能简化成不受信任二进制文件也能模仿的友好应用名称。

代码签名仍不能证明进程行为良好。它根据签名模型确认来源，而不是确认意图。正确签名的代理可能服从仓库中的恶意指令，已签名插件宿主也可能加载受攻击者控制的内容。因此，会话批准应表示「这个已识别进程可以在退出前请求代理执行操作」，而不是「该软件的所有请求都安全」。

实现前还要定义子进程与会话的关系。自动信任每个子进程会让长进程树难以撤销，还可能让通用 shell 继承超出预期的权限。要求每个短命辅助程序都触发新人工提示，又会使自动化无法使用。清晰的设计把授权放在获批代理进程拥有的代理连接上。子进程不继承凭据，只能通过明确受控、并且能随父会话撤销的通道访问受保护操作。

进程退出是一个有用的自动终点，但不是唯一终点。某次运行开始出现异常行为时，操作员需要立即撤销。代理执行器应在撤销后拒绝已排队和未来的操作，关闭会话句柄或让它失效，并记录该决定。已经发往远端的请求能否停止取决于协议和远程服务，因此界面不能假装撤销能倒转已经完成的工作。

还要测试重启语义。有些代理客户端会自我更新，在辅助程序崩溃后重新连接，或在新进程中恢复对话。便利代码常把这看作同一个逻辑会话。除非专门设计的认证交接协议能证明连续性，安全边界就应把新进程视为新的主体。代理提交的对话标识符不能作为证明，因为代理可以复制它。

由此形成的是一组分层决定，而不是一个过大的权限：

1. 凭据存储当前是否解锁并可用？
2. 这个新代理进程能否在其生命周期内执行操作？
3. 这个特定凭据是否要求每次使用都作出决定？
4. 可信执行器是否会限制并记录请求的操作？

前三个问题管权限，第四个管执行和证据。把它们合并成模糊的「可信代理」开关，会让撤销和事件审查困难得多。

## 代理式执行不让凭据进入代理内存

代理式执行器把接口从「把令牌给我」改成「替我执行这个已定义操作」。代理提交包含非秘密参数的请求。执行器检查权限，在自己的边界内取回凭据，把它注入出站协议，执行操作，然后只返回代理需要的结果。

对于 HTTP API，发给执行器的请求可能是：

```json
{
  "credential": "staging-release-api",
  "method": "POST",
  "url": "https://api.example.invalid/v1/releases",
  "headers": {"content-type": "application/json"},
  "body": {"commit": "8a31c2e", "channel": "candidate"}
}
```

执行器在授权后加入 bearer、basic 或自定义认证头。代理可见的响应可以保留状态、所选请求头和正文，同时省去注入的凭据：

```json
{
  "status": 201,
  "headers": {"content-type": "application/json"},
  "body": {"release_id": "rel_1042", "state": "queued"}
}
```

这个边界比遮盖更明确。遮盖是假设秘密已经进入不受信任的数据路径，再尝试隐藏可识别形式。代理式执行从一开始就避免披露。它也让撤销真正有效：执行器一旦拒绝下一个请求，代理手上没有缓存的不记名令牌可以绕过它。

执行器仍需谨慎处理输出。API 可能回显请求头，SSH 命令可能打印环境数据，详细客户端也可能在错误中包含认证材料。结果返回前要过滤已知凭据位置，限制诊断细节，并像测试成功调用一样严格测试失败路径。「代理永远收不到凭据」必须覆盖错误和日志，不能只覆盖正常响应。

代理式 SSH 遵循同一思路，但需要理解协议的执行器。代理请求针对某个具名主机身份运行命令。执行器在内部使用私钥，完成主机验证，运行命令，并返回 stdout、stderr 和退出状态。即使之后会清理，把临时私钥文件交给代理仍然属于披露。

## 执行器限制凭据窃取，却不能阻止所有有害操作

不让代理取得令牌可以消除一大类故障，但不能保证代理请求的操作正确。获准的 `DELETE` 请求可以在完全不泄露凭据的情况下删除数据。SSH 命令也能在私钥受到完美保护时破坏主机。执行器控制权限怎样行使，却不能只凭语法推断业务意图。

目标控制很重要。如果代理可以选择任意 URL，而执行器盲目添加凭据，代理就可能把凭据发往攻击者控制的主机。可靠的 HTTP 执行器会把各凭据绑定到适当目标，或以其他方式保证只在预期位置注入认证。重定向也需要同样审查，因为合法来源可能把客户端重定向到别处。对 SSH 而言，主机身份检查必须属于执行过程，不能变成代理可选的标志。

响应数据仍然暴露。安全保管的凭据可能授权请求返回客户记录、部署配置或另一个秘密。代理只需要完成任务所需的最少响应，但通用 API 很难强制这一点。代理式执行减少凭据暴露，却不会自动防止数据外泄。

提示注入依旧相关。仓库文本、问题评论、构建输出和抓取的文档都可能诱使代理请求一个有效但并非本意的操作。人工批准只有在卡片显示足够细节且人认真阅读时才有帮助。高风险系统可能需要权限更窄的远程凭据、API 端范围、受保护分支、预发布环境、执行器中的命令允许列表，或单独的工作流引擎。这些控制应靠近它们能够理解的操作。

「把所有秘密放进 Keychain 并要求 Touch ID」这一常见建议并不成立，因为它把正确的存储选择和过宽的使用决定混在一起。Touch ID 可以证明释放秘密材料时有人在场，却无法阻止获批接收者之后误用或复制材料。值得人工确认的操作，应在操作边界要求生物识别，同时根本不要把令牌交给代理。

## 审计证据应描述操作，并能发现篡改

有用的审计记录要回答：谁请求了操作，哪个进程实例发出请求，选择了哪个凭据身份，使用了什么目标和操作，发生了何种授权，操作何时运行，以及如何结束。记录不应包含凭据本身。控制台记录稿很少满足这个标准，因为它把模型文本、工具杂讯、命令和截断输出混在一起，没有稳定的事件模型。

会话事件和调用事件应分开，同时保留两者关系。会话日志应显示进程何时首次出现、系统如何识别它、谁批准了它、是否有人撤销，以及它何时退出。活动日志应显示每次 HTTP 或 SSH 操作及其结果。事件审查者才能同时回答「这次运行是否获批？」和「它实际做了什么？」

普通加密日志能保护静态机密性，却无法证明无人删除或重新排序记录。哈希链把每个条目与前一个条目相连，使后续验证能发现被更改、缺失或重新排序的密文记录。它不能证明每个事件都曾被记录，也不能阻止整个日志库被删除。它提供的是现存记录完整性的证据，必须准确说明这个限制。

实用的验证命令应给出简单、可自动处理的结果。例如，Sallyport 从一份只写不可回读的加密哈希链审计日志生成 Sessions 和 Activity 日志，其验证器无需解密密钥即可离线检查密文：

```sh
sp audit verify
```

成功时，验证器应报告检查过的链并返回退出状态零；链路断裂时，应标明失败并返回非零状态。把这项检查放入事件取证和备份验证，而不只放在产品界面中。由同一个可变应用显示的绿色图标并不能提供独立证据。

日志还需要明确的遮盖约定。记录凭据别名，而不是秘密值。决定请求正文和命令输出是保存、截断、散列还是排除。还要测试畸形请求和传输错误，因为失败日志往往比成功日志捕获更多原始上下文。

## 用对抗性测试评估整条路径

只有当测试试图违反架构声明时，声明才有意义。创建一次性凭据和端点，再运行正常代理工作中使用的确切二进制文件和进程边界。一张权限对话框截图不能证明哪些数据进入了内存，又有哪些留在子进程中。

一份紧凑的测试计划可以覆盖主要边界：

1. 启动新代理进程，确认旧会话批准不会继承。记录向操作员显示的进程身份。
2. 请求低风险 API 读取，再请求受逐次保护的写入。确认批准卡能区分目标和操作。
3. 在代理可见的 stdout、stderr、环境、记录稿、工具载荷和崩溃输出中搜索一次性秘密及其编码变体。
4. 撤销正在运行的会话，然后分别从父进程和先前生成的子进程重试。两者都应在网络或 SSH 执行前失败。
5. 修改、删除并重新排序审计记录副本。确认离线验证对每条损坏的链都返回失败。

再加入两个团队常遗漏的滥用案例。让测试端点重定向到另一个来源，并验证认证不会跟随。让端点回显所有收到的请求头，再以详细错误失败，确认执行器会在响应到达代理之前移除凭据材料。

对于 SSH，应尝试未知主机密钥、已更改主机密钥、交互式密码提示，以及打印环境的命令。确认每个决定由谁负责。无状态辅助程序不应悄悄创建第二个凭据缓存，也不应继承超过需要的环境数据。

通过条件必须陈述可观察结果。「秘密受到保护」无法测试。「字节序列及其 base64 形式从不出现在代理可见文件、进程环境、工具响应或日志中」才可测试。「撤销有效」太模糊。「被撤销进程及其现有子进程的每次调用，都在打开套接字之前失败」给了工程师明确测量对象。

## 根据代理得到的权限选择控制

当传统应用取回低影响凭据、代码路径狭窄、用户直接发起每个操作，并且威胁模型允许该应用得到秘密时，只用 Keychain 也可能足够。它仍然可以在更强的设计中充当可靠存储层。没有必要为了替换成熟的加密存储而自制秘密文件。

如果自主编程工具能选择操作、运行不受信任的项目代码、生成任意子进程，或在无人持续关注时继续工作，答案就不同了。应向代理提供操作能力，而不是凭据字节。认证每次新进程运行，仅为敏感密钥保留逐次确认，把凭据绑定到预期协议和目标，并让撤销立即切断之后的使用。

在选择设计之前，先写下必须成立的最强声明。磁盘加密需要存储控制；只有本次运行的已签名进程能请求操作，需要进程认证和会话授权；进程能调用发布 API 却拿不到令牌，需要执行器；能够发现现存审计记录被修改，则需要完整性证据。每条声明都有不同的测试和负责人。

同时记录每条声明剩余的风险。执行器不能撤销远程写入，会话边界不能删除 API 已返回的数据，代码签名不能证明指令无害，哈希链也不能证明整个日志库都被保留。这样审查就不会让一个控制为它不负责的工作得分。

迁移不必一次改变所有集成。先处理泄露后会产生最大独立权限的部署令牌、基础设施 API 密钥和访问共享系统的 SSH 身份。把读取接口换成小型操作接口，再删除旧环境变量与辅助命令，并用测试凭据证明旧路径确实失败。

还要明确运维责任：谁能解锁保险库、批准进程、设置逐次确认、撤销会话、轮换远程凭据和验证审计链。代理行为异常时，应先撤销会话，保存并验证审计材料，再轮换任何可能进入代理内存的凭据。

Sallyport 在 macOS 上采用这个模型：代理通过它的 `sp mcp` 服务器请求 HTTP 和 SSH 操作，API 密钥与 SSH 密钥留在其进程内加密保险库中。固定分层控制包括绝对保险库门、默认针对每个新代理进程的授权，以及可为指定密钥启用的逐次批准。

不要把这种设计误认为通用策略引擎或网络拦截代理。范围较窄的执行器之所以能对凭据保管作出有力承诺，正是因为它只负责明确的执行路径。路径之外的操作需要自己的控制，远程服务仍应实施权限范围、环境隔离和账户级撤销。

审查时只需问一个具体问题：批准后，自主进程能否打印、复制或独立重用凭据？如果可以，Keychain 保护了存储，却没有限制使用。继续用 Keychain 做它擅长的事，再把权限边界移到真正执行并记录操作的组件上。
