阅读需 8 分钟

SSH 就绪检查清单如何改变代理安全性

在代理获得 shell 访问权限前,使用这份 SSH 就绪检查清单测试结果解析、主机身份、幂等性、批准和未知远程状态。

SSH 就绪检查清单如何改变代理安全性

对代理来说,只读 HTTP 是一个宽容的首个通道。请求有方法、范围有限的目标、状态码、请求头,通常还有结构明确并有文档说明的正文。失败的 GET 一般不会改变服务。凭据仍可能处理不当,敏感数据也可能泄露,但这种执行模型能让审查者在较小范围内判断风险。

SSH 改变了风险的单位。代理不是调用一项定义明确的操作,而是向远程 shell 发送文本。其行为取决于登录账户、shell、工作目录、环境、操作系统、已安装工具、引用规则和机器的实时状态。一条命令可能部分成功,在报告结果前断开连接,下一次重试就可能把工作做两遍。

这不意味着 SSH 不适合代理。它意味着“HTTP 通道已经可用”不足以作为增加 SSH 的依据。准入测试必须覆盖五项独立属性:可信的结果解析、已验证的主机身份、可安全重复的命令、能说明后果的批准,以及对未知远程状态的明确处理。只要缺少其中一项,就不要开启第二个通道。

将 SSH 视为不同的执行模型

SSH 就绪的前提是承认:远程 shell 不是换了 URL 的 HTTP 传输。HTTP API 公开的是由服务器选定的操作。SSH 公开的是远程账户选定的解释器,再让客户端把无结构的命令字符串带入其中。

对于只读 HTTP 调用,审查者通常可从 GET、主机和路径推断其影响。状态 404 这样的响应在协议中也有惯常位置,即使应用赋予它特定领域含义。对于 SSH,test -f /srv/app/release && cat /srv/app/release 可能因文件不存在而返回 1,cat /srv/app/release 也可能因权限被拒绝而返回 1。如果解析器把两者都归为“命令失败”,代理就无法安全决定下一步。

远程环境也会改变含义。sed -i 在常见操作系统中的行为不同。登录 shell 可能加载会向 stdout 输出横幅的启动文件。PATH 可能找到包装程序,而不是你预计的二进制文件。区域设置可能改变人类可读的诊断信息。伪终端会把面向人的交互行为混入供解析器处理的命令中。典型 HTTP 请求架构不会呈现这些变量。

启用命令前,先写好通道契约。它应说明远程用户、可接受的主机、shell、初始目录、环境策略、是否禁止伪终端、最长运行时间、输出限制和确切的结果封装。对敏感操作固定可执行文件路径。必须处理文本时,设置已知的区域设置。优先使用远程程序产生的机器可读输出,但不要以为程序有 JSON 选项,就能让外围 shell 也变得结构化。

将非交互探测作为第一道关卡:

/usr/bin/ssh \
  -o BatchMode=yes \
  -o StrictHostKeyChecking=yes \
  -o ConnectTimeout=10 \
  -T [email protected] \
  'umask 077; printf "%s\n" "{\"probe\":\"ssh-ready\",\"version\":1}"'

OpenSSH 文档说明,BatchMode=yes 会禁用提示,包括密码提示和主机密钥确认提示。-T 会禁用伪终端分配。StrictHostKeyChecking=yes 会拒绝未知或变更过的密钥。预期 stdout 仅有一个 JSON 对象,stderr 为空,远程退出状态为零。应单独测试每一种偏差。无法区分未知主机和格式错误 JSON 的系统,还没有通过这道关卡。

让结果封装没有歧义

只有在传输错误、远程终止、stdout 和 stderr 始终彼此分离时,结果解析才算准备就绪。把它们合并到一个文本字段中,会制造危险的虚假确定感。

RFC 4254 将 stdout 定义为通道数据,将 stderr 定义为扩展通道数据。它还定义了 exit-statusexit-signal 消息,但措辞很重要:发送退出状态是建议行为,不是强制要求,客户端也可能忽略它。OpenSSH 在这个协议之上提供了有用的本地约定。其 ssh 命令会返回远程命令的状态,发生错误时则返回 255。

