# 为 AI 编程代理正确配置堡垒主机访问

堡垒主机可以为 AI 编程代理进入私有基础设施提供更安全的路径，但前提是你把它当作访问边界的一部分。仅仅在 SSH 配置文件中出现一个跳板主机，并不等于有了控制。代理可能绕过它、借用凭据、建立隧道，或者只留下能够证明“某个东西连接过跳板主机”的审计记录。

对于自主执行的任务，更可靠的设计要严格得多：代理只能到达一个受控入口，入口只能连接已知目标，凭据不能进入代理的上下文，记录还必须区分代理请求的主机与实际接受连接的主机。这个区别听起来有些吹毛求疵，直到第一次事故复盘。那时它决定了你手里的是证据，还是一个令人安心的故事。

## 堡垒主机会改变路径，不会改变代理的权限

堡垒主机访问设计可以限制网络可达性，但不能减少代理到达特权账户后能做的事情。团队常常因为两者都涉及 SSH 而混淆它们。它们是不同的控制，应当分别失效。

当私有目标只接受来自堡垒主机或其子网的管理流量时，跳板主机很有帮助。它为你提供了一个位置，可以施加出站限制、收集连接证据，并在调查期间切断访问。它还避免让每台工作站、构建运行器和代理沙箱都需要直接访问敏感网络。

但它不能回答自主客户端更关心的四个问题：

- 代理能否通过另一条路径直接访问目标？
- 目标账户的权限是否超出了当前任务的需要？
- 代理能否建立未经审核的隧道？
- 事后能否确认最终机器和远程命令？

如果第一个问题的答案是“能”，ProxyJump 就只是便利设置，而不是强制措施。如果第二个问题的答案是“能”，堡垒主机只是把权限过大的凭据进入网络的位置挪了过去。

我见过团队精心维护跳板主机，却为了“故障排查”允许 CI 子网拥有广泛的 SSH 出站权限。运行在该子网中的代理不需要漏洞利用，也不需要什么高明技巧，它可以直接访问最终地址。架构失败的原因是网络允许绕过，而不是 SSH 表现异常。

把各层职责说清楚。代理环境只能连接入口主机。入口主机只能连接指定的目标网络和端口。最终主机应当只为狭窄用途授权账户。每一层都应生成记录，回答不同的问题。

## ProxyJump 不会暴露最终命令

SSH 跳板主机通常无法检查代理在最终主机上执行的命令。这正是团队承诺“堡垒主机可以记录全部命令”时最容易忽略的一点。

OpenSSH 在 `ssh_config(5)` 中说明，`ProxyJump` 会先连接跳板主机，然后建立到最终目标的转发。在常见的 OpenSSH 使用方式中，客户端实际上请求跳板主机与目标建立 TCP 连接，随后通过这条字节流运行端到端 SSH 会话。最终的 SSH 传输仍然在客户端与目标之间加密。

堡垒主机通常可以知道它连接到了 `10.42.8.19:22`。它可能记录堡垒主机上使用的账户、来源、时间，以及转发请求的目标。但它不会自动知道加密会话后来执行的是 `systemctl restart api`、`cat /etc/shadow`，还是一个交互式 shell。

这会产生三种经常被混为一谈的观察结果：

1. **请求目标**是代理提供的内容，例如 `prod-api-01`。
2. **连接目标**是代理实际打开的地址和端口。
3. **执行目标**是接受最终 SSH 身份验证的服务器身份和账户。

它们可能不同。DNS 变更后，别名可能解析到不同地址。遭入侵或过期的 known-host 条目会让连接变得可疑。代理规则可能把一个友好的别名指向意外地址。只记录 `prod-api-01`，无法证明哪台服务器执行了命令。

同样的区分也适用于命令。客户端网关可以在传输前记录请求的命令。最终主机上的包装器可以记录 OpenSSH 传递给 exec 请求的命令。堡垒主机可以记录 TCP 目标。应当通过会话标识和时间窗口关联这些观察结果，不要假装某一层能看到全部内容。

如果你需要一个能够检查并授权每条远程命令的统一位置，那么普通的端到端 SSH 加跳板主机并不是合适的基础设施。可以使用专用远程执行服务、受限命令端点，或只接受预定操作的目标侧包装器。这样的选择会牺牲灵活性，但也能避免声称你并没有的可见性。

