# 在访问敏感服务前固定代理客户端版本

本地代理客户端不应仅仅因为名字出现在获准工具列表中，就获得敏感服务的访问权限。真正决定请求内容的是可执行文件、运行时、扩展以及它的启动方式。只要其中任何一项发生变化，你面对的就是不同的安全主体，即使窗口标题和命令名称看起来仍然熟悉。

代理客户端版本固定是一种发布控制措施，不是解决糟糕提示词或权限过大的办法。它带来一个实用特性：你可以测试一个已知构件，记录测试对象，并拒绝让未经审查的替代版本访问敏感服务。听起来很普通，却能避免许多本可避免的故障。

我见过一些团队在保险库和审批对话框上投入了大量精力，却允许桌面更新器在夜间替换发起这些请求的程序。他们依然保留了人工参与，但人工批准的是一段没人评估过的代码所产生的行为。这算不上有意义的边界。

## 固定可执行文件身份，而不是友好的版本标签

确切的版本号必不可少，但单靠它还不足以准确识别本地客户端。`2.4.1` 这样的发布标签只能说明发布者打算发布什么。它不能证明你安装了哪些字节、是谁签署了它们、哪个运行时启动了它，或者某个扩展是否在启动后改变了它的行为。

一份可用的批准记录应在多个层面识别构件：

- 客户端名称和确切的发布字符串。
- 下载的安装程序或可执行文件的 SHA-256 摘要。
- 操作系统能够提供时，记录代码签名机构和捆绑标识符。
- 安装路径、运行时版本和扩展清单。
- 日期、审查人，以及该记录获准访问的服务或凭据范围。

区分「版本固定」和「构件固定」很重要。版本固定告诉安装器应该寻找哪个发布版本。构件固定则允许你拒绝与测试文件不同的文件。团队经常把这两个概念混为一谈，因为包管理器会用「pin」同时表示二者。后果很糟：他们批准了 `1.4.3`，却从被入侵的镜像或遭篡改的缓存中收到一个重新构建的 `1.4.3`，而本地没有任何测试能发现差异。

签名身份可以增加一道检查，但不能替代摘要。合法发布者也可能签署有问题的版本，而仅有摘要又无法说明文件是否来自你预期的发布者。只要客户端能够访问生产系统，两者都应记录。

不要为了文书工作而制造文书工作。这份记录需要在事件发生时回答一个问题：「哪个代码有权发送这个请求？」如果它只写着「编程代理」，就无法回答这个问题。

## 锁定依赖树不等于审查客户端

锁文件很有帮助，但它解决的问题比许多团队想象的更窄。包锁定文件会为某次安装选择依赖版本，却不会检查原生模块、验证每个安装后操作、阻止运行时从用户目录加载代码，也不能证明启动的可执行文件就是审查人检查过的构件。

对于通过语言生态分发的客户端，这一点尤其重要。一个看似只有一个程序的命令，实际可能由一个小型启动器、一个运行时、一棵包依赖树以及一个或多个下载的插件组成。只固定顶层包，可能会让信任决定中最大的一部分处于未控制状态。

OpenSSF SLSA 规范清楚地区分了来源信息和完整性。来源信息描述构件在哪里、如何构建。完整性检查确认构件没有发生变化。这两种声明都不代表程序适合获得凭据，但它们仍然重要，因为团队无法审查或复现一个不断变化的目标。

先弄清开发者启动代理时究竟运行了什么。在 macOS 上，应向 shell 查询，而不是相信 Dock 图标：

```sh
command -v agent-client
file "$(command -v agent-client)"
head -n 1 "$(command -v agent-client)"
```

当第一个结果是脚本时，`head` 命令就很重要。第一行如果是 `#!/usr/bin/env node`，说明 Node 运行时和包依赖树都属于执行路径。如果第一行把任务交给另一个启动器，就需要继续追踪。不要把 shell 别名、符号链接或引导脚本当成客户端来批准。

对于应用程序捆绑包，应检查真正的可执行文件和签名元数据：

```sh
APP="/Applications/Agent Client.app"
BIN="$APP/Contents/MacOS/Agent Client"
shasum -a 256 "$BIN"
codesign -dv --verbose=4 "$APP" 2>&1 | grep -E 'Identifier=|TeamIdentifier=|Authority='
spctl --assess --type execute --verbose=4 "$APP"
```

