# 让 MCP 智能体摆脱环境变量：迁移凭据

把凭据放进 MCP 服务器的环境变量，是一种一开始很方便的捷径。但当智能体可以执行命令、检查文件、排查故障或启动辅助程序后，这种做法就会变得危险。问题不在于每个智能体都会故意打印令牌，而在于你把一个用于探索和执行操作的进程交给了可重复使用的秘密，却要求它表现得像是看不见这个秘密。

把权限移出智能体进程。让智能体请求一个定义明确的 HTTP 或 SSH 操作，再由持有凭据的本地组件执行。这个变化听起来很小，却会迫使你弄清每个工具实际做什么、需要哪个身份，以及如何证明令牌没有偷偷进入新的路径。

我见过迁移失败的情况：有人从一个配置文件中删除了 `API_TOKEN`，却忘了 shell 配置文件、任务运行器或复制的项目模板里还留着它。API 调用仍然成功，所有人都放松了警惕，而智能体仍然拥有旧凭据。干净的迁移应把发现、替换和验证分别当作独立任务。

## 环境变量赋予智能体超出工具所需的权限

环境变量不属于某一个请求，而属于一个进程，通常还属于该进程启动的所有程序。如果 MCP 客户端启动服务器时把 `SERVICE_TOKEN` 放进环境中，服务器就能读取它。除非有人仔细清除，子进程也能继承它。调试命令、崩溃报告、测试夹具，以及意外输出的 `env`，都可能把本地便利变成持久泄露。

这和工具接收经过身份验证的结果不同。结果可能包含代码仓库列表、部署状态或错误响应，智能体需要这些信息来继续工作。它不需要发起请求所用的 Bearer 令牌。

两种设计都可能产生同样成功的请求，因此这种区别很容易被混淆。但它们并不等价：

- **持有凭据**意味着智能体可以在预期工具调用之外使用、复制、转换或外泄秘密。
- **操作权限**意味着智能体可以请求受信任的本地执行器，在规定的控制下发起请求。
- **访问结果**意味着智能体可以看到决定下一步行动所需的响应。

混淆这一区别，会导致一个常见但糟糕的建议：先把令牌放进秘密管理器，再在启动时注入智能体环境。这样可能改善静态存储安全，却没有改变运行时边界。智能体仍然拿到了令牌。

MCP 规范描述了客户端、服务器和工具之间的协议，但没有把环境变量定义为凭据边界。只有在接收环境变量的进程本来就被信任可以持有底层秘密时，才应把环境变量当作本地配置的传递方式。自主编程智能体往往达不到这个标准。

## 更改配置前先建立工具清单

先写一份清单。不要一上来就编辑 JSON 文件，因为配置文件很少能说明全部情况。工具可能直接读取一个变量，也可能调用读取另一个变量的包装程序，或者依赖从用户主目录文件中加载凭据的命令行客户端。

为每个 MCP 工具记录工具名称、命令、目标、身份验证方式、凭据所有者、权限范围，以及可用于测试的无害请求。同时记录秘密目前从哪里进入进程：客户端配置、shell 启动文件、`.env` 文件、CI 导出变量、密码管理器命令，还是辅助脚本。

一个简洁的清单可以这样写：

| 工具 | 操作目标 | 当前秘密路径 | 替代边界 | 测试 |
| --- | --- | --- | --- | --- |
| 问题搜索 | 问题 API | 客户端配置中的 `ISSUES_TOKEN` | 本地 HTTP 操作 | 列出一个已知项目 |
| 部署状态 | 部署 API | `.env.local` | 本地 HTTP 操作 | 读取服务状态 |
| 主机诊断 | SSH 主机别名 | 私钥文件路径 | 本地 SSH 操作 | 运行 `uname` |
| 发布软件包 | 软件包仓库 API | shell 导出变量 | 本地 HTTP 操作 | 读取软件包元数据 |

不要把宽泛权限藏在 `prod-token` 或 `default-key` 这类模糊名称后面。凭据记录的名称应告诉操作人员它能做什么、要访问哪里。`deploy-api-production-read` 不够漂亮，却能让匆忙的审查安全得多。

清单还会暴露某个凭据是否根本不该存在。我发现过一些只有读取项目元数据权限的工具，却挂着可写令牌，因为有人复制了开发环境配置。迁移正是签发更窄权限凭据的好时机，不是把所有旧权限装进更漂亮容器的理由。

## 删除秘密注入，不要只是掩盖它

迁移后的配置必须停止把秘密交给智能体或其 MCP 服务器。把明文令牌替换成 `${SERVICE_TOKEN}`、`$(secret-tool lookup ...)`，或一个未受保护文件的路径，都不满足这个要求。你只改了写法，没有改变权限。

先寻找当前引用。在项目目录中，下面的命令可以找到很多明显情况：

