云端 Mac SSH 主机密钥校验与安全轮换实战

云端 Mac SSH 主机密钥校验与安全轮换实战

开发者第一次连入云端 Mac 时,终端通常会询问是否接受未知主机。手工操作里输入 yes 很快,放进 CI 后改用 StrictHostKeyChecking=no 更快,但两种做法都绕过了身份校验。只要 DNS、地址配置或链路被错误导向,凭据和构建材料就可能发送给错误的机器。正确做法是先从可信终端取得主机密钥指纹,再让开发机和自动化任务只接受这把密钥。

先建立独立的信任基准

主机密钥用于证明“当前连接的是预期中的那台 Mac”,它不同于用户登录所用的私钥。以 Ed25519 主机密钥为例,可通过 XcodeVM 控制台提供的 Web VNC 打开本机终端,在目标机器上读取公钥指纹:

sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256

记录完整的 SHA256: 值,同时记录实例标识、连接地址和读取方式。不要只通过待验证的 SSH 链路获取这个值,否则验证会形成闭环。

随后在开发机读取远端当前提供的公钥:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan -T 5 -t ed25519 "$MAC_HOST" > ~/.ssh/xcodevm_known_hosts.new
ssh-keygen -lf ~/.ssh/xcodevm_known_hosts.new -E sha256

逐字符比较两处指纹。匹配后再安装文件:

install -m 600 ~/.ssh/xcodevm_known_hosts.new ~/.ssh/xcodevm_known_hosts
rm -f ~/.ssh/xcodevm_known_hosts.new

ssh-keyscan 解决的是“读取公钥”,不是“证明公钥可信”。没有独立指纹作基准时,扫描结果不能直接进入生产信任文件。

将 known_hosts 与项目隔离

全局 ~/.ssh/known_hosts 适合日常交互,但团队项目最好使用独立文件。这样可以明确审查变更,也不会因个人清理记录而影响自动化任务。

~/.ssh/config 中建立别名:

Host xcodevm-build
    HostName 192.0.2.10
    User builder
    IdentityFile ~/.ssh/xcodevm_build_ed25519
    IdentitiesOnly yes
    UserKnownHostsFile ~/.ssh/xcodevm_known_hosts
    StrictHostKeyChecking yes
    HashKnownHosts yes
    ServerAliveInterval 30
    ServerAliveCountMax 3

IdentitiesOnly yes 可避免客户端依次尝试过多身份文件;StrictHostKeyChecking yes 确保未知或变化的密钥直接失败。连接前检查配置展开结果,避免别名被其他规则覆盖:

ssh -G xcodevm-build |
  grep -E '^(hostname|user|identityfile|userknownhostsfile|stricthostkeychecking) '

如果同一项目有多台机器,应为每台机器保存独立条目,并将实例标识写入内部变更记录,不要用一个宽泛别名指向会随机变化的地址。

在 CI 中做指纹固定

CI 不应永久继承某位开发者的个人信任文件。更稳妥的方式是把预期 SHA256 指纹作为受保护变量保存,每次任务先扫描、比较,再建立临时 known_hosts

set -euo pipefail

: "${MAC_HOST:?MAC_HOST is required}"
: "${EXPECTED_ED25519_SHA256:?fingerprint is required}"

scan_file="$(mktemp)"
known_hosts_file="$(mktemp)"
trap 'rm -f "$scan_file" "$known_hosts_file"' EXIT

ssh-keyscan -T 5 -t ed25519 "$MAC_HOST" > "$scan_file" 2>/dev/null
actual="$(ssh-keygen -lf "$scan_file" -E sha256 | awk '{print $2}')"

if [ "$actual" != "$EXPECTED_ED25519_SHA256" ]; then
  printf '%s
' "SSH host key verification failed" >&2
  exit 1
fi

install -m 600 "$scan_file" "$known_hosts_file"

ssh \
  -o UserKnownHostsFile="$known_hosts_file" \
  -o StrictHostKeyChecking=yes \
  -o IdentitiesOnly=yes \
  "$MAC_USER@$MAC_HOST" \
  'uname -m && sw_vers -productVersion'

私钥、预期指纹和主机地址应分别管理。日志可以输出实际指纹帮助排查,但不要打印私钥内容、连接密码或未脱敏的环境变量。

正确处理密钥变化

主机密钥变化可能来自系统重置、主机密钥重新生成、连接地址复用,也可能代表连接被导向错误目标。看到 REMOTE HOST IDENTIFICATION HAS CHANGED 时,应先停止自动任务,不能直接执行删除记录命令后重试。

安全轮换按以下顺序进行:

  1. 在控制台确认实例标识、连接地址和近期操作是否符合预期。
  2. 通过 Web VNC 的本机终端重新读取 Ed25519 公钥指纹。
  3. 让另一位维护者复核新旧指纹及变更原因。
  4. 扫描远端公钥并验证新指纹,生成新的信任文件。
  5. 先更新测试任务,完成一次只读连接和环境检查。
  6. 再更新正式任务,并撤销旧指纹。

若使用哈希化记录,可先定位旧条目:

ssh-keygen -F "$MAC_HOST" -f ~/.ssh/xcodevm_known_hosts
ssh-keygen -R "$MAC_HOST" -f ~/.ssh/xcodevm_known_hosts

第二条命令只能在新指纹已经独立确认后执行。删除旧记录只是轮换动作,不是验证动作。

用检查清单完成验收

上线前至少检查以下项目:

检查项 合格条件
指纹来源 从目标机器本机终端读取
密钥算法 明确固定 Ed25519,不接受任意算法
文件权限 .ssh 为 700,信任文件为 600
客户端策略 StrictHostKeyChecking=yes
CI 隔离 每个任务使用独立临时文件
变化处理 先停任务、再核验、后替换
日志内容 保留指纹与失败阶段,不暴露凭据

还应执行一次负向测试:临时把预期指纹改错,确认任务在真正运行远端命令前退出。然后恢复正确值,再验证只读命令、仓库访问和构建入口。这样才能证明保护措施确实参与执行,而不是仅存在于配置文件中。

SSH 信任链不需要复杂系统,关键是把“首次接受”和“变化后重连”从临场点击改成可审查流程。可信终端提供基准,项目文件保存固定关系,CI 在每次连接前验证,任何变化都先停下来核对。完成这些步骤后,远程开发和自动构建才能共享同一套明确的主机身份边界。

常见问题

ssh-keyscan 得到的主机密钥可以直接信任吗?

不可以。ssh-keyscan 只负责读取远端当前返回的公钥,必须再与通过本机终端等独立可信路径取得的 SHA256 指纹比较,匹配后才能写入受信任文件。

主机密钥变化后可以删除 known_hosts 记录并重新连接吗?

不能把删除记录当作验证。应先暂停连接,确认实例地址或系统状态是否变化,再从可信终端重新读取指纹;只有新指纹核对一致后才替换旧记录。

CI 应共用开发者个人的 known_hosts 吗?

不建议。CI 应使用权限为 600 的任务专用文件,显式开启 StrictHostKeyChecking,并在任务结束时删除临时文件,避免不同项目共享信任状态。

XcodeVM 云端 Mac

选择适合当前工作负载的独享物理机

核对内存、存储、节点与计费周期后进入下单配置。所有节点全年正常运行,实际可用性以控制台实时返回为准。

选择方案并租用