工程

SSH 主机密钥 TOFU 及你的移动客户端为何应该在意

一份关于 Trust On First Use(TOFU)主机密钥验证的实用指南,并解释大多数移动 SSH 应用做错了什么。

CC Chen Chen· 创始人·2026 年 4 月 30 日·约 10 分钟

TOFU 到底是什么意思

Trust On First Use(首次连接信任)是一种安全模型:第一次连接到某台服务器时,你把它的标识记下来。从那以后,任何用不同标识回应的连接都值得怀疑。

对 SSH 而言,这个标识就是服务器的公开主机密钥——通常是 Ed25519 或 RSA 指纹。第一次 SSH 连到 prod-server.example.com 时,服务器会出示它的公钥。你要么信任(因为这台是你搭的),要么不信任(不是你搭的,有人在冒充)。一旦接受,指纹就被写入你的 known_hosts 文件。未来的连接必须出示同一把密钥,否则 OpenSSH 直接拒绝继续。

TOFU 并不完美——第一次连接是未经验证的——但它务实。握手过一次之后,即使在恶意网络里你也能挡掉主动中间人攻击。

为什么这件事在手机上更重要

手机一天里要在多个可信和不可信网络之间来回切换:家里 Wi-Fi、办公室 Wi-Fi、蜂窝、酒店那个一看就有问题的强制门户页、咖啡店里突然叫"Free WiFi Login"的网络。每一个都是攻击者可能潜伏、冒充你目标 SSH 服务器的位置。

如果你的 SSH 客户端正确保存了主机密钥,这些都无所谓。攻击者的密钥对不上,连接直接失败。如果你的客户端保存主机密钥(或每次都接受服务器发来的任何东西),那你就毫无防护——等于把凭证递给了任何在 22 端口上回应的人。

大多数移动客户端怎么做(差)

设计 TermAI 之前我测过好几款流行的移动 SSH 应用。最常见的默认行为:

  • 首次连接提示、再不复核。第一次连接时弹一个指纹对话框,你点接受,密钥被记下,之后每次默默匹配。这就是 OpenSSH 的行为——实现得当时没问题。
  • 首次连接提示但 UI 含混。同上,但对话框写着类似"无法验证服务器"和两个按钮"接受"/"取消"。大多数用户条件反射地点接受。更糟的是,密钥后续变了,界面给的还是同一个对话框,用户照样点接受。密钥变更应该看起来跟首次连接完全不一样。
  • 压根不管。有些应用就直接连,从来不显示指纹,也从来不在密钥变化时报警。这是设计上就坏掉的。

第三类比我预想的常见得多,付费产品里也有。

OpenSSH 是怎么做的

OpenSSH 的 known_hosts 文件是参考实现。行为如下:

# 首次连接
$ ssh prod-server
The authenticity of host 'prod-server' can't be established.
ED25519 key fingerprint is SHA256:abc...xyz.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'prod-server' to the list of known hosts.

# 之后,密钥若发生变更
$ ssh prod-server
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Host key verification failed.

首次提示客气,密钥不匹配时变得敌意明显,直接拒绝连接。你必须手动删掉旧条目才能继续。这种不对称——首次平静、变更激进——正是 TermAI 继承的设计。

TermAI 是怎么做的

我们把主机指纹存在设备上一个 AES 加密的 Hive 数据库里。加密密钥放在 iOS Keychain(或 Android Keystore 等价物)里,所以即使有人把数据库文件从设备里拿走,没系统钥匙串解锁也读不出来。

  1. 首次连接。服务器出示主机密钥,TermAI 算出 SHA256 指纹并展示:SHA256:abc...xyz。可以点击复制。接受按钮是默认选项,取消按钮也清晰可见。一旦接受,指纹就被存起来。
  2. 后续连接。指纹匹配已存值→连接默默继续。没有提示、没有警告、不打断。
  3. 后续连接出现不匹配。硬停。红色对话框。对话框解释发生了什么、同时显示已存指纹和新指纹,并且没有一键接受按钮。要继续,必须进入设置→Known Hosts明确删除已存指纹,再重新连接重新信任。这是刻意设计的两步流程。

两步流程是关键。如果一次点击就能覆盖密钥不匹配警告,用户在时间压力下会一路点过去。这个打断本身就是保护。

怎么看出真的中间人攻击

如果客户端警告密钥变了,要问的是:服务器密钥是因为正当原因真的换了,还是有什么在攻击这条连接?

正当原因:

  • 你给服务器重装了系统
  • 你主动轮换了主机密钥
  • 你把 DNS 主机名指向了另一台服务器
  • 服务器在负载均衡器后面,这次连到了不同的后端(应给所有后端配同一把密钥)

可疑原因:

  • 你在一个不完全信任的新 Wi-Fi 上
  • 你刚遇到一个意料之外的强制门户
  • 服务器管理员没告诉你要换密钥
  • 你在合法服务器上对过之后,这指纹依然完全陌生

不确定时的正确做法:别连。打开服务器的网页控制台 / 云服务商的串口控制台 / 同网段一台可信机器,对服务器跑 ssh-keyscan,对比指纹。如果跟你客户端看到的新密钥一致,就没事。不一致,那你在跟中间人通话。

合法的密钥变更

如果你打算正当地轮换主机密钥(最佳实践:每两年左右一次),有干净的做法让客户端不至于全慌:

  1. 生成新主机密钥,但保留旧的可接受状态
  2. 过渡期内让 sshd 同时提供两把密钥
  3. 通过用户信任的渠道把新指纹分发出去(团队密码管理器、签名公告)
  4. 所有人都更新完成后,把旧密钥移除

个人服务器更简单:跟用的人提前说"我周二轮换主机密钥,新指纹是 XX"。打个招呼就把一个警告对话框变成预期内的确认。

快速事实

  • TOFU = 首次使用即信任:与 OpenSSH 相同的主机密钥模型
  • 按连接固定密钥:密钥变化时给出同时显示新旧指纹的警告
  • 带外核实:在服务器上跑 ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub,对比后再接受
  • 防中间人:密钥变化正是中间人攻击的样子,所以永不自动接受
  • 永不关闭校验:StrictHostKeyChecking no 会拆掉对中间人唯一的防御
试试 TermAI

免费上手,iOS 和 Android 都有。免费档每天 5 次 AI,SSH/SFTP 不限量,内置 Tailscale。

CC
Chen Chen — Founder of TermAI

Writes about mobile DevOps, terminal UX, and the surprising depth of "boring" infrastructure.

Was this useful? ← Back to blog