# 代理环境变量：审计智能体 API 流量

代理环境变量是可执行的路由指令，不是什么无害的 shell 偏好设置。如果 AI 编程智能体继承了 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 或 `NO_PROXY`，一个看似直接发往内部 API 的请求，可能在智能体开展任何有效工作之前，就沿着另一条网络路径发出。

我见过一些团队花了好几天审查令牌权限范围和端点允许列表，最后却发现智能体运行器继承了开发者 shell 中用于本地调试的代理。凭据有效，API 客户端也完全按照配置运行，但流量还是去了没人预期的地方。在授予智能体访问内部服务的权限之前，先审计进程环境。

## 代理变量改变的是路由，不只是连接设置

代理变量会告诉支持它的客户端，把请求交给中间代理。对于普通 HTTP，客户端通常会把完整的目标 URL 发给代理。对于 HTTPS，客户端通常会请求代理为目标主机打开一个 `CONNECT` 隧道，然后通过这条隧道执行 TLS。

这个区别很重要，因为即使代理无法解密 HTTPS，它仍会影响可用性、目标控制、DNS 行为和可观测性。代理可以拒绝连接，在 TCP 层重定向连接，记录请求的主机和端口，或者成为智能体访问某项服务的唯一通道。

应将这些变量视为智能体对外发起连接权限的一部分：

- `HTTP_PROXY` 和 `http_proxy` 通常影响 `http://` URL。
- `HTTPS_PROXY` 和 `https_proxy` 通常影响 `https://` URL。
- 支持它们的客户端通常会把 `ALL_PROXY` 和 `all_proxy` 作为后备设置。
- `NO_PROXY` 和 `no_proxy` 通常会让指定目标绕过代理。

“通常”这个词很关键。环境变量是一种约定，不是每个运行时都以完全相同方式实现的网络标准。智能体可能调用命令行客户端、使用某种语言的 HTTP 库、运行软件包管理器，或启动辅助进程。每一层都可能独立决定是否使用代理。

变量未设置，也不能证明请求一定会直连。客户端可能读取配置文件、使用系统代理设置、遵循 PAC 文件，或者显式调用本地中继。本文聚焦环境变量，是因为它们很容易被继承，在拥挤的进程环境中很难发现，而且常常被当作临时设置，直到它们变成永久配置。

## 启动器决定智能体继承什么

智能体只能接收到自身进程环境中已有的变量，或由父进程向下传递的变量。你检查 `env` 的终端，可能与实际运行智能体的进程毫无关系。

在 macOS 上尤其容易出错。从交互式 shell 启动的进程会继承 shell 中导出的变量。由 Finder 启动的图形应用，或通过 `launchd` 启动的服务，则遵循不同的继承路径。IDE 可能为集成终端启动一种环境，为扩展宿主启动另一种环境。昨天启动的后台智能体，也可能在 shell 变量消失很久后继续保留旧的代理值。

在修改设置前，先绘制执行链。明确回答四个问题：

1. 哪个进程启动了智能体？
2. 智能体是否会启动 shell、软件包工具、测试运行器或远程辅助进程？
3. 其中哪些进程会发起 HTTP 请求？
4. 每个环境变量由哪个启动点提供？

除非你能指出父进程，并从该终端复现运行过程，否则不要接受“智能体在我的终端里运行”这种说法。智能体可能是编辑器、任务运行器或使用已保存环境的自动化服务的子进程。

父进程注入的代理变量会传递给所有子进程，除非子进程主动移除它。这就是为什么一行 shell 导出命令会悄悄改变软件包安装、源代码管理辅助工具、云 CLI、浏览器自动化和测试装置发出的请求。受影响的请求可能根本不是你启动智能体时想到的那一个。

## 不同客户端对代理变量的解释不同

`HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY` 没有统一的解释方式。任何假定某个客户端的行为适用于另一个客户端的安全审查，都不完整。

curl 文档中有一个很多人会忽略的重要例外：curl 只接受小写的 `http_proxy` 来配置 HTTP 代理。文档解释说，这样可以避免 CGI 问题，因为传入的 `Proxy:` 请求头可能会变成 `HTTP_PROXY` 环境变量。curl 会接受其他几个代理变量的大写形式，但 HTTP 这一例外是有意设计的。