不要把这一约定当作普遍真理。远程程序本身也可能退出 255,这会在进程边界上与 OpenSSH 的客户端错误值冲突。服务器或库可能不发送 exit-status 消息。信号终止不同于普通的非零退出。通道可能已产生输出,但在客户端收到最终状态前关闭。

内部结果应像有类型的记录,而不是一份文字记录:

{
  "phase": "completed",
  "transport": "ok",
  "host": "host.example",
  "host_key_fingerprint": "SHA256:verified-value",
  "exit_status": 0,
  "exit_signal": null,
  "stdout": "{\"state\":\"present\",\"release\":\"2026.07\"}\n",
  "stderr": "",
  "truncated": false,
  "started_at": "request timestamp",
  "finished_at": "result timestamp"
}

exit_status 可以为空。让 phase 至少区分 rejected、not-started、started、completed 和 unknown。将截断作为数据记录,绝不可悄悄截去输出后,把剩余内容当作完整结果交给代理。附上该连接实际使用的已验证主机指纹,而不只是代理请求的主机名。

接着测试结果矩阵。运行一条成功但没有输出的命令,一条向两个流写入内容后以 7 退出的命令,一条被信号终止的命令,一条超过截止时间的命令,一条在技术栈允许字节时输出无效 UTF-8 的命令,以及一条超过每个输出限制的命令。远程命令休眠时断开客户端连接,再检查结果。适配器不应凭空生成 exit_status: 0,不应从 stdout 推断成功,也不应在无法证明命令是否运行过时把超时称作“失败”。

shell 组合方式也值得单独测试。POSIX 规定,除非启用 pipefail,管道通常报告最后一条命令的状态。这意味着 generate | upload 可能报告成功,因为 generate 失败后,upload 接收了空输入。不要依赖交互 shell 的默认设置。将多命令操作放进经过审查、带明确错误处理和版本号的脚本中,然后只调用一个脚本入口。

在代理能够触达主机前验证主机

主机验证是清单管理问题,不是让代理回答的提示。有效的用户凭据证明客户端在服务器看来是谁。服务器的主机密钥证明回应客户端的是哪台服务器。两者都需要。

绝不可把 StrictHostKeyChecking=no 当作自动化的解决办法。当前 OpenSSH 文档指出,该设置可以自动添加新密钥,并且在一定限制下,主机密钥变更时仍可能允许连接继续。accept-new 更好,因为它会拒绝变更过的密钥,但它仍信任首次连接。对于代理通道,应在运行前配置可信关系,并使用 StrictHostKeyChecking=yes

ssh-keyscan 有助于收集公开主机密钥,但不能验证它们。其手册明确警告,网络攻击者可以替换密钥,并建议通过带外方式验证输出,或只在受信网络中使用。将实时输出直接复制进 known_hosts,会把验证步骤变成记录最先回应者的内容。

通过独立的控制平面获取指纹:云实例控制台、镜像构建记录、由主机所有者审查的配置仓库,或管理员直接交接。存储主机名、端口、允许的主机密钥算法、指纹、所有者、环境和轮换流程。别忘了审查别名和跳板主机。最终目标可以被完美固定,但未固定的跳板主机会破坏整条信任路径。

一个实用的准入检查会比较观察到的密钥与已批准的密钥,而不改变信任关系:

ssh-keyscan -T 5 -t ed25519 host.example > observed.keys
ssh-keygen -lf observed.keys

指纹输出包括位长度、指纹、主机标签和密钥类型。人工或可信的清单服务应将指纹与独立提供的值进行比对。只有匹配后,自动化才可安装 known-hosts 条目。扫描本身是待比较的证据,不是可直接信任的证据。

在强制固定前规划好轮换。OpenSSH 的 UpdateHostKeys 可在服务器已通过现有可信密钥完成验证后学习额外密钥,支持渐进式轮换。无论使用这一扩展还是分发新的 known-hosts 集合,都要定义重叠期和紧急路径。密钥变更应停止执行并生成明确的身份错误。它绝不应触发普通重试、自动删除旧条目,或让批准卡要求匆忙的审查者接受来历不明的指纹。