哈希输出通常形如 `digest  path`。`codesign` 输出一般会包含标识符、团队标识符和一条或多条机构信息。将这些输出与批准记录一起保存。`spctl` 会要求 macOS 按当前安全策略评估应用。它是有用的证据，但不代表这个应用就应该获得部署凭据的访问权限。

只写一句「锁文件已提交」的审查记录，留下了太多未解答的问题。保留锁文件，然后识别真正发起敏感操作的构件。

## 协议兼容性不能证明客户端可信

客户端可以正确使用代理协议，却仍然不适合访问敏感服务。协议兼容性回答的是两个程序能否交换消息。客户端信任回答的是：特定程序以特定状态启动后，是否可以请求需要凭据的操作。

Model Context Protocol 文档描述了客户端和服务器在初始化期间交换声明的能力。这种能力协商对互操作很有用，但它不是对客户端二进制文件的证明，也不是对已加载扩展的声明，更不保证客户端会保留用户意图。不要把成功握手当成身份检查。

当团队因为某个工具拥有已知的服务器名称，就允许任何支持 MCP 的本地客户端调用敏感工具时，这种混淆就出现了。MCP 服务器看到的是通过协议通道发送的请求，却可能不了解构造请求的程序。恶意客户端或只是未经审查的客户端，都可以使用与测试客户端相同的方法名。

将以下三类记录分开：

1. 你测试过的协议版本和能力。
2. 你批准的客户端构件、签名身份、运行时和扩展。
3. 该客户端可以请求的服务权限。

其中任何一项发生变化，都值得重新审查。协议升级可能改变默认设置或消息处理方式。客户端升级可能改变它调用哪些工具或何时重试。服务权限变化则可能让原本无害的重试变得具有破坏性。

还有一个常被弄错的区别：固定客户端并不会固定代理的指令。用户提示、仓库文件、工具描述、远程内容和模型输出，都可能影响一个已固定的客户端。版本固定只能减少因软件被替换而产生的意外，并不能让任意工具调用变得安全。对于后果难以轻易撤销的操作，应在前面设置人工批准环节。

## 测试权限路径，而不只是聊天窗口

候选客户端只有在计划开放的完整访问路径中都表现正确，才算通过审查。让它总结仓库或创建临时文件，只能证明用户界面正常，不能测试凭据注入、批准行为、重试、重定向、SSH 主机检查，也不能测试工具返回错误后客户端会做什么。

使用暂存服务或专门创建的范围受限测试凭据。该凭据应能展示目标操作，却不能修改生产数据、轮换共享密钥或访问无关账户。如果无法创建这样的凭据，说明服务的权限粒度对自主客户端来说过粗，需要单独讨论访问设计。

有意识地测试以下情况：

- 一次普通的获准请求，并收到预期响应。
- 一次超出凭据范围并被拒绝的请求。
- 触发重试行为的响应延迟或连接失败。
- 客户端使用 HTTP 时的重定向或端点变化。
- 客户端使用 SSH 时的主机密钥变化。

后两项经常能发现令人意外的行为。HTTP 客户端可能会跟随重定向，而重定向可能把请求发往另一台主机。凭据是否跟随，取决于客户端和身份验证实现。你需要观察结果，不能只从发布说明中推断。SSH 客户端必须把主机密钥不匹配视为停止条件，直到有人处理这个变化。代理不应因为任务要求继续，就自行决定陌生的主机密钥可以接受。

记录请求元数据，但不要记录秘密。对于 HTTP，测试端点可以记录方法、主机、路径、状态和选定的非敏感标头。确认网关添加了哪个授权标头，然后确认客户端不会在工具结果、错误消息或本地记录中把该标头值传回来。

对于破坏性端点，让暂存操作留下一个容易识别的标记。只返回 HTTP 200 的测试证明不了多少。你需要证明恰好执行了一次预期操作、重试后没有执行错误操作，并且审计记录能识别正确的会话。

## 将候选版本与获准客户端隔离

候选发布版本应拥有独立的安装、配置、缓存和扩展目录。共享这些目录会让测试构建访问与全新安装不同的状态，也会让回滚变得不可信。我调试过的最令人恼火的问题，往往来自一个「新」客户端悄悄加载旧插件，或复用了已认证的浏览器会话。