Go 将 `http.ProxyFromEnvironment` 说明为一个读取 `HTTP_PROXY`、`HTTPS_PROXY` 和 `NO_PROXY` 以及其小写替代形式的函数。它的行为也包含 CGI 防护：当 CGI 环境中存在 `REQUEST_METHOD` 时，Go 不会使用 `HTTP_PROXY`，因为它可能来自请求头。这个保护并不意味着 Go 进程天然安全。仍需审查 `HTTPS_PROXY`、小写形式、显式传输设置以及非 CGI 环境。

许多 JavaScript 应用会让情况更复杂。Node.js 运行时内置的 HTTP API 历来没有统一采用一种环境代理策略。应用及其依赖通常会自行添加代理支持。同一次智能体运行中，一个命令可能遵守 `HTTPS_PROXY`，另一个命令可能忽略它，第三个命令则可能读取自定义选项。

不要用一张记录各种运行时传闻的表格来代替验证。找出每个能够联网的可执行文件并进行测试。记录可执行文件版本、调用方式、目标 URL、相关变量，以及它是直接连接还是通过预期代理连接。将结果放入智能体部署记录中，因为依赖更新可能改变行为。

一个经常被混淆的区别是：了解代理不等于强制使用代理。支持代理变量的客户端在变量存在时可以被引导到代理；但当变量缺失、格式错误、被 `NO_PROXY` 绕过，或辅助进程忽略变量时，它仍可能直接连接。如果你需要阻止直接外连，应在网络边界执行这一策略，而不是寄希望于每个库都会读取环境变量。

## 对内部名称明确测试 NO_PROXY

`NO_PROXY` 是一个绕过列表，错误的绕过列表可能让内部流量经过代理，也可能让你原本希望接受检查的流量直接发出。不能假定其中任何一种结果是安全的。

大多数实现接受以逗号分隔的条目。除此之外，各实现就会出现差异。客户端可能以不同方式处理 `registry.corp.example`、`.corp.example`、`corp.example`、`10.20.0.0/16`、`10.20.30.40`、`localhost` 和 `*`。某个客户端中有效的后缀匹配，在另一个客户端中可能范围过大，或者完全不起作用。带端口的条目也没有统一行为。

在测试证明其含义之前，避免使用范围过大的条目。裸域名后缀可能会让你没有打算绕过的主机也被豁免。星号可能比审查者预期的范围更广泛地关闭代理。CIDR 支持在存在时很有用，但不能假定它具有可移植性。明确的内部主机名看起来普通，但在这里，普通反而有用。

先列出一小组必须直连的命名服务，例如内部源代码主机、制品仓库和服务发现端点。只有在确认客户端会按预期匹配后缀，并且后缀下的每台主机都应该使用相同路由时，才添加域名后缀。

还要区分主机名匹配和名称解析。客户端通常会先决定是否适用 `NO_PROXY`，然后才建立连接。如果列表中包含主机名，即使该主机解析到预期范围之外的地址，匹配也可能成功。如果列表中包含 IP 网段，客户端可能需要先解析名称才能做出判断。具体顺序由实现决定。

绕过测试需要使用你能控制、并能在日志中识别的目标。不要使用生产 API，再根据 200 响应推断结果。内部测试端点应该报告远端地址，或在访问日志中输出请求标识符。然后比较存在绕过条目和不存在绕过条目时发出的请求。你需要的是连接路径的证据，而不是根据应用输出做出的猜测。

## HTTPS 会隐藏内容，但代理仍能获得有意义的信息

HTTPS `CONNECT` 隧道通常会保护请求头和请求正文，使其不被传统正向代理看到。但这并不意味着代理无关紧要。代理能看到 `CONNECT` 请求中指定的主机和端口、连接时间、字节数，以及通常的源地址。根据客户端和网络情况，相关 DNS 流量可能还会泄露更多信息。

如果客户端信任代理用于 TLS 拦截的证书颁发机构，代理就能读取解密后的 API 凭据。这种情况会出现在一些受管理的企业网络和调试环境中。是否信任本地根证书是安全边界决策，不是便利设置。如果智能体进程信任该根证书，代理操作者就能检查拦截策略适用的任何主机上的流量。

