新服务器上线几分钟,/var/log/auth.log 里就会开始刷 Failed password。这是背景噪音,不是针对你——公网上的扫描器是无差别扫全网的。
我随手在一台闲置小机上统计过一天的量:lastb 显示失败尝试 1382 次,来源横跨十几个国家的 IP 段。结论前置:对付它的思路不是"设个复杂密码",而是把"密码"这条攻击面整个删掉。 五步走完,爆破就从"能不能猜中"变成"根本没有可猜的东西"。
威胁模型长这样

公网扫描器 → 22/tcp → 拿用户名表 + 密码字典硬撞 → 撞中就登入。加固的目标是让中间那一步无处下手:密码关掉、root 关掉、试错还被自动封 IP。
操作步骤速览

一、先配密钥,再谈禁密码(顺序不能反)

$ ssh-keygen -t ed25519 -C "ops@laptop"
$ ssh-copy-id -i ~/.ssh/id_ed25519.pub deploy@your-server
$ ssh -i ~/.ssh/id_ed25519 deploy@your-server # 确认能免密登入
ed25519 优于 RSA:更短、更快、同等强度。ssh-copy-id 会把公钥追加到服务器的 ~/.ssh/authorized_keys。
最后一步必须在禁用密码之前跑通——先确认"密钥能进",否则下一步操作会让你把自己锁在门外。
二、改 sshd:禁密码、禁 root 直登

新建 /etc/ssh/sshd_config.d/99-hardening.conf(比直接改主配置更干净,也不惧系统升级覆盖):
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
AllowUsers deploy
MaxAuthTries 3
LoginGraceTime 20
PasswordAuthentication no:核心,关掉密码验证;PermitRootLogin prohibit-password:root 只允许密钥,不允许密码;AllowUsers deploy:白名单一个普通账号,收缩可登录面;MaxAuthTries 3:单连接最多试 3 次,直接掐掉字典遍历。
$ sshd -t && systemctl reload ssh # 语法检查通过再平滑重载
sshd -t 先验证语法,别让一条手滑的配置直接把 SSH 打死。改配置时保持另一个 SSH 会话开着,方便改崩了立刻回滚。
三、fail2ban:让爆破源自动进小黑屋

$ apt install -y fail2ban
/etc/fail2ban/jail.local:
[DEFAULT]
backend = systemd
bantime = 1d
findtime = 10m
maxretry = 3
[sshd]
enabled = true
# 累犯加倍:被 ban 过 3 次的 IP,关一周
[sshd-recidive]
enabled = true
filter = sshd
banaction = iptables-multiport
bantime = 1w
findtime = 1d
maxretry = 3
$ systemctl enable --now fail2ban
$ fail2ban-client status sshd # 看当前封禁列表
含义:10 分钟内错 3 次,封 1 天。shd-recidive 那一段更狠——被放过一次还不改的,直接关一周。参数按自己习惯调,bantime 拉长更省心。
四、锦上添花:改端口 + 二步验证