在影响发生的边界要求幂等性

只有在任意部分执行后重复运行仍能产生相同预期状态、且不重复造成影响时,命令才能安全重试。只读语法不能赋予这种属性,零退出状态也不能证明它。

有些命令天生可重复:读取固定文件、检查服务状态,或在受控权限下使用 mkdir -p 创建目录。另一些命令需要保护条件。用 echo ... >> file 追加一行、发送通知、以生成的标识符创建用户、通过本地工具对账户收费,以及重启服务,都不会仅仅因为 shell 命令短小就变得安全。

在这一层,“重试暂时性 SSH 错误”是错误建议。它之所以流行,是因为重新连接能修复许多网络故障,也因为 HTTP 客户端库会标准化重试。SSH 可能在远程进程已提交变更、客户端尚未收到退出状态后失去连接。自动重试会重复已完成的操作。

把可重复性放进远程操作中。每个变更请求都应在批准前生成稳定的操作 ID。尽可能在同一事务中将该 ID 与影响一并存储。操作再次运行时,返回已记录的结果,而不是再次应用变更。如果标记与影响无法由一个事务覆盖,则增加能判断哪一侧已完成的核对查询。

一个小型部署脚本可以让这一契约清晰可见:

#!/bin/sh
set -eu

op_id=$1
release=$2
state_dir=/var/lib/agent-ops
record="$state_dir/$op_id"

test -d "$state_dir" || exit 70
if test -f "$record"; then
  cat "$record"
  exit 0
fi

current=$(/usr/bin/readlink /srv/app/current || true)
if test "$current" = "/srv/app/releases/$release"; then
  /usr/bin/printf '{"operation":"%s","state":"already-current"}\n' "$op_id"
  exit 0
fi

test -d "/srv/app/releases/$release" || exit 66
/usr/bin/ln -sfn "/srv/app/releases/$release" /srv/app/current.new
/usr/bin/mv -f /srv/app/current.new /srv/app/current
/usr/bin/printf '{"operation":"%s","state":"changed","release":"%s"}\n' \
  "$op_id" "$release" > "$record.tmp"
/usr/bin/mv -f "$record.tmp" "$record"
cat "$record"

这个示例并非在所有情况下都具备原子性。符号链接交换和操作记录是两次文件系统变更,因此在两者之间崩溃会留下空档。明确的 current 检查会核对这一特定空档。你的操作需要与自身影响相关的保护条件,而不是从这个脚本照搬通用标记。

将每条允许的命令归类为只读、收敛、去重或不可重复。收敛意味着重复执行会朝一个声明的状态推进,例如设置某个配置值。去重意味着远程端能识别操作 ID。不可重复操作在出现不确定性后,需要单独的状态查询和人工决定。如果所有者无法为命令分类,就不要准入。

向批准人展示影响,而不是 shell 文本

撤销正在运行的代理任务
Sessions 日志会显示正在运行的代理任务,并提供即时撤销控制。

只有审查者能在操作前识别目标、权限、预期影响和最坏可能后果,批准才有价值。原始 shell 文本是必要证据,却不是好的摘要。

比较 systemctl restart api 与一张写有以下内容的批准卡:生产主机 api-03,远程用户 deploy,重启服务 api,现有连接可能中断,操作 ID rel-2026-07-24-04,命令版本 restart-service/v2。第二种描述让审查者拥有可与变更核对的事实,也揭示了缺失的上下文。如果代理说不清会影响哪台主机或哪个服务,它就不应获得批准。

批准载荷应绑定到确切的执行请求。包括规范主机名和端口、已验证指纹、远程账户、命令或审查过的脚本摘要、规范化参数、工作目录、环境附加项、超时、请求的权限提升、操作 ID,以及该操作是否可重复。对这份载荷做哈希,并只执行已批准的哈希。否则代理可以为一条命令获得批准,再在发出前修改参数。