明文 HTTP 更糟。正向代理可以收到完整 URL 和请求头，其中可能包括 bearer token 或基本身份验证信息。不要因为“网络是私有的”，就允许智能体使用明文 HTTP 访问经过身份验证的内部 API。代理环境变量只是让私有网络假设失效的多种方式之一。

另一个不太明显的问题是代理 URL 中嵌入了凭据，例如 `http://user:password@proxy.example:8080`。这个值可能出现在诊断输出、崩溃报告、 shell 历史记录、进程检查结果或记录环境变量的日志中。代理身份验证应使用适合所在环境的受管机制，安全审查也应将代理凭据本身视为机密。

如果你无法确认代理操作者、代理监听地址，以及是否可能进行 TLS 拦截，就不要让特权智能体流量经过它。这不是多疑。代理已经成为自动化进程与敏感服务之间路径的一部分。

## 在不倾倒机密的情况下审计运行中的进程

先清点变量名称和代理端点，同时避免完整导出环境。在可能启动智能体的 shell 中运行：

```sh
env | grep -Ei '(^|_)(http|https|all|no)_proxy='
```

输出应类似：

```text
HTTPS_PROXY=http://127.0.0.1:8888
NO_PROXY=localhost,127.0.0.1,registry.corp.example
```

如果代理 URL 中包含用户信息，不要把原始输出粘贴到工单中。删除凭据后，记录协议、主机和端口。回环地址并不自动代表安全。本地代理经常属于合法调试工具，但恶意软件和不需要的软件也可能监听回环地址。确认哪个进程拥有该监听端口。

在 macOS 上，可以这样检查监听端口：

```sh
lsof -nP -iTCP:8888 -sTCP:LISTEN
```

正常结果会显示命令和进程 ID。如果没有预期进程拥有该端口，就先停下来调查。不要因为地址以 `127.0.0.1` 开头，就让智能体把凭据发送给这个监听程序。

接下来检查实际的智能体进程。`ps` 可以在 macOS 上显示进程环境，但也可能暴露无关的机密。将访问限制在机器所有者或管理员，收集必要的信息，不要把结果粘贴到聊天或共享日志中。

```sh
ps eww -p "$AGENT_PID" | tr ' ' '\n' | grep -Ei '(^|_)(http|https|all|no)_proxy='
```

将 `AGENT_PID` 设置为运行中智能体的进程 ID。如果代理凭据存在，这条命令仍可能打印出来，因此应在可信终端上运行，并在保存证据前完成脱敏。如果结果与 shell 清单不同，说明父进程注入或移除了变量。

对于由 `launchd` 启动的进程，应检查任务定义和启动器，而不是依赖终端。`launchctl getenv HTTPS_PROXY` 可以显示当前 launchd 域中的变量，但那里没有变量，并不能证明某个特定任务没有代理。任务可以定义自己的环境，包装脚本也可以在启动智能体前导出变量。

## 用无害请求证明路由

配置审查告诉你应该发生什么，受控请求告诉你实际发生了什么。两者都需要。

创建或使用一个不会包含敏感信息的端点，让它记录对端地址和请求路径。然后用详细连接输出发起请求。curl 很有用，因为它会显示自己是否连接到代理，以及是否发送了 `CONNECT` 请求。

```sh
HTTPS_PROXY=http://127.0.0.1:8888 \
NO_PROXY= \
curl -v --connect-timeout 5 https://probe.corp.example/agent-proxy-check
```

curl 通过代理处理 HTTPS 时，详细输出通常会包含类似这样的行：

```text
* Uses proxy env variable HTTPS_PROXY == 'http://127.0.0.1:8888'
* Establish HTTP proxy tunnel to probe.corp.example:443
> CONNECT probe.corp.example:443 HTTP/1.1
```

然后有意进行直连对比：

```sh
HTTPS_PROXY=http://127.0.0.1:8888 \
NO_PROXY=probe.corp.example \
curl -v --connect-timeout 5 https://probe.corp.example/agent-proxy-check
```

直连运行应显示连接到目标，而不是向代理发送 `CONNECT` 请求。还要在探测服务的日志中确认结果。如果 curl 表示绕过了代理，但服务器看到的源地址不符合预期，或请求失败，就分别检查 DNS、路由和任何透明网络代理。