## 网络规则必须让获批路径不可绕过

只有当周围网络让其他路径不可用时，受控入口才真正有效。先解决可达性，再讨论审批对话框或日志格式。

代理运行环境只需要向堡垒主机的地址和端口发起出站 SSH。它不应拥有通往生产子网、公开管理接口或第二个未纳入设计的跳板主机的宽泛路由。DNS 在这里同样重要。如果代理可以解析并连接原始目标地址，仅靠友好的主机名约定无法保护你。

堡垒主机需要一份简短的出站允许列表。如果它只管理两个私有网段中 TCP 22 端口上的主机，就只允许这些网段和端口。不要给它任意出站访问，然后把它称为堡垒主机。入口主机一旦可以访问每个服务、数据库和公共端点，在代理账户被滥用时就会变成通用中继。

最终目标只能接受来自堡垒路径的 SSH 管理流量。云安全组、主机防火墙和网络 ACL 都能提供帮助。只有在能够明确维护责任时，才叠加多个控制。无人维护的规则不能算防御。

用拒绝场景测试路径，而不只是测试成功命令。从运行代理的同一用户、容器或虚拟机中执行这些检查：

```sh
nc -vz bastion.internal.example 22
nc -vz 10.42.8.19 22
ssh -o ConnectTimeout=5 prod-api-01 true
```

第一个连接应当成功。直接连接最终主机应当失败。SSH 命令只能因为配置的路径使用堡垒主机而成功。如果环境中没有 `nc`，就使用环境通常提供的 TCP 测试命令。重点是测试网络路径，不要让 SSH 配置掩盖直接路由。

防火墙变化、新增子网或代理运行器变化后，都要重复测试。这是一个成本很低、收益很高的小测试，能发现 SSH 配置看起来正确但网络仍允许直连的常见问题。

## 代理需要可撤销的身份

不要给代理一个共享的管理员密钥，然后希望堡垒主机能让这种做法变得合理。被复制的私钥会形成一个影响时间很长的授权决定。

为每个代理进程或每次运行分配可以归因和撤销的身份。可以使用短期 SSH 证书、为限定任务注册的临时密钥，或由操作网关持有并代表代理执行 SSH 的凭据。具体选择取决于你的环境，但代理不应把可重复使用的秘密写入会话记录、工作区、shell 历史或工具输出。

如果你已经运行 SSH 证书颁发机构，SSH 证书会很有帮助。证书可以包含较短的有效期、目标账户的主体，以及源地址限制等关键选项。目标主机可以信任 CA，而不用维护不断增长的单独公钥列表，因此签发和过期更容易检查。

证书不会让宽权限账户变得狭窄。给 `root` 的证书在过期前仍然是 root 凭据。证书也不能证明命令意图。应把它们看作签发和生命周期管理机制。

网关会更明显地改变暴露模式。Sallyport 可以通过 `sp-ssh` 辅助程序执行 SSH，同时让 SSH 密钥留在加密保险库中，不进入代理进程。这能保护私钥不出现在代理记录里，但团队仍应测试预期的跳板路径、目标身份和命令证据如何被记录。

审批应绑定到可识别的进程，而不是代理在聊天窗口中写下的句子。进程可以经过签名、记录启动时间、绑定本地用户，并作为运行中的会话撤销。自然语言说明可以作为上下文，但不是访问控制边界。

避免所有自动化任务共享一个凭据。当目标侧日志显示 `deploy` 时，你还需要另一条记录来识别发起调用的代理运行。如果每次运行都使用相同密钥和账户，事故复盘就会变成考古工作。

## 同时记录执行声明、路径事实和结果

代理 SSH 审计记录应明确说明每个字段的含义，避免审计系统声称它无法支持的内容。

在请求操作时，记录代理运行身份、请求的主机别名、请求的远程账户、命令文本或结构化操作，以及调用方是否请求 TTY 或转发。这是执行声明，说明调用方试图做什么。

在路径边界记录入口主机身份、解析后的目标地址、目标端口和连接结果。这是路径事实。如果代理打开了连接，它可以说明连接打开到了哪里，但不能从加密字节中真实推断最终命令。