准确呈现 shell 引用方式,但不要让审查者在脑中执行它。只解析你拥有的命令形式。如果范围内仍有任意 shell 文本,应标记为任意内容,并完整呈现整个字符串,不作省略。标记重定向、命令替换、管道、后台执行、sudo、文件删除、权限变更、包管理、服务控制和网络下载。标记不是结论,而是在提示审查者后果可能藏在哪里。

影响越大,批准范围就必须越窄。针对已批准清单的固定只读探测,按会话批准可以合理。状态变更、使用特权远程账户,或参数决定目标的命令,适合逐次调用批准。不要因为两者共享 SSH 密钥,就让一次无害的 uname 批准悄悄授权后续部署。

Sallyport 的固定控制正好对应这一划分:其会话授权识别新的代理进程,而每次调用密钥可要求每次使用都获得批准。重要的设计工作仍在请求本身:批准卡必须说明远程影响,因为持有获批通道并不能解释一条命令会做什么。

测试批准完整性,而不只看界面。修改已批准参数中的一个字节,确认执行会停止。让两个请求竞争复用同一操作 ID。在批准和发送之间撤销会话。批准卡出现后锁定凭据存储。每项测试都应以已记录的拒绝或必须重新批准的请求结束,绝不可尽力继续执行。

将远程未知状态作为一等结果建模

只要客户端无法证明远程影响是否完成,未知就是有效结果。称它为失败会诱发重试,称它为成功则会掩盖未完成的工作。

来看一个常见过程。代理连接后启动脚本,脚本替换一个配置文件,服务开始重新加载。就在这时,网络路径中断。客户端既未收到退出状态,也未收到最终 stdout。本地超时触发,将调用标为失败。代理重试。第二次运行看到新文件,再次发送重新加载请求,还可能覆盖第一次运行的诊断记录。原始调用确实完成了一些工作,尽管客户端从未观察到完成。

RFC 4254 让这种歧义并不令人意外。协议将命令输出、退出状态、退出信号、EOF 和通道关闭作为独立消息传递。它建议返回退出状态,但不保证一定返回。即使通道干净关闭,也只告诉客户端通道的情况,无法说明外部系统是否达到所请求的业务状态。

上线前先定义状态机:

  1. not_started:连接、身份、认证或批准在发送前失败。
  2. started:远程端已接受命令,但尚无最终结果。
  3. completed:已收到最终状态和全部受限输出。
  4. unknown:可能已发送执行请求,但客户端失去了完成证明。
  5. reconciled:后续独立查询已确认最终状态。

通常只有 not_started 可以自动重试,即使这个标签也必须来自可信边界。如果承载命令的字节可能已到达服务器,就使用 unknown。截止时间不会取消远程进程,除非你有已确认的取消协议。关闭客户端套接字不是这种协议。

每条变更命令都需要在批准前指定核对方案。方案可以查询操作记录、比较已部署的发布标识符、读取服务管理器状态,或向下游系统查询稳定操作 ID。尽可能使用只读凭据进行核对。保留原始请求、其部分输出、时间戳、主机指纹和操作 ID,让后续查询能回答正确的问题。

设定未知状态预算。决定系统等待多久、谁会收到告警、哪些操作会被未解决的操作阻塞,以及何时转由人工处理。绝不要让两项不确定操作针对同一资源竞争。按资源串行化,或使用带所有者和过期策略、且能在客户端断连后存续的远程锁。

在扩展命令前限制远程账户

批准每次敏感的 SSH 使用
将密钥设为每次调用均需批准,让每项远程操作都等待你的决定。

SSH 是否就绪,更多取决于远程权限,而非客户端意图。再完美的批准界面,也无法弥补一个能改写主机的登录账户。

为代理通道创建专用账户。仅授予已准入操作所需的最小文件系统访问和服务权限。避免使用通用管理员账户。如需提权,仅允许固定路径且参数受控的指定命令。将不受限制的 sudo、允许程序中的 shell 转义、可写脚本目录和可写可执行文件,都视为通往更广权限的等价路径。