改端口(可选,纯降噪),在 99-hardening.conf 里加:
Port 22022
⚠️ 改完必须同步放行防火墙和云安全组,否则下次连不上。别忘了 ufw allow 22022/tcp 或云控制台的入站规则。
二步验证(TOTP),给密钥再加一层:
$ apt install -y libpam-google-authenticator
$ google-authenticator # 扫码,记下备用码
然后在 /etc/pam.d/sshd 顶部加 auth required pam_google_authenticator.so,并在 sshd_config 里设:
AuthenticationMethods publickey,keyboard-interactive
KbdInteractiveAuthentication yes
此后登录 = 密钥 + 6 位动态码。备用码(emergency scratch codes)一定要抄下来,手机丢了是唯一指望。
五、审计:谁在敲你的门
加固之后再看数据,会有一种"后院清净了"的感觉。上手就三条命令:
# 最近的失败登录(需 sudo)
$ lastb | head -20
$ grep -c "Failed password" /var/log/auth.log # 当日失败次数
# 新版系统走 journald
$ journalctl -u ssh --since today | grep -c Failed
# 被 fail2ban 封过的 IP 排行
$ zgrep -h "Ban " /var/log/fail2ban.log* | awk '{print $NF}' | sort | uniq -c | sort -rn | head
看 lastb 你会确认一件事:绝大多数失败尝试来自少数几个 IP 段反复扫,而且用户名全是 root、admin、test 这类通用名。这也解释了为什么 AllowUsers 白名单特别好用——真有人不厌其烦地猜,结果连个合法用户名都没猜对。
再进一步,如果你管理的不止一台机器,别在每台上都开 SSH 对公网。收敛到一台跳板机(堡垒机):其余机器的 22 端口只允许跳板机访问,日常先登跳板机再内网跳过去。攻击面从 N 台缩到 1 台,日志也集中在一处,省心得多。
五个常见的坑
- 禁密码前没验证密钥:最常见的自锁死法。永远先确认密钥能登入,并确认 VNC / 救援控制台可用。
- 改了端口没放行:防火墙、云安全组、以及可能存在的 CDN 端口白名单都得同步改。
- fail2ban 读不到日志:Debian/Ubuntu 新版默认走 systemd journal,
jail.local里要设backend = systemd并装python3-systemd,否则它一直"看不见"爆破。 - AllowUsers 把自己也锁了:写白名单时,你当前用的账号必须在列;
root若要保留也要显式列出。 - 二步验证加了却发现密钥登录也要求输码:
AuthenticationMethods与 PAM 顺序要匹配,改完在另一个会话里验证,别关掉当前连接。
FAQ
Q:加固完还能用密码从手机连吗?
不能——这正是目的。手机端把私钥导进 Termius / JuiceSSH 即可,体验和密码登录一样。
Q:fail2ban 会不会误封我自己?
正常使用不会(有密钥直接过)。若你反复输错口令或忘带 key,可能被自己封,fail2ban-client set sshd unbanip <你的IP> 解封即可。
Q:要上 Cloudflare 之类挡一下吗?
SSH 走的是自建端口,CF 挡不了。想加一道,可以用 CF Zero Trust / 跳板机,但那属于进阶玩法,对绝大多数站没必要。
Q:加固后 SSH 速度会变慢吗?
几乎不会。密钥握手比密码验证更快,fail2ban 只对失败连接生效,正常登录走的是"密钥直接过"的快路径,体感反而更跟手。
Q:多台机器要一台台配吗?
配置本身可以复制,但建议把这份 99-hardening.conf 存进你的配置仓库(或 rsync 一套基线过去),新机器上线直接落地。一次写对、长期复用,这才是运维效率的来源。
发布前自查
- [ ] 密钥可免密登入,且同步保留了一个已登录会话
- [ ]
sshd -t通过后 reload,未使用 restart - [ ] fail2ban 状态显示
sshd已启用 - [ ] 端口变更已同步到防火墙 + 云安全组
- [ ] VPS 控制台 / 救援模式可用(最后的退路)
一个容易被忽略的习惯:把这份
99-hardening.conf连同jail.local一起存进你的配置仓库。新机器上线时直接落地,比每次凭记忆重敲一遍可靠得多——而且出问题时能一眼看出"跟标准配置差在哪"。
小结
加固的本质是减法:删掉密码这条路、删掉 root 直登这条路,剩下的只有"持有密钥的人"能进。再加 fail2ban 兜底、二步验证加码,服务器就从不设防的大楼变成一把需要两把钥匙的锁。
全程十几分钟,之后长期免维护。这类"做一次、省心几年"的事,是运维里投入产出比最高的。
常见问题
禁用密码登录后,私钥丢了怎么办?
绝大多数 VPS 商的控制台(VNC / 救援模式)能绕过 SSH 登进去改配置。所以改配置前先确认你的控制台可用,并且私钥一定多端备份。
把 SSH 端口从 22 改掉算安全措施吗?
只能降噪,不能防御。真正起作用的是"只允许密钥 + fail2ban"。端口改不改都行,别指望一个非标准端口挡住有备而来的人。