# AI 代理操作网关与 MITM 代理：控制点

AI 代理操作网关和中间人代理都可以位于代理与外部服务之间。表面上的相似性经常导致错误的架构决策。一种模型代表代理执行经过身份验证的操作，同时持有凭据。另一种模型转发或拦截代理已经决定发出的流量。

这种差异决定了你可以在哪里拒绝请求、代理能够窃取什么、审批究竟意味着什么，以及审计记录描述的是一次有意执行的操作，还是从数据包流量中重建出的过程。如果要求是「代理绝不能持有生产凭据」，代理通常不是合适的主要边界。它仍然可能有自己的用途，但无法修复已经存在于客户端进程中的密钥问题。

## 网关执行命名操作，代理处理连接

**AI 代理操作网关**接收执行外部操作的请求，选择存储的凭据，执行操作，然后返回结果。代理请求完成工作，但不会收到用于在其他地方重复执行的 bearer token、密码或私钥。

正向代理接收来自客户端的网络连接，并将其转发到目标地址。请求仍然由客户端掌握。对于普通 HTTP，客户端会把 HTTP 请求发送给代理，因此代理可以读取方法、URL、请求头和请求体。对于 HTTPS，常见情况是 CONNECT 隧道：代理与目标建立 TCP 连接，并在两个方向转发加密字节。

MITM 代理会改变 HTTPS 的处理方式。它终止客户端的 TLS 连接，检查或修改解密后的 HTTP 消息，然后与上游服务器建立另一条 TLS 连接。客户端必须信任由代理控制的证书颁发机构，因为代理会为目标主机出示证书。

这两种模型回答的是不同的问题：

- 代理关注流量可以去哪里，以及在进行拦截时流量表达了什么。
- 操作网关关注自己是否会执行某个经过身份验证的具体操作。
- 代理可以在客户端拥有的请求中添加或删除内容。
- 网关可以保留凭据，并自行构造经过身份验证的请求。

最后一点不是措辞差异，而是安全边界。它决定了被攻破的代理能否拿着凭据，在另一台机器、另一个时间、通过另一条路径调用服务。

假设代理被要求创建一次部署。在代理设计中，代理通常会准备 `POST /deployments`，选择 JSON 请求体，然后发送请求。代理服务器可以允许、拒绝、记录请求，或注入 `Authorization` 请求头。在执行器设计中，代理会调用带有参数的 `create_deployment` 操作。网关解析配置好的目标和密钥，执行 HTTP 调用，并返回该操作允许返回的状态和响应体。

执行器仍然需要谨慎设计。定义一个「发送任意 HTTP 请求」这样的通用操作，很容易重新构造出代理模型。但凭据边界仍然不同：网关持有凭据，代理不持有。

## TLS 会把检查问题变成证书颁发机构问题

HTTPS 流量经过普通正向代理，并不意味着代理就能看到内容。这个误解非常常见，以至于一些团队基于错误假设建立控制措施，直到客户端第一次使用 CONNECT 才暴露问题。

RFC 9110 将 CONNECT 描述为建立到目标主机和端口的隧道请求。隧道建立后，代理只转发字节。TLS 握手发生在隧道内部，由客户端和源站服务器完成。代理可以记录目标主机、端口、时间、字节数和连接结果，但无法读取 `POST /v1/...` 或加密请求头中的 API 密钥。

要检查 HTTPS，拦截代理必须成为客户端的 TLS 端点。RFC 8446 规定的 TLS 1.3 要求客户端验证证书链和主机名。只有当客户端信任一个可以为被拦截网站签发证书的证书颁发机构时，代理才能通过这种验证。

这会带来真实的运维工作：

1. 在代理使用的每台机器或运行时中安装并保护私有 CA。
2. 让语言运行时、包管理器、命令行工具、容器和嵌入式客户端都信任它。
3. 处理固定公有证书，或使用独立证书存储的客户端。
4. 代理收到解密后的请求体和凭据后，保护这些数据。
5. 解释为什么一个原本不信任未知证书颁发者的进程，现在却信任你的拦截 CA。

在受管理的企业环境中，这可能是合适的做法，但它绝不是一个小实现细节。解密所有代理流量的代理，会成为经过它的所有密钥、请求体和响应的高价值存放点。

持有凭据的执行器不需要冒充每个目标站点的证书，才能观察一次操作。它会作为该操作的 HTTP 客户端。它与目标建立正常的 TLS 连接，像任何客户端一样验证目标的公有证书，并在执行时加入存储的凭据。

这并不会消除 TLS 问题。网关必须正确验证证书，并保护密钥存储。但为了看见操作内容，它不需要把私有拦截 CA 分发给代理进程。

有一个很实用的检查方法：如果设计文档写着代理会检查 HTTPS 请求，就问清楚客户端在哪里信任拦截 CA。如果没有人能明确回答，代理看到的就只有隧道，或者系统会在工具正确验证证书时失败。