OpenSSH 的 authorized_keys 限制可以降低某个凭据的暴露面。根据设计,强制命令可以将每个连接路由到调度程序,而选项可以禁用伪终端、代理转发、X11 转发和端口转发。服务器配置也可限制转发。应使用服务器实际的手册并测试生效配置,因为一个宽松的 include 或 match 块就可能推翻你的假设。

调度程序应接受少量操作名称和数据,验证两者,并以绝对路径调用可执行文件,不重新拼接任意 shell 文本。例如,read-release 不接受参数,activate-release 则接受符合严格格式的发布标识符。SSH 通道仍是承载方式,但远程面开始更像定义明确的 API。

不要把开发者的认证代理转发到自主会话中。代理转发允许远程端在连接存续期间通过转发的套接字请求签名。受攻陷的远程主机未必能提取私钥,但可以使用签名能力。应给通道专用凭据,其服务器端授权本就受到严格限制。

检查到每个可执行文件和配置文件为止的整条文件系统所有权。如果受限账户可以修改父目录、替换调度程序、影响被加载的启动文件,或通过 PATH 抢先放置二进制文件,允许列表只是摆设。也要检查解释器。运行功能广泛的解释器的权限,通常就意味着该账户能做任何它有权限做的事。

让第一批命令保持简单:固定清单读取、输出受限的健康查询,以及一项有经过测试的核对路径的收敛式变更。端口转发、交互 shell、任意上传、包管理和自由形式的 root 命令,应留给后续审查,前提是它们确有必要。

证明在截断和断连情况下仍可观测

一次锁定所有操作
保险库锁定后,Sallyport 会在使用凭据前拒绝 HTTP 和 SSH 操作。

当审计证据无需依赖代理自己的摘要,就能重建授权、发送、远程身份和观察到的结果时,审计才算准备就绪。日志必须保留不确定性,而不是把它删改掉。

记录稳定的请求 ID 和操作 ID、代理进程或会话身份、批准决定、批准人方式、已批准载荷哈希、规范目标、主机密钥指纹、远程账户、开始时间、发送时间、最终时间、退出状态或信号、每个流的字节数、截断标志和最终状态分类。stdout 与 stderr 分开保存。如果策略不允许保留完整输出,保存允许的部分、摘要和清晰的保留元数据。

输出限制需要两种行为:停止本地收集,并决定远程端会发生什么。达到 1 MB 后直接关闭通道,可能仍会让进程继续运行。远程包装器可限制输出、将其发送到受控文件并报告摘要,但该包装器同样需要磁盘配额和清理机制。测试永不关闭 stdout 的命令、父进程退出后仍存活的子进程,以及持续向 stderr 写入的进程。

日志还应说明哪些事没有发生。主机密钥不匹配、保险库锁定、批准被拒、会话过期、请求格式错误或命令不被允许,都必须在返回前产生拒绝记录。否则运维人员只能看到一段空白,无法分辨系统悄无声息还是发生了绕过。

Sallyport 会将代理任务和单次调用记录到同一份加密、哈希链式审计日志中,sp audit verify 无需密钥即可在密文上离线检查该链。这为通道提供了防篡改的本地证据,但远程操作 ID 和核对结果仍需要出现在请求和结果中,运维人员才能将调用与机器状态对应起来。

收集事故审查者会看到的证据时,进行故障注入。分别在发送前、刚发送后、stdout 输出到一半时,以及远程进程退出后但本地完成前终止客户端。轮换主机密钥但不更新清单。在写入标记前填满远程文件系统。返回成功退出码但结构化输出格式错误。对每种情况都问一个问题:审查者能否看出使用了什么权限、哪些内容可能已改变,以及下一步必须做什么?

只有通过关卡后才准入 SSH

当团队能够证明其故障行为时,第二个通道才算准备就绪,而不是一条顺利执行的命令抵达测试主机时。使用带负责人和保留证据的书面关卡。

准入记录应包含:

  1. 一份通道契约,明确 shell、账户、目录、环境、超时、输出上限和结果架构。
  2. 一份已验证的主机清单,具有独立的指纹来源、跳板主机覆盖和经过测试的轮换流程。
  3. 一份命令目录,分类重复行为,并为每项变更指定核对查询。
  4. 一份批准规范,绑定到确切主机、账户、命令版本、参数、权限、超时和操作 ID。
  5. 故障注入结果,证明未知状态、拒绝、截断、撤销和审计重建。

