当(且仅当) DNSSEC 正常工作时, SSHFP 记录可被视为 SSH 指纹的可信来源,以缓解 TOFU 问题和一些简单的 MITM 攻击。

一定记得先搞个支持 DNSSEC 的域名把 A/AAAA 记录指到你要配置 SSHFP 的机器的 IP(v4/v6) 地址,比如

sub.example.com. 600 IN A 1.2.3.4
sub.example.com. 600 IN AAAA 2001:da8::1 

然后还要记得打开 DNSSEC, 不然白搭。

RRType 参数

SSHFP 类型记录由 RFC4255 定义,新算法载于 RFC6594、RFC7479 和 RFC8709。[IANA 文档]

这里的指纹以十六进制数字表示。

SSHFP <KEY-ALGO> <HASH-TYPE> <FINGERPRINT>

密钥/哈希算法在记录中的对应值如下:

Value Key Algorithm Reference
0 Reserved RFC4255
1 RSA RFC4255
2 DSA RFC4255
3 ECDSA RFC6594
4 Ed25519 RFC7479
5 Unassigned
6 Ed448 RFC8709
Value Hash Algorithm Reference
0 Reserved RFC4255
1 SHA-1 RFC4255
2 SHA-256 RFC6594

完整的 SSHFP 记录如下:

sub.example.com. 1 IN SSHFP 4 2 EA34384D651008B503FA6FE9C205C350747C7A6FDE7FB858827B1BD21252A22A

个人而言,只推荐使用 SHA-256(2) 类型的哈希值。

生成 SSHFP 记录

在获取服务器的指纹时,你应该通过 SSH 以外的渠道。例如 VNC 或者直接接触到机器。

但对 SSH 主机密钥的 MITM 并不常见,对吧?XD 只需记录指纹并稍后验证即可,出了问题再说。

使用 ssh-keygen -r sub.example.com 生成 SSHFP 记录,其中 sub.example.com 得有 A/AAAA 记录,指到你要配置 SSHFP 的机器的 IP(v4/v6) 地址。

出来的结果大概长这样:

sub.example.com IN SSHFP 1 1 f1d42234c0cff9d91c81dfcc4e3dddccb1b46291
sub.example.com IN SSHFP 1 2 38c3012a84307d1e318757e272c222e88ed4c7157acfc701cb3d9863efe742bd
sub.example.com IN SSHFP 3 1 854ad9e6999aafe3c232ca2fd010631a905b528d
sub.example.com IN SSHFP 3 2 75bbca71294fb4c53a7b06750124a15765715f864e1695d1ebafe2ac5be6f3ba
sub.example.com IN SSHFP 4 1 6545295ef9703ec1306f0db66c71a8faab4c9210
sub.example.com IN SSHFP 4 2 39f62f08439af467dae63e165477f1904c1c73fa94f7f085deaa654a3ae7a6db

一股脑加上当然没问题,但我建议放弃 SHA-1 指纹,毕竟 SHA-256 已经遍地都是了,SHA-1 也不太安全了。

那就砍掉了一半的工作量:

sub.example.com IN SSHFP 1 2 38c3012a84307d1e318757e272c222e88ed4c7157acfc701cb3d9863efe742bd
sub.example.com IN SSHFP 3 2 75bbca71294fb4c53a7b06750124a15765715f864e1695d1ebafe2ac5be6f3ba
sub.example.com IN SSHFP 4 2 39f62f08439af467dae63e165477f1904c1c73fa94f7f085deaa654a3ae7a6db

往域名的 DNS 里面导入记录就行了。

客户端配置

OpenSSH 默认不查询 SSHFP 记录。你需要在 ssh 命令后面添加参数 -o VerifyHostKeyDNS=yes , 或者在 ssh 配置文件里,比如 ~/.ssh/config, 添加设置字段 VerifyHostKeyDNS yes . [APNIC Blog]

你可以先试着把 known_hosts 移走(比如重命名为 known_hosts.bak),然后用 ssh 连接你的机器:

ssh -o VerifyHostKeyDNS=yes <USERNAME>@sub.example.com

如果没有任何关于 HostKey 的提示,直接登进去了,那就说明已经配好了。