## 凭据托管会改变代理被攻破后的损害范围

真正有用的安全边界不是「代理通过我们的设备发出网络请求」，而是代理能否获得可以重复使用的权限。

Bearer token 很能说明问题。API 服务器通常会接受任何能够连接它的进程提供的 bearer token。如果代理收到了这个字符串，它就可以把令牌写入文件、放进工具输出、传给其他端点，或在审批会话结束后继续使用。事后再对日志做脱敏并没有帮助，因为凭据已经离开了预定边界。

如果代理自己注入令牌，它可以减少暴露。但这种设计仍然需要仔细审查。代理控制着到达代理服务器的请求。除非代理理解 API 语义并可靠地执行限制，否则代理可能利用注入的令牌访问令牌允许的任何端点、方法或请求体。

例如，一条「为 `api.example.internal` 注入这个令牌」的代理规则，会让请求进程获得该主机上的全部有效权限。代理看不到令牌字符串，但可以要求代理服务器执行破坏性调用。这对权限范围很窄的服务账户也许可以接受，但它与允许一个命名的部署操作、同时拒绝其他管理端点，是两种不同的控制。

操作网关可以把凭据绑定到使用它的操作路径。代理提供的是参数，而不是授权材料。网关可以在创建网络请求前，向人展示凭据身份和计划执行的操作。如果请求不符合配置好的通道，它还可以直接拒绝，而不尝试连接。

这里需要区分**不泄露密钥**和**限制权限范围**。

不泄露密钥，意味着代理永远看不到原始凭据。注入请求头的代理可以做到这一点。

限制权限范围，意味着代理不能把一个允许的集成变成通用的、经过身份验证的传输通道。这需要操作定义、目标处理、参数验证，以及在产生副作用前执行的控制。通用代理不会自动提供这些能力。

SSH 让这个问题更加明显。SSH 公钥认证会在协议交换过程中证明客户端持有私钥。如果代理拥有私钥，它就能在任何接受该密钥的地方完成身份验证。如果代理只拥有一条经过网关转发的 SSH 命令，那么网关或 helper 必须在不导出密钥的情况下完成认证。

RFC 4253 描述了 SSH 传输协议及其认证架构。实际结论很简单：你不能安全地把「SSH 私钥请求头」注入客户端请求。要么进程持有签名权限，要么由另一个进程代为建立经过身份验证的连接。

## 授权必须发生在外部副作用之前

控制点只有在目标行为发生前介入才有价值。上游服务器接受请求后再记录它，可以提供证据，但不能提供否决权。

MITM 代理通常会根据主机、URL、方法、请求头、请求体、客户端身份或目标类别，提供策略和审批系统。当代理能看到解密流量，并且理解应用协议时，这些控制可以很强。但它们也会变成规则维护问题。有人必须决定 `/projects/123/members` 是否安全，JSON 请求体是否会把普通更新变成权限授予，以及经过编码的请求能否绕过字符串匹配。

我见过团队从三条代理规则开始，最后维护一套没有文档的例外语言。问题不在于规则本身一定不好，而在于把代理的任意请求格式当成稳定且含义明确的策略界面。

操作网关可以使用更小的词汇表。授权决策可以参考调用进程、配置好的凭据、请求的操作和提供的参数。人不需要检查一条不透明的 `curl` 命令，再猜测下游会附加哪个存储密钥。

Sallyport 采用固定的决策阶梯，而不是策略语言。保险库锁定时会拒绝所有操作。默认情况下，新代理进程需要会话授权，审批界面会显示该进程的代码签名主体。单个凭据也可以设置为每次使用都需要批准。这些控制有意保持粗粒度，但都发生在应用执行 HTTP 或 SSH 操作之前。

这种方法有一个需要明确说明的限制：固定的审批模型无法表达每个组织的条件规则。如果你需要「只有在维护窗口期间，并且工单字段具有指定值时才允许写入」这样的规则，就需要能够评估和维护这类规则的系统。不要把一个简单的操作网关伪装成通用策略引擎。

控制集保持简单的好处，是操作员可以预测结果。保险库要么锁定，要么解锁。进程运行要么通过审批，要么没有。凭据要么每次询问，要么不询问。当没人能解释一次调用为何通过时，安全控制在实际环境中就会失效。

## 请求可见性和操作权限是两种不同属性

团队经常要求「完整可见性」，但实际需要的可能是两件事：记录哪个代理发起了工作，以及记录每次外部操作。数据包级可见性有助于调试，却不能替代操作记录。

代理日志可能包含源地址、目标地址、TLS 详情、拦截成功时的 HTTP 字段以及原始字节。这些都是有用的取证材料，但也会带来复杂的身份问题。只有在环境提供并保留该身份时，连接才能告诉你哪个进程发起了连接。请求能告诉你到达代理的内容，却不一定能告诉你是哪条模型指令或哪个代理会话造成了它。