先通过只读探测。接着在可丢弃环境中准入一项收敛式写入。在每个边界让它断开,并核对结果。随后在生产形态的主机上,使用无害资源重复测试。与拥有该主机的人一起审查证据,而不只与构建代理网关的团队审查。

将回滚与重试分开。回滚是一项新的、明确的变更,有自己的批准、操作 ID、前置条件和可能的未知状态。超时后自动运行反向命令,可能损害其实已正确完成的变更。系统必须先确定当前状态,才能再次改变该状态。

与准入标准同时设定移除标准。当脚本摘要未经审查就变更、主机离开清单、核对不再有效、输出变得无界,或运维人员无法解释未知结果时,禁用该操作。通道访问不是永久毕业证书。

SSH 应逐项操作获得准入。如果你无法固定服务器、说明影响、安全重复、区分所有结果状态并在事后重建调用,检查清单的正确结果就是“尚未准备好”。继续使用只读 HTTP 通道,先补齐缺失的边界,再避免远程 shell 将一次模糊的重试变成第二次生产变更。

常见问题

AI 代理何时可以使用 SSH?

当主机身份已固定、命令返回有类型的结果、变更可安全重复、批准绑定到确切影响,且断连会产生明确的未知状态时,代理才适合使用 SSH。一次成功的测试登录只能证明连通性。

先启用只读 SSH 是否足够安全?

这是合适的第一阶段,但远程账户仍应只有有限权限,输出也应受限。看似只读的命令可能执行启动文件、通过 PATH 调用意外的二进制程序,或在输出中暴露机密。

自动化 SSH 应使用 StrictHostKeyChecking no 吗?

不可以。应在运行前配置并验证主机密钥,然后使用 StrictHostKeyChecking=yes。关闭检查只是用自动化可能察觉不到的身份验证失败,换掉了一个操作提示。

ssh-keyscan 能安全地建立 known_hosts 吗?

ssh-keyscan 可以收集密钥,但不能验证收到的密钥。安装前,应将其指纹与通过独立可信渠道获得的值进行比对。

SSH 退出码 0 能证明变更成功吗?

它只能证明报告的远程命令状态为零。命令对成功的定义可能不可靠,管道可能掩盖先前的失败,预期的外部状态也可能仍然错误,因此应检查结构化输出或核对状态。

SSH 退出码 255 是什么意思?

OpenSSH 客户端遇到错误时使用 255,否则返回远程命令的状态。远程程序也可以自行选择 255,因此适配层应将传输状态与远程退出状态分开保存。

SSH 命令何时具有幂等性?

如果命令在任何一次部分执行后重复运行,都能达到相同的预期状态而不重复产生影响,它就是幂等的。应测试影响发生的边界,而非命令的写法,并使用稳定的操作 ID 或基于状态的保护条件。

代理应自动重试 SSH 超时吗?

只有系统能证明命令从未启动时才可以。只要可能已发出执行请求,就应将结果标记为未知,并在考虑再次变更前运行只读的核对查询。

SSH 批准卡应显示什么?

应显示规范主机名、已验证指纹、远程账户、预期影响、确切的命令或脚本版本、规范化参数、权限变更、超时和操作 ID。将批准绑定到这份载荷,确保点击后没有内容可以被更改。

团队应如何测试代理的 SSH 访问?

在远程执行前、执行中和执行后注入故障,包括主机密钥变更、输出溢出、信号、超时和断连。留下的证据应让运维人员知道哪些内容可能已改变,以及哪项核对操作是安全的。

Sallyport

Sallyport 替你的 AI 智能体执行 API 调用和 SSH 命令。密钥留在你 Mac 上的本地密钥库里;每次运行由你批准,每个操作都落入一份密封的审计日志。

© 2026 Sallyport · 依据 Apache-2.0 开源 · Oleg Sotnikov