其他 SSH 客户端可能还没有实现这一功能,仍然需要手动确认指纹。但现在 SHA256 指纹通常以 base64 格式而非(SSHFP 采用的)十六进制显示,比如 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU

因此要进行转换。先获取 SSHFP 记录(执行 dig SSHFP sub.example.com +short 或其他玩意,比如 Dig Web Interface 或者 dns.google),然后通过 cmdline 或使用 CyberChef 将其转换为 base64。

echo "$HEX_FINGERPRINT" | xxd -r -p | base64

Troubleshooting as Mentioned Here

别慌,不是烂尾,只是没翻译引用的原文(

You may sometimes encounter this:

$ ssh root@examplehost.example.org

The authenticity of host 'examplehost.example.org (192.0.2.123)' can't be established.
ECDSA key fingerprint is SHA256:MH85JK0yq+JNl1lPKUlxit+dGFqWMS/MmohcINp/e9Q.
Matching host key fingerprint found in DNS.
Are you sure you want to continue connecting (yes/no/[fingerprint])?

This message usually indicates an issue with DNSSEC. OpenSSH can find the SSHFP records but is not able to validate them using DNSSEC. You should check that DNSSEC is set up correctly for your domain using a tool like DNSViz. You should also check that your DNS resolver supports DNSSEC. You can do this using dig:

$ dig examplehost.example.org

...

;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

...

Look for the flags section and check for the ad flag, indicating that the response has been DNSSEC validated. If the ad flag doesn’t show, check the default DNS servers for your system. Some ISP or home router DNS servers may not fully support DNSSEC.

好吧,但问题不是出在这里。DNSviz 和 dig 都显示我的 DNSSEC 设置没有问题。

这人也没辙

但我的一些 ssh 客户端(arch、termux)可以正常工作,所以我看了看 debug 日志,发现了一些东西:

debug2: ldns: got 1 answers from DNS
debug1: found 1 secure fingerprints in DNS
debug1: verify_host_key_dns: matched SSHFP type 4 fptype 2
debug1: matching host key fingerprint found in DNS

DNSSEC 验证是用 ldns 处理的

问题可能是 Debian 上没有支持 DNSSEC 的解析器。但 Debian 没有将 ldnsopenssh 打包,因此需要额外的 DNSSEC 解析器。

献给 Debian 的命令行胶水

安装 ldns 相关的包不起作用。因此我决定为 ssh 架一个专门的 DNS 解析器,覆盖系统默认的设置。

unbound 是一个 DNSSEC 解析器。也有其他不错的选择,但与 dns-over-https 不同,unbound 是在 Debian 软件仓库里的,你不必下载一堆依赖来编译,可以省去很多麻烦。

设定 也非常简单。

unbound-anchor 需要用于根密钥固定。拆包拆疯了属于是。

sudo apt install unbound unbound-anchor

查看 etc/unbound/unbound.conf 中的配置,默认的配置应该已经足够了。建议将监听地址限制为 127.0.0.1

启动吧。

sudo unbound-anchor
sudo systemctl enable --now unbound

使用 resolvconf-override 覆盖默认 DNS 服务器,并使用 RES_OPTIONS=trust-ad 让 SSH 客户端信任 DNS 解析器回答的 AD 位。显然,在这个设置下,AD 位来自本地自架的 unbound 服务,篡改不来。

resolvconf-override 需要自己编译。

git clone https://gitlab.freedesktop.org/hadess/resolvconf-override.git
cd resolvconf-override

mkdir build
cd build
meson ..
ninja

sudo cp libresolvconf-override.so /usr/lib64/

然后在 ssh 命令前添加一堆环境变量。

LD_PRELOAD=/usr/lib64/libresolvconf-override.so NAMESERVER1=127.0.0.1 RES_OPTIONS=trust-ad ssh -o "VerifyHostKeyDNS=yes" username@hostname

那为什么不在 shell 中添加别名呢?

# ~/.bashrc or ~/.zshrc
alias sshr='LD_PRELOAD=/usr/lib64/libresolvconf-override.so NAMESERVER1=127.0.0.1 RES_OPTIONS=trust-ad ssh -o "VerifyHostKeyDNS=yes"'