操作网关从操作边界开始。它可以记录请求操作的代理运行，以及实际执行的单次调用。这些记录回答的是不同问题：

- 哪个获得授权的代理进程拥有有效会话？
- 该会话请求执行了哪个外部操作？
- 执行器使用了哪个凭据或通道？
- 返回了什么结果，或调用在哪里失败？

不要把审计轨迹和访问控制混为一谈。详细日志无法阻止代理删除数据，如果你已经授予它这项操作。它可以让事后修改更容易被发现，也能让事故调查不必过度依赖服务自身的数据库。

Sallyport 从同一份加密、哈希链式审计日志生成 Sessions 和 Activity 日志。`sp audit verify` 命令可以离线检查密文上的哈希链，不需要保险库密钥。这种设计把验证能力与解密密钥的能力分开，适合需要验证历史、但不应获得凭据访问权限的调查人员。

具体的验证流程应当有一个人们熟悉的输出形式。例如：

```text
$ sp audit verify
Verifying audit chain...
Entries checked: 184
Chain status: valid
```

确切数字会变化。重要的是，任何被修改、删除或重新排序的记录都应导致验证失败，而不是悄悄产生一段更短的历史记录。如果需要防止整台机器被删除，请把加密审计数据保留在生成它的机器之外。哈希链可以检测你保留下来的记录是否被篡改，但无法证明攻击者连同机器一起删除的文件曾经存在。

## HTTP 和 SSH 暴露出不同的中介限制

HTTP 看起来很简单，因为它有请求头、URL 和方法。这种表象容易让团队把所有集成都当成注入请求头的问题。

对于使用 HTTP bearer token 的 API，执行器可以保存一条凭据记录，说明如何认证以及凭据适用于哪里。代理请求操作时，执行器自行添加 bearer 请求头。对于自定义请求头方案，它会添加配置好的请求头，但不会把请求头的值返回给代理。Basic 认证遵循同样的托管规则，不过执行器必须把用户名和密码当作密钥，而不是普通配置文本。

实际设计的关键在于区分输入。代理可以提供这样的请求数据：

```json
{
  "method": "POST",
  "path": "/repos/acme/widget/deployments",
  "body": {
    "environment": "staging",
    "revision": "7d3c1a"
  }
}
```

代理不应提供这样的内容：

```json
{
  "authorization": "Bearer token-value-goes-here"
}
```

这种分离可以避免常见故障：工具模式公开了密钥字段，模型把值放入其中，随后该值出现在追踪记录、终端滚屏、测试夹具或复制出来的对话文本中。把令牌当成工具参数是设计错误，即使界面会在调用后隐藏它也一样。

HTTP 中介仍然需要边界。如果网关接受任意 URL，代理就可以通过带凭据的路径访问内部服务、云元数据端点或无关主机。如果它接受任意请求头，代理可能尝试请求走私，或覆盖认证语义。如果它接受任意请求体，就必须接受 API 本身会变成策略语言这一事实。

SSH 的边界不同。一个操作可能需要运行远程命令、复制文件或查询主机。网关需要目标地址以及由自己保留的私钥。无状态 helper 可以建立 SSH 连接，并返回标准输出、标准错误和退出状态，而不把密钥放入代理的环境中。

命令边界很重要。这样的请求：

```text
host: build-host
command: git rev-parse HEAD
```

比代理在本地 shell 中运行命令的审查范围更窄，后者可以访问 `~/.ssh`、任意代理设置和不受限制的命令行。它仍然是经过身份验证的远程命令。如果凭据可以在远程运行 `rm -rf`，网关不会因为改变传输方式就让它变安全。应限制远程账户的权限，并为实际执行的任务选择合适的凭据。

代理可以通过 TCP 隧道传输 SSH，但仅仅转发 22 端口并不能检查 SSH 命令。要检查 SSH 协议内容，它必须充当 SSH 端点，再与上游建立另一条 SSH 连接，同时承担由此产生的主机信任、身份验证、记录和兼容性责任。把它称为代理，并不会减少工程工作。

## 当代理掌握正确的层次时，它仍然有用

反对把 MITM 代理作为代理操作网关，并不等于反对代理。只要位于真正需要的层次，代理可以很好地解决多个问题。

当你需要限制运行时可以访问的网络或主机名、强制流量经过指定路径、控制普通工具的出站访问，或收集连接元数据时，可以使用正向代理或出站网关。即使代理不了解你的操作模型，这些控制也能阻止它访问未经批准的主机。

当你拥有客户端、能够管理信任 CA、需要诊断或约束大量传统 HTTP 客户端的行为，并且愿意负责处理解密流量时，可以使用 MITM 代理。安全测试环境和受管理的设备群通常满足这些条件。

