SSH 上的密码认证是一台普通服务器最大的暴露面。对付它,Fail2ban 和任何阈值都没用:一千个地址每小时各试一次,永远到不了上限,而只要猜测还有可能,它们就会一直猜下去。密钥直接消除了这种可能性,而在整份 hardening 清单里,这是唯一一项能质变地改变局面的设置。
关于它只有一件事让人害怕:关掉密码之后把自己留在门外。下面是让这件事不可能发生的顺序。
密钥
在你自己的机器上生成,而不是在服务器上:
ssh-keygen -t ed25519 -C "work laptop"
用 ed25519 而不是 RSA:更短、更快,而且没有长度方面的问题。只有需要够到某些很旧的东西时才需要 RSA;那时用 -t rsa -b 4096。
给密钥设置口令短语是值得的:密钥文件可能从笔记本上被偷走,而没有口令短语它立刻就能用。你不必每次都输——那由 ssh-agent 负责,而在 macOS 和多数桌面 Linux 上它本来就在运行。
趁密码还能用的时候,复制到服务器只需一条命令:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@203.0.113.25
如果没有 ssh-copy-id,就把 .pub 文件的内容作为一行追加到服务器的 ~/.ssh/authorized_keys 里。权限很重要:.ssh 目录是 700,文件是 600,属主是该用户。换成别的值,sshd 会悄悄拒绝读取这个文件,而这正是「密钥不管用」最常见的原因。
一锤定音的检查
在关掉任何东西之前,在不关闭第一个连接的情况下打开第二个:
ssh -o PasswordAuthentication=no user@203.0.113.25
这个选项禁止客户端退回去用密码——所以如果登录成功了,那就是用密钥成功的。在这条命令能跑通之前,不要往下走。而且把第一个连接一直开到最后:正是它能修好你可能弄坏的一切。
关掉密码,以及一个不显眼的陷阱
设置放进独立文件,好让包更新覆盖不掉它。但文件名很重要:
sudo tee /etc/ssh/sshd_config.d/10-hardening.conf <<'EOF'
PasswordAuthentication no
PermitRootLogin no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
MaxAuthTries 3
LogLevel VERBOSE
EOF
关键在于,OpenSSH 对某个参数采用的是它取到的第一个值,而不是像几乎所有其他地方那样取最后一个。sshd_config.d 里的文件按字母序读取,而 Ubuntu 的云镜像会在那里放一个 50-cloud-init.conf,它里面常常含有 PasswordAuthentication yes。在这种情况下,你那个编号 90 的文件什么也做不了:设置看起来已经生效,而密码照常可用。数字 10 正由此而来——它被更早读到。
实际生效的结果不必靠猜就能看到:
sudo sshd -T | grep -Ei 'passwordauth|permitrootlogin|pubkeyauth'
这条命令打印的是把所有 include 都算进去之后的有效配置。请相信它,而不是文件的内容。
然后是语法检查和轻柔地重载服务:
sudo sshd -t && sudo systemctl reload ssh
sshd -t 是强制的:配置里的一个笔误加上 restart,会让服务停掉而你被留在外面。reload 也不会切断已经打开的会话。
Ubuntu 24.04:端口不在你以为的地方
较新的 Ubuntu 上有个单独的陷阱:那里的 sshd 是通过 systemd 的 socket 启动的。在这种模式下,sshd_config 里的 Port 那一行什么也不做——监听的是 socket 而不是守护进程。如果你要改端口,该改的是这个:
sudo systemctl edit ssh.socket
并且要在 ListenStream=(空值用来重置之前的值)里写上新端口。暴露它的症状是:配置改了,服务重启了,而服务器仍然在 22 上应答。
既然说到改端口:作为一种防护它不起作用——扫描器几分钟内就能在任何端口上找到服务。它唯一的效果是日志更安静,因为大规模猜测只冲着 22 去。这很方便,但别把它和安全混为一谈。
究竟允许谁登录
一个有用的补充是一份明确的名单:
AllowUsers deploy admin
不在名单上的一切,在密钥被检查之前就被切断了。这也顺带覆盖了某个软件包创建了带 shell 和家目录的系统用户的情况。
备用的进门方式
一台笔记本上的一把密钥是单点故障。硬盘坏了、笔记本丢了,服务器就永远够不着了。合理的最低限度是:
authorized_keys里放一把来自另一台设备的第二密钥;- 私钥的副本放在密码管理器里或加密的 U 盘上;
- 经过验证的服务商控制台访问——VNC 或串口。经过验证的意思是你至少通过它登录过一次,而不是「面板里某处有个按钮」。
顺便看看 authorized_keys 里现在都有什么——你的用户和 root 的都要看。躺在那里的陌生密钥能挺过任何一次改密码,并且仍然是可用的访问权限。
之后
密码猜测不会消失,它只是变得毫无用处:日志里仍会有成千上万行 Failed password,而其中没有一行能以成功收尾。现在值得盯的是成功的登录:来自哪个地址、用哪个用户、在什么时间。来自陌生地址的一次成功密钥登录,是比一百万次失败重要得多的事件,而且在总的洪流里要难发现得多。下面的演示页面展示的正是这幅画面。