在最终主机记录目标主机的稳定身份、已认证账户、身份验证指纹或证书序列号、服务器收到 exec 请求时的命令、退出状态，以及相关本地服务日志。这是执行证据。最终主机是普通 SSH 端点中唯一能在不破坏 SSH 端到端设计的情况下看到远程命令的位置。

使用能让缺失证据显现出来的事件结构：

```json
{
  "run_id": "run_7c31",
  "requested": {
    "host": "prod-api-01",
    "user": "deploy",
    "command": "sudo systemctl restart api",
    "tty": false
  },
  "route": {
    "bastion": "bastion.internal.example",
    "destination_ip": "10.42.8.19",
    "destination_port": 22
  },
  "final_host": {
    "host_fingerprint": "SHA256:example",
    "authenticated_user": "deploy",
    "command_observed": true,
    "exit_status": 0
  }
}
```

示例特意把 `requested.command` 与最终主机上的观察结果分开。如果会话是交互式的，就将 `command_observed` 设为 false，并说明原因。没有解释的空字段会让人误以为工具捕获了命令。

对于普通命令执行，`sshd_config(5)` 记录了 `ForceCommand`，它可以为匹配的账户或 match 块强制指定命令。目标侧包装器可以读取 exec 请求中的 `SSH_ORIGINAL_COMMAND`，验证允许的操作，并在调用获批程序前写入审计记录。它需要谨慎处理 shell。不要通过 `eval` 传递不受信任的命令字符串，也不要假设交互式 shell 一定存在 `SSH_ORIGINAL_COMMAND`。

哈希链和只追加存储有助于发现审计事件后续被修改，但它们无法修复薄弱的事件内容。一条保存得完美、却只写着“SSH 已连接”的记录，仍然是薄弱证据。

## 让 SSH 配置可检查

为人类保留主机别名，但在把配置交给代理前，先检查 OpenSSH 实际会做什么。`ssh -G` 会输出 OpenSSH 处理匹配的 `Host` 条目后生成的有效客户端配置。

下面的示例将一个命名目标通过指定堡垒主机进行路由，并关闭一些经常引发意外的选项：

```sshconfig
Host agent-bastion
    HostName bastion.internal.example
    User agent-gateway
    IdentityFile ~/.ssh/agent_gateway
    IdentitiesOnly yes

Host prod-api-01
    HostName 10.42.8.19
    User deploy
    ProxyJump agent-bastion
    StrictHostKeyChecking yes
    UserKnownHostsFile ~/.ssh/agent_known_hosts
    ForwardAgent no
    RequestTTY no
```

然后检查展开后的设置：

```sh
ssh -G prod-api-01 | grep -E '^(hostname|user|proxyjump|forwardagent|requesttty) '
```

输出应当具有类似这样的结构：

```text
user deploy
hostname 10.42.8.19
requesttty no
forwardagent no
proxyjump agent-bastion
```

这能证明客户端计划如何运行，但不能证明网络阻止了直接 SSH、目标提供了预期主机密钥，或堡垒主机限制了出站访问。这些都要单独测试。

不要让代理自由提供任意 `ssh` 参数。如果执行层只是透传 shell 字符串，代理可以用命令行选项覆盖配置、指定原始 IP 地址、替换 `ProxyJump`，或添加转发选项。尽可能把目标、账户和允许的 SSH 选项放进结构化操作输入中。如果接受原始命令，就应把它视为代码，像对待来自不受信任来源的 shell 脚本一样谨慎。

主机密钥验证尤其需要注意，因为代理会自动重试，常常把错误当成任务障碍。通过受控流程预先加载可信主机指纹。保持严格检查。不要教代理删除 known-host 条目，或接受已变化的密钥来让部署继续。主机密钥变化时，必须由操作员确认变化原因。

## 转发和 shell 会绕过审核

除非特定任务确实需要，否则应为自主代理禁用转发和交互式 shell。这两项功能都会把受限的远程命令变成更宽泛的访问通道。

本地转发允许客户端打开一个本地端口，通过 SSH 访问内部服务。远程转发允许远端端点暴露一条返回客户端侧的路径。动态转发会创建 SOCKS 代理。代理转发会让另一台主机获得凭据。每一项对人工管理员都可能合理，但每一项也都会削弱“堡垒主机是唯一受控路径”的说法。