当要求与自主代理的身份和权限直接相关时，应使用操作网关：代理必须请求外部操作，不能持有凭据，并且必须留下可审查的操作记录，说明执行器完成了什么。

许多严肃部署会组合使用它们。代理运行时只获得受限的出站访问，因此无法直接发起任意调用。操作网关获得访问已批准外部服务所需的狭窄网络权限。网关负责凭据和审批，网络层负责阻止绕过路径。

不要因为代理已经位于网络路径中，就把所有控制都放进代理。这样通常会迫使你把应用层授权塞进 URL 模式，并把密钥处理交给原本只为转发流量而设计的拦截服务。部署图看起来很整齐，所以这种选择很受欢迎，但实际操作边界会变得更糟。

## 一个看似合理的代理设计会在交接处失败

假设一个运行在开发环境中的编程代理，需要查询问题跟踪系统、创建部署，并通过 SSH 检查构建主机。团队安装 HTTPS 代理，并配置如下环境变量：

```text
HTTPS_PROXY=http://proxy.internal:8080
HTTP_PROXY=http://proxy.internal:8080
```

代理会为问题跟踪系统注入 API 令牌。团队认为令牌受到了保护，因为代理从未从配置文件中读取它。

现在，代理向同一主机上的管理端点发送请求。代理服务器看到的是已允许的主机名，于是注入同一个令牌。如果规则不了解端点语义，它就通过间接接口把令牌的全部权限交给了代理。

团队尝试用路径允许列表修复问题。很快，他们需要为分页、附件上传端点、重定向、其他 API 版本，以及先写入后读取的工作流添加例外。代理现在承载着一套应用策略，而服务 API 每次变化，这套策略也必须变化。

与此同时，一个工具不遵守 `HTTPS_PROXY`。另一个工具使用独立的证书存储，导致 TLS 拦截失败。第三个工具运行在使用不同 CA 包的容器中。有人为了让工作继续而添加绕过规则。这个绕过路径，正是受到提示注入的代理或被攻破的依赖之后最可能选择的路径。

最后 SSH 暴露了这种不匹配。代理无法把 SSH 私钥注入隧道。团队「暂时」把密钥挂载到代理环境中，或启动一个能够访问本地 SSH agent 的进程。此时，系统已经失去了最初想要保留的属性。

操作执行设计改变了交接方式。代理通过本地 stdio 连接调用 MCP 工具。执行器持有 API 凭据或 SSH 密钥，建立外部连接，并返回结果。MCP 负责传输工具请求，但不会让代理获得秘密的自由通行权。Model Context Protocol 规范定义了工具交互边界，但凭据托管仍然由实现者负责。

这种设计并不能消除提示注入。恶意指令仍然可能诱导代理请求它获准执行的有害操作。它能把失败范围限制在网关公开的权限之内，为操作员提供批准或拒绝的控制点，也避免每次成功的工具调用都变成外泄可重复使用密钥的机会。

## 根据你拒绝交出的权限选择架构

先写出你必须实现的那句话。如果是「进程只能访问获准的目标」，就把网络控制放在路径中。如果是「进程绝不能拥有这个 API 密钥或 SSH 私钥」，就让另一个进程执行经过身份验证的操作。如果是「人必须批准凭据的每次使用」，就确保审批发生在执行器调用远程服务之前。

然后用绕过路径检验这个声明。代理能读取带有令牌的环境变量吗？能访问凭据文件、本地 SSH agent、浏览器会话、云元数据服务或不受限制的出站网络吗？它能否要求一个通用 HTTP 工具用同一个注入凭据访问另一条路径？TLS 拦截失败时，操作员是否会关闭证书验证或创建直连路径？

一次简短而有用的评审应回答四个问题：

1. 每个密钥在内存中由哪个进程持有？
2. 哪个进程创建经过身份验证的连接？
3. 人可以在哪里、在远程服务看到调用前拒绝它？
4. 哪条记录把一次特定的代理运行与已完成的操作关联起来？

如果前两个问题的答案都是「代理」，那么任何代理规则都无法改变基本风险。如果答案是「代理服务器」，就要决定是否准备好运行 TLS 拦截和应用策略。如果答案是「持有凭据的执行器」，就要确保它提供的操作范围足够窄，并且代理无法绕过它。

Sallyport 在 macOS 上为最后一种安排而设计：它的 `sp mcp` shim 让支持 MCP 的代理请求 HTTP 和 SSH 操作，同时由应用保留密钥并执行这些操作。它不是 MITM 代理，也不应被描述成 MITM 代理。

第一个实现任务通常并不引人注目：从代理运行时中移除直接凭据。在这件事实现之前，审批和流量日志都只是围绕着一个仍然持有密钥的进程设置的护栏。