```sh
rg -n --hidden --glob '! .git' 'API[_-]?KEY|API[_-]?TOKEN|SECRET|PASSWORD|PRIVATE[_-]?KEY|Authorization: Bearer' .
```

预期输出应是一组 `文件:行号:匹配文本` 条目。如果包含有效值，不要把输出粘贴进工单。用它建立整改清单，再分别搜索常见的用户级位置，例如 shell 配置文件和 MCP 客户端设置。

接着比较旧智能体进程和新进程能够看到的环境。在受控测试 shell 中只列出名称，不要打印值：

```sh
env | cut -d= -f1 | sort | rg 'TOKEN|KEY|SECRET|PASSWORD'
```

你希望启动智能体的进程中不再出现旧名称。如果那里仍有 `SERVICE_TOKEN`，即使新的网关路径能正常工作，迁移也没有完成。

不要采用一种看似折中的设计：MCP 服务器拥有令牌，而主智能体没有。这样确实减少了一条暴露路径，但服务器仍然拥有不受限制的凭据。如果服务器可以执行任意命令、加载插件或写入日志，你只是把问题移到了一个通常更少受到审查的进程里。

使用非秘密配置选择目标。只要不会授予访问权限，基础 URL、主机别名、账户标识和凭据标签都可以放进配置。经过身份验证的材料应保存在智能体无法将其作为数据查询的本地保险库中。

## 把新路径建模为请求、凭据和结果

新流程应有明确边界：智能体命名一个操作并提供普通请求数据，本地执行器选择并注入凭据，然后返回响应。智能体永远不会收到一个可以自行解析为秘密的占位符。

对于 HTTP 工具，要把公开的请求形式和私有的身份验证步骤分开。智能体可以请求执行下面的请求：

```text
GET https://api.example.internal/projects/atlas/issues?state=open
```

本地执行器附加相应的 Bearer、Basic 或自定义请求头凭据，然后返回响应正文和状态。如果智能体请求未经批准的主机、方法或账户，执行器应拒绝请求，而不是猜测哪个凭据合适。

对于 SSH，请求包含主机和命令，私钥留在本地。这一点很重要，因为 SSH 工具经常会通过路径、代理转发、配置包含文件和继承的 `SSH_AUTH_SOCK` 偷渡权限。在工具配置中放一个私钥路径并不是安全替代方案。智能体往往可以读取文件、复制文件，或修改使用该文件的命令。

Sallyport 通过内置的 `sp mcp` stdio shim 采用了这种形式：智能体发起 MCP 调用，应用执行 HTTP API 调用和 SSH 命令，不把 API 密钥或 SSH 密钥交给智能体。但如果你不同时删除旧的环境注入，这个边界就没有意义。

不要把本地执行器做成带有一个全能凭据的通用代理。智能体的请求应指出已配置的目标和凭据，而不是提供任意 URL 加上凭据选择。否则，受到提示注入的智能体可能把合法凭据变成请求签名器，访问你从未打算允许的地方。

## 选择人们仍能真正评估的批准点

当人能够理解自己批准的内容时，人工批准才有用。如果长时间运行的智能体不断生成几乎相同的提示，直到人们一路点击通过，批准就会失效。这种失败是可以预见的，是设计缺陷，不是操作人员没有通过警觉性测试。

需要确认某个特定智能体进程可以在一次运行期间使用已配置操作时，可以采用会话级批准。批准应以有助于区分真实客户端和仿冒程序的方式标识进程。仅凭进程名证据很弱，因为任何程序都可以选择一个熟悉的名称。

对于需要新鲜人工判断的高风险凭据，保留每次调用批准。生产写权限、破坏性主机命令和与支付相关的 API 值得这种阻力。只读问题查询通常不值得。如果每次工具调用都要求做决定，操作人员就会停止阅读决定内容。

即使此前已批准的智能体仍在运行，锁定的保险库也应拒绝请求。这正是保险库门禁的意义。无人值守的进程不能仅仅因为下午早些时候拥有权限，就一直继承权限。

Sallyport 提供三个固定控制项，而不是策略语言：保险库门禁、对每个新智能体进程的授权，以及可选的每个密钥批准要求。这个固定模型比规则引擎更窄，因此疲惫的操作人员需要误写的复杂规则更少。

## 把成功和保密作为两项独立声明来测试

工具返回绿色结果，只能证明某个东西为请求完成了身份验证。它不能证明智能体无法取得凭据。迁移测试应从低风险操作开始，同时检查这两项声明。

对每个工具使用下面的顺序：