这只能证明 curl 的行为，不能证明智能体的行为。通过智能体的确切执行路径重复测试。如果它调用脚本，就运行该脚本。如果它通过依赖库调用 API，就用相同配置临时加入探测 URL。如果它启动辅助进程，就收集辅助进程的路由证据。测试另一个客户端只能作为线索。

不要使用公共的“我的 IP 是什么”服务来做这项工作。这样会把内部路由审查变成不必要的外部泄露，而且无法告诉你应用了哪条内部代理策略。

## 干净的启动环境胜过永久 shell 导出

不要把企业代理导出配置放进通用 shell 配置文件，然后期待自主工具能够安全地做例外处理。全局导出之所以常见，是因为它能让一个被阻止的命令暂时运行起来，但它也会传播给所有你之后忘记存在的子进程。

为智能体使用明确的启动器。从已知环境开始，只传递本次运行需要的变量，并在启动命令或包装脚本中清楚显示代理的使用方式。在 Unix shell 中，`env -i` 会清除继承的变量，因此必须恢复程序运行所需的基本设置：

```sh
env -i \
PATH="$PATH" \
HOME="$HOME" \
LANG="${LANG:-en_US.UTF-8}" \
NO_PROXY="localhost,127.0.0.1,registry.corp.example" \
agent-command
```

这个示例有意没有设置代理。如果智能体确实需要代理，请在确认代理所有者并测试其行为后，在启动器中添加代理变量。不要直接把 shell 配置文件中的代理值复制到脚本中，先检查其中是否包含凭据，或是否指向已经失效的本地服务。

干净环境可能会让依赖证书位置、云配置文件、SSH 代理套接字或软件包缓存等变量的工具无法运行。这种失败能提供有用信息。逐个恢复智能体需要的变量，并记录每个变量存在的原因。智能体启动器应足够精简，让另一位工程师读完后就能理解请求会发往哪里。

网络控制应为此提供支持。如果智能体只能访问少数内部服务，请根据所在环境使用防火墙规则、执行策略的出口代理或专用网络分段。环境变量只能为遵守它的客户端选择路由，不能阻止已被入侵的进程或不遵守设置的库直接打开套接字。

## 不要把凭据交给选择路由的进程

保持代理配置整洁，可以减少意外改道，但这并不意味着把 API 令牌交给自主智能体是明智的。如果智能体环境中包含 bearer token，那么所有能够访问该环境的进程都进入了这个机密的暴露路径。

Sallyport 采用了不同的边界：智能体通过 MCP shim 请求 HTTP 或 SSH 操作，凭据留在应用的加密保管库中，由应用执行操作。这样可以限制智能体进程中代理变量泄露所造成的影响，因为智能体从未以明文获得 API 或 SSH 凭据。

不要夸大这个边界的作用。凭据网关不会自动为智能体运行的每条命令定义安全代理行为，也不能让有害目标变得无害。你仍需控制允许访问的目标和操作，审查实际执行网络调用的应用进程，并保留请求路径的证据。

真正有用的区分是机密保管与网络路由。机密保管回答谁能读取凭据，网络路由回答哪个中间方处理请求。团队经常解决了其中一个问题，就以为两个问题都解决了。事实并非如此。

## 将无法解释的代理使用视为需要控制的事件

如果特权智能体通过未知代理发出了请求，应在同一受污染环境中继续调查之前先停止运行。记录智能体进程 ID、父进程、代理地址、受影响的目标名称和时间范围。按照事件处理流程保存相关智能体和代理日志。

然后从源头移除变量。在当前终端中删除变量，只能修复该终端未来启动的子进程。检查 shell 配置文件、IDE 任务设置、启动代理、CI 配置、包装脚本，以及任何会写入环境设置的配置管理系统。修正启动点后，重启受影响的进程。

按协议评估凭据。如果受影响的流量是带身份验证的明文 HTTP，请轮换暴露的凭据。如果是通过代理的 HTTPS，请确认端点是否执行了正常 TLS 验证，以及智能体是否信任拦截证书。不要因为 HTTPS 就假定无需审查，也不要在保留同一注入路径的情况下盲目轮换凭据。

最后，在智能体启动位置加入预检。出现意外代理变量时，应终止运行或要求明确审查。检查应报告变量名称和经过脱敏的端点，将其与该任务批准的路由进行比较，并留下预检已经运行的记录。第一个无法解释的代理是警告，第二个则是你选择继续保留的部署缺陷。