在 macOS 上，使用单独的用户账户能为严肃测试提供最干净的边界。单独账户会改变主目录、应用支持目录、缓存、登录项和许多凭据存储位置。对于较快的开发者测试，如果客户端说明了如何选择各个路径，并且你确认它确实遵守这些设置，那么使用单独目录也可以。

不要让两个版本指向同一个可写配置文件。客户端经常在启动时更新配置格式。新版本可能写入旧版本忽略或处理不当的字段。这样会把回滚变成部分迁移，而你恰恰需要让回滚过程平淡可靠。

对扩展的警惕程度应与客户端相同。记录扩展的确切版本、来源位置、可行时记录哈希，并确认客户端是否会自动获取更新。如果客户端会从用户级插件目录这类宽泛目录中发现扩展，候选测试开始时应让该目录保持为空，只添加测试所需的扩展。

干净的测试还会暴露一个不那么光鲜的问题：未记录的依赖。如果候选版本只有在继承获准环境中的环境变量、浏览器 Cookie、shell 函数或全局包缓存后才能工作，就要记录这些输入。每个隐藏依赖都会让日后的行为更难复现。

## 让推广成为小型、可重复的发布变更

推广应该用另一份经过审查的记录替换当前记录，而不是依靠某个人记得自己点击过哪个下载按钮。每次都写下并执行相同的顺序。这个顺序能让审查人使用共同的术语，也能为值班工程师提供合理的回滚路径。

1. 从发布者的正常发布渠道下载候选版本，记录来源、确切版本、哈希和签名者。
2. 将它安装到隔离的测试位置，记录它加载的运行时和扩展。
3. 使用暂存凭据运行权限路径测试，包括拒绝和失败情况。
4. 将观察到的请求、提示和日志与获准行为进行比较，调查每一个新增的权限请求。
5. 将获准候选版本安装到有特权的位置，保留之前的构件，并且只有在安装检查与记录一致后才更改访问权限。

最后的顺序很重要。不要先授予访问权限，再计划稍后检查已安装文件。如果安装检查失败，就让候选版本无法调用敏感服务。拒绝一个发布版本是正常结果，不是流程失败。

使用一份简单的记录，并将它放在运维笔记旁边。YAML 便于人在事件处理中阅读：

```yaml
client:
  name: agent-client
  version: "2.4.1"
  executable_sha256: "replace-with-verified-digest"
  signer_team_id: "record-the-observed-team-id"
  install_path: "/Applications/Agent Client.app"
  runtime: "native bundle"
review:
  tested_on: "2025-03-08"
  reviewer: "initials"
  extensions: []
access:
  environments: ["staging", "production-read"]
  forbidden_actions: ["secret-rotation", "deployment-write"]
rollback:
  previous_version: "2.4.0"
```

上面的值是占位符，不是可以复制来当作证据的模板。请根据实际检查结果填写，尤其不要在没有对下载文件进行哈希计算的情况下，直接粘贴发布公告中的摘要。

避免使用「该供应商发布的所有版本都允许」这类宽泛规则。它们很受欢迎，因为可以减少审查工作，也会抹去版本固定提供的精确控制。供应商身份可以作为审查输入，但不能成为未知代码使用生产凭据的长期授权。

## 自动更新和敏感访问不应共享同一边界

对于许多桌面应用，自动更新是合理的。当应用可以通过存储的凭据、SSH 密钥或特权服务账户触发外部操作时，风险就变了。在这种情况下，更新器可能在一个工作日到下一个工作日之间替换请求授权的程序。

有三种可行模式。最安全的做法是为有特权的安装禁用自动替换，手动推广发布版本。另一种做法是让开发者使用自动更新但无法访问敏感服务的副本，同时用固定版本的副本处理特权工作。第三种做法是把操作边界放在客户端之外，让每个新进程在执行任何重要操作前都必须获得新的授权。

第三种模式可以限制意外更新造成的损害，但不要夸大它的作用。只有在新的批准卡片能够有意义地识别进程时，新的批准才有用。每天看到通用的客户端名称就点击通过，会训练人们批准任何出现的东西。应显示签名机构、进程路径或其他身份证据，让审查人可以与获准记录进行比较。

不要因为更新后的客户端来自操作系统应用商店，或因为 macOS 认为它已签名，就默默允许它。那些机制可以降低部分供应链风险，但不能说明新行为是否符合你的访问规则。是否授予组织凭据，仍然由你的组织负责。