OpenSSH 提供的服务器侧限制应当配置在最终目标或堡垒账户上，而不只是代理的客户端配置中。`sshd_config(5)` 记录了 `DisableForwarding`，`authorized_keys` 则支持 `no-port-forwarding`、`no-agent-forwarding`、`no-X11-forwarding` 和 `no-pty` 等选项。使用你的 OpenSSH 版本支持的控制，然后通过真实连接尝试验证它们。

受限账户可以使用类似这样的 authorized key 条目：

```text
no-port-forwarding,no-agent-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... agent-run
```

这行设置会限制该公钥的功能，但不会限制账户可以执行的命令。如果命令范围很重要，还要配合受限账户、`ForceCommand` 包装器或特定服务接口。

对于交互式 shell，规则应当直接明确：默认不要给自主代理使用。shell 接受一连串命令、管道、重定向、后台进程和复制的凭据。初始操作请求中的命令字段不会描述完整会话。强制包装器可能阻止 shell，但必须进行测试，因为 shell 行为会随账户配置和 SSH 子系统请求而变化。

有些维护任务确实需要隧道或 shell。应把它们作为明确的例外路径，要求指定操作员批准并设置短期有效期。把例外隐藏在通用代理凭据中，临时访问就会变成永久访问。

## 目标限制优于命令字符串过滤

简单的 shell 命令前缀允许列表容易绕过，也难以维护。它很受欢迎，是因为看起来很精确：允许 `systemctl restart api`，拒绝其他内容。但 shell 语法会让这种信心变得脆弱。

假设过滤器接受以 `systemctl restart api` 开头的文本。调用方可能追加 shell 操作符、使用不同的可执行文件路径、通过环境变量触发行为，或利用解析方式不一致的包装器。即使解析器写得很谨慎，也无法预知一个会调用其他程序的命令的全部影响。

当任务形态稳定时，应使用结构化操作。部署运行器可以接受应用名称和发布标识。维护端点可以从固定列表中接受服务名称。受限包装器可以把少量操作名映射到固定的 argv 数组。在每种情况下，都尽量完全避免 shell。

如果必须支持自由格式命令，就要承认你实际授予的是该账户的远程 shell 权限。记录它，为凭据设置时限，限制目标环境，并要求更高等级的单独审批。不要把字符串过滤器包装成命令授权。

目标账户还应当有操作系统级权限边界。能够运行不受限制 `sudo` 的 `deploy` 账户，本质上就是一个多打几个字的管理员账户。只授予自动化所需的服务操作、文件和目录。把 sudo 规则当作代码审查，包括每个命令路径和参数模式。

实际测试是：一个被要求重启服务的代理，是否能够读取生产机密、建立反向隧道、修改 SSH 授权或更改审计收集器。如果可以，即使每次连接都使用了正确的堡垒主机，账户范围仍然是错误的。

## 事故响应从停止实时路径开始

当代理表现可疑时，在重建其意图前先撤销实时访问。如果有操作边界，就在那里停止其活动会话；阻止来源访问堡垒主机，并阻止相关凭据再次进行身份验证。在有人轮换日志文件或重启保存证据的主机前，先保存日志。

然后从分开的记录建立时间线。先看代理请求的操作，再将其与堡垒主机的连接目标和时间匹配，最后与最终主机的身份验证和命令证据匹配。检查是否出现 TTY、转发请求或备用目标。差异不一定意味着恶意行为，但它会告诉你应该调查哪个控制点。

不要为了“让代理修复问题”而永久授予更多权限。这是常见的恐慌式做法。如果代理已经偏离预期，扩大其账户权限或开放直接路径，只会移除你需要的证据，并制造另一场事故。

在生产环境真正需要之前，先进行一次小规模演练。在无害的 SSH 命令执行期间撤销测试代理，确认新的请求会失败，并验证记录显示了请求的主机、实际路径、目标身份和退出结果。如果这次演练还需要在群聊中拼凑发生了什么，你的访问设计仍然过于模糊。

当堡垒主机能够强制执行路径、支持快速撤销，并为审计记录贡献精确事实时，它才真正有存在价值。先配置路径，让凭据远离代理，并在最终主机上收集可见的命令证据。除此之外，它只是一个带有安全色彩名称的 SSH 便利功能。