1. 锁定本地保险库并调用工具。没有环境令牌回退时，调用必须失败。
2. 解锁保险库，启动一个新的智能体进程，并在设置要求时批准该进程。运行清单中确定的无害请求。
3. 检查操作日志或服务器端审计记录，确认确切的目标、账户、方法和结果状态。确认记录包含操作，但不包含秘密。
4. 从智能体允许的执行路径检查其环境中的旧变量名，并在工作区搜索令牌前缀。两处都不应出现令牌。
5. 重启智能体进程并重复请求。这可以检查你是否意外依赖了旧 shell 中继承的状态。

第一步会捕捉一个微妙但严重的错误。有些团队配置了保险库路径，却保留旧变量作为回退。保险库锁定后，工具仍然成功。这看起来很可靠，直到同一个回退出现在 CI 工作节点、复制的仓库或智能体对话记录中。

不要把让智能体打印所有环境变量当作主要证明。这个测试本身就会创建泄露路径，还可能把旧令牌放进对话历史。应从受控 shell 中检查名称；如果需要验证继承行为，可以在演练中使用一个刻意设置的非秘密标记变量。

对于 SSH，应测试返回无害系统身份信息的命令，而不是会改变状态的命令。确认结果后，再确认智能体配置包含的是主机引用，而不是私钥材料。同时检查 SSH 配置中是否有 `ForwardAgent yes`。代理转发可能让远程主机获得使用本地身份的路径，这和暴露私钥文件是另一种风险。

## 审计记录必须能够还原有争议的操作

当行动日志能回答一个糟糕的周一早晨问题时，它才真正有用：哪个智能体运行发起了这次请求，通过哪个已配置凭据，是否经过人工批准？像 `tool succeeded` 这样的模糊记录解决不了问题。

为智能体运行保留会话记录，为具体操作保留调用记录。会话记录告诉你进程何时开始、批准了哪个身份，以及何时撤销。调用记录告诉你该会话下发生了什么。如果需要在许多运行中调查某一次，不要把两者混成一个扁平事件流。

防篡改证据很重要，因为拥有本地命令执行能力的智能体可能会在执行不理想的操作后试图抹掉痕迹。哈希链式日志能帮助你发现历史记录是否被更改或删除，但前提是验证过程不依赖智能体配合。

Sallyport 将两类日志都写入加密、只写不可改、哈希链式审计日志，并支持使用下面的命令离线验证密文链：

```sh
sp audit verify
```

成功结果应明确显示验证已完成；如果失败，在理解断点前应把日志视为可疑。验证不能告诉你操作是否明智，只能告诉你记录是否仍然连续。

不要把审计数据放进模型提示或普通聊天记录。即使记录从未包含凭据，也可能包含敏感请求上下文或响应元数据。调查权限不应变成随意浏览的后门。

## 轮换是证明迁移确实完成的收尾工作

每条新路径通过测试后，轮换旧凭据。因为“我们可能需要回滚”而让旧凭据继续有效，只会延长被遗忘的配置仍能完成身份验证的时间。回滚应围绕另一个受控的替代方案设计，而不是依赖已经散落在本地环境中的秘密。

轮换顺序很重要。创建新的、范围受限的凭据，将它存放在本地，验证新路径，删除旧的秘密注入，然后撤销旧凭据。如果旧令牌可能出现在代码仓库、聊天消息、支持工单或构建输出中，应先撤销它，并在恢复安全访问期间接受服务中断。

撤销后再次执行发现搜索。你要寻找的是过时引用，而不是有效秘密值。从示例文件、入职文档、shell 配置文件、任务脚本和测试说明中删除无用的变量名。未来的开发者会出乎意料地忠实地复制示例。

最后，在运维记录中保留一个有意设计的失败测试：锁定保险库并运行一次无害工具调用。如果调用成功，说明有人重新引入了绕过路径。这个检查抓到的糟糕迁移，往往比另一张精美的架构图更多。

## 把宽权限凭据视为工具设计缺陷

把令牌移入保险库，并不会让宽权限令牌适合智能体，只是改变了谁持有它。能够查询项目问题的工具，不应悄悄带有删除项目、修改账单或部署生产代码的权限。

根据实际工作和后果拆分工具。只读发现操作可以使用权限有限的凭据和适度批准。改变状态的操作需要更窄的范围、明确的目标，有时还需要每次调用都获得同意。这也让智能体更容易受到监督，因为它能使用的动词和你交给它的任务相匹配。

对于以工具简单为理由设置通用 `admin` 凭据，要保持警惕。这种设置今天很受欢迎，因为它减少了配置工作；但明天会让事故响应更糟，因为你无法判断哪些请求确实需要这些权限，哪些请求只是继承了它们。

当智能体能够完成预期工作、锁定保险库后它会停止、新进程必须重新获得你配置的权限，并且旧令牌不再出现在其环境中时，迁移才算成功。如果其中任何一项没有测试，你得到的只是一个能运行的演示，而不是凭据边界。