版本固定还需要定期到期审查。永久固定最终会变成未打补丁的固定版本。根据客户端、它能够访问的服务以及供应商的安全公告设定审查周期。如果行为没有变化，审查不必制造紧张气氛，但必须经过有意识的决定。

## 按操作授权可以捕捉版本固定无法解决的行为

即使客户端已固定，也可能通过仓库、问题描述、网页或工具结果收到恶意指令。它还可能在你为合法工作授予的权限范围内做出错误选择。人工授权应重点关注具有实质影响的操作，例如写入生产数据、修改基础设施、向新目的地传输数据，或向敏感主机打开 SSH 会话。

如果批准提示要求人们批准技术噪声，就会失效。每个无害读取操作都弹出提示，会训练用户直接点击。请求已经隐藏目标后才出现的提示，也没有多少帮助。在网关发送请求前，显示操作、目的地、方法和凭据身份。选择数量要足够少，确保人确实能评估它们。

一种实用的划分方式是：低风险读取可以通过会话批准，能够改变状态或暴露数据的操作则必须获得明确批准。边界取决于服务。读取源代码仓库可能很普通，读取客户数据库却可能意味着披露数据。不要只按 HTTP 方法分类。

Sallyport 使用固定的决策阶梯：保险库锁定时拒绝所有操作，新代理进程默认需要会话授权，选定的凭据条目可以要求每次使用都批准。这种窄模型是有意设计的。通用策略语言会提供更多旋钮，也会给团队更多机会写出自己并不理解的例外。

让撤销立即生效。当候选客户端行为异常时，应能在开始漫长调查前停止当前访问会话。之后可能还需要撤销服务凭据，但那是一个会破坏无关工作的粗粒度响应。先撤销会话，让正在运行的进程失去访问能力。

## 审计记录必须把操作连接到获准运行

只写着「API 调用成功」的操作日志，不足以支持代理工作。你需要把调用连接到本地进程、批准决定、所用凭据以及当时有效的客户端记录。否则事件审查就会沦为在终端、浏览器历史记录和服务日志之间比对时间戳。

为每个代理进程保留会话记录。记录进程启动时间、用户如何授权、观察到的身份，以及进程结束或被撤销的时间。为单个操作保留独立的活动记录。该记录应标识目的地、操作、结果和会话关联，同时省略秘密材料。

防篡改证据很重要，因为代理可以在很短时间内生成大量工作，而本地日志在出错后很容易被修改。哈希链能让人在验证序列时发现删除或篡改。它不能阻止受入侵的机器执行操作，但能为调查人员发现被改写的历史提供更好的依据。

Sallyport 将 Sessions 和 Activity 日志都投影到同一份加密哈希链审计日志中，`sp audit verify` 可以在没有保险库密钥的情况下，对密文离线检查哈希链。当审查记录的人不应获得操作所用秘密时，这种设计很有用。

在推广期间测试审计路径。批准一个暂存会话，执行一次获准请求和一次被拒请求，撤销会话，然后确认记录识别出全部四个事件。如果记录无法显示被拒请求或撤销操作，那么你缺少的正是行为出错时最重要的信息。

## 周边系统保持可变时，版本固定也会失效

即使客户端经过仔细哈希，它仍然运行在一台可能发生变化的机器上。操作系统、运行时、shell 环境、DNS 配置、代理设置、证书存储、本地辅助二进制文件和开发者安装的扩展，都会影响请求如何离开机器。版本固定只是控制链中的一环，不是可以贴在危险设置上的标签。

先检查那些无需改变客户端摘要，就能改变特权操作的部分。检查用于选择端点、代理行为或凭据位置的环境变量。确认客户端调用的是哪个 SSH 二进制文件或辅助程序。记录预期的 known-hosts 位置，并确认测试会拒绝陌生服务器身份。检查配置是否允许将任意本地命令作为工具执行。

不要试图用策略引擎解决所有不确定性。大多数团队需要更少的活动部件，而不是一套凌晨两点没人能解释的大型规则集。目的地短允许列表、范围受限的凭据、敏感调用的明确批准以及已知客户端构件，比一大堆未经测试的条件更有用。

运维习惯其实很简单：客户端发生变化时，暂停访问，直到新构件获得批准。保留旧的获准副本，针对真实权限路径测试替代版本，并记录证据。这种纪律没有自主演示那么令人兴奋，却能防止后台更新变成未经审查的生产变更。
