服务器 SSH 加固实战:密钥登录、fail2ban 与账号模型的分层落地

为什么裸奔的 SSH 端口是个人服务器最大的风险敞口

大多数个人站长的服务器,唯一长期暴露在公网的服务其实不是 80/443,而是 22 端口。Web 服务前面通常还有 CDN、WAF 挡着,SSH 却往往是真的「裸奔」:直接对全网开放,只靠一个密码保护。只要 IP 被扫描到——而公网扫描是持续的、自动化的、不针对你个人的——爆破就开始了。

这篇文章不讲「改成非标准端口就安全了」这种半对半错的说法,而是把 SSH 加固拆成可独立验证的几层:认证方式、访问来源、账号模型、以及出问题时你怎么还能进去。最后一条常常被忽略,但它是最重要的。

第一层:先关掉密码登录,这是收益最大的一步

密码登录意味着攻击面是「所有可能的密码」,而且是通过网络逐次尝试的。改成密钥登录后,攻击面变成「所有可能的私钥」——在数学上不可枚举。这一步的收益远大于其他所有加固措施之和。

# 本地生成密钥(推荐 ed25519,比 RSA 更短更强)
ssh-keygen -t ed25519 -C "admin@mysite" -f ~/.ssh/id_ed25519_site
# 不要把私钥复制到服务器上,只上传公钥

# 上传公钥(第一次用密码登录完成这一步)
ssh-copy-id -i ~/.ssh/id_ed25519_site.pub -p 22 root@1.2.3.4

# 验证密钥能登录之后,再改服务端配置
# /etc/ssh/sshd_config
PasswordAuthentication no
PubkeyAuthentication yes
PermitRootLogin prohibit-password
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes

改完配置不要直接重启,先做语法检查再平滑重载:

sshd -t                    # 语法检查,有错会明确报行号
systemctl reload sshd      # 重载而不是重启,已建立的连接不断

关键顺序问题:一定要先确认密钥登录可用,再关密码登录。正确做法是保持当前 SSH 会话开着,另开一个终端用密钥登录一次。成功了再 reload。如果顺序反了,你会把自己锁在门外——这时候只能走 VNC 控制台或救援模式,代价远大于多花两分钟验证。

第二层:fail2ban 与 sshd 自身的限速,各自的适用场景

关掉密码登录后,爆破已经基本无效。但日志里的噪声还在,而且如果有人用你已授权的某个用户密钥泄漏(比如笔记本丢失)试图登录,你希望在暴力尝试阶段就被挡住。这时需要限速。

两种方案各有侧重,可以叠加:

  • sshd 内建限速(MaxAuthTries / MaxStartups):无需额外软件,立即生效。MaxAuthTries 3 表示单连接最多试 3 次就断;MaxStartups 10:30:60 表示并发未认证连接超过 10 后按 30% 概率随机拒绝,超过 60 全拒。它能挡住「单连接猛试」。
  • fail2ban:读日志、动态封 IP,能挡住「多连接、分散来源」的慢速爆破。代价是多一个常驻进程与一套 iptables/nftables 规则,需要正确配置 ignoreip 否则会封掉自己的 IP。
# fail2ban 的 sshd jail 精简配置 /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled  = true
port     = ssh
maxretry = 4
findtime = 600
bantime  = 3600
# 务必白名单,否则被 CDN 回源 IP 或自己动态 IP 封掉后很难受
ignoreip = 127.0.0.1/8 ::1 你的固定出口IP

验证 fail2ban 真的在工作,别只看它 is running:

fail2ban-client status sshd
# 关注 "Currently banned" 与 "Total banned"。如果 Total banned 一直是 0,
# 大概率是日志路径配错了(容器/非 systemd 环境常见),而不是没人爆破。

# 手动验证一次封禁逻辑
fail2ban-client set sshd banip 203.0.113.9
iptables -L -n | grep 203.0.113.9   # 或 nft list ruleset
fail2ban-client set sshd unbanip 203.0.113.9

第三层:账号模型——比端口和密码更根本的问题

很多个人服务器的真实状态是:只有 root 一个账号,所有操作都用它。这在协议层面就不安全——PermitRootLogin yes 让攻击者连「用户名」都不用猜,一半信息已经送出去了。正确模型是三层:

# 1. 建一个普通用户
useradd -m -s /bin/bash deploy
mkdir -p /home/deploy/.ssh && chmod 700 /home/deploy/.ssh
# 把公钥放到 /home/deploy/.ssh/authorized_keys,权限 600
chown -R deploy:deploy /home/deploy/.ssh

# 2. 只给它需要的那部分 sudo 权限,不要 NOPASSWD ALL
#    /etc/sudoers.d/deploy
deploy ALL=(ALL) NOPASSWD: /bin/systemctl restart nginx, /bin/systemctl reload nginx

# 3. root 只允许密钥、不允许密码、不允许直接登录 shell 之外的任何东西
PermitRootLogin prohibit-password

# 4. 校验 sudoers 语法,这一步错了会导致 sudo 完全不可用
visudo -c

这里有个非常值得强调的细节:NOPASSWD: ALL 等于把普通用户的隔离意义完全抵消——攻击者拿到 deploy 就等于拿到 root。而白名单式授权(只允许特定 systemctl 子命令)虽然常被吐槽不够方便,却能在密钥泄漏时把损失限制在特定操作上。个人服务器上,方便和安全往往是同一个决策的两面,选哪个要明确,别默认选方便。

另外注意 authorized_keys 的权限检查很严格:如果 .ssh 目录是 777 或文件不是 600,sshd 会静默拒绝使用该密钥,日志里只有一句 Authentication refused: bad ownership or modes。这是新服务器上配好密钥却仍要密码的第三大原因(前两个是 SELinux 上下文与 home 目录权限)。

第四层:改端口到底有用吗

先给结论:改端口不是安全措施,是降噪措施。它减少日志里的自动扫描量,让你更容易在日志里看到针对你的真实攻击。它不改变认证强度,专业攻击者扫全端口不需要额外成本。

# /etc/ssh/sshd_config
Port 22022
# 若系统启用了 SELinux,改端口后必须注册,否则 sshd 起不来
semanage port -a -t ssh_port_t -p tcp 22022
# 防火墙放行新端口,且确认新端口可连之后再删旧的
firewall-cmd --permanent --add-port=22022/tcp && firewall-cmd --reload

这里的关键陷阱是顺序:先放行新端口 → 验证新端口能登录 → 才删旧端口 → 最后才关 22。任何一步提前,都会造成短暂的自我锁定。很多人改端口后「SSH 连不上」,原因往往不是 sshd 没起来,而是防火墙没放行、或者只改了 sshd_config 但 ssh.service 被 ssh.socket 接管了(systemd 的 socket 激活模式下,端口由 socket 单元决定,不读 sshd_config 的 Port)。

# 确认到底是谁在监听 22
ss -tlnp | grep -E ':22|:22022'
systemctl status ssh.socket 2>/dev/null    # 若存在且 active,端口可能由它决定

第五层:别忘了「你还能进去」这件事

所有加固措施都是在增加「攻击者进来的难度」和「你自己进不去的风险」之间做交换。所以合规的做法是事先准备好后路:

  • 云厂商的 VNC / 串口控制台:确认它可用,且控制台密码你知道。这是最后的救命通道。
  • 会话保持:执行任何高风险 SSH 改动前,先 tmux new -s work 把会话放进 tmux。即使网络断了、客户端崩了,重连后 tmux attach 就能回到原会话,包括那些未保存的编辑。
  • 配置备份:改文件前 cp sshd_config sshd_config.bak.$(date +%F)。这是最便宜的回滚。
  • 不要在改完就关掉唯一的会话:验证永远用新开的会话,而不是复用当前这个。当前会话能连不代表新会话能连(密钥、权限、sshd 生效状态都不同)。
# 一个实用的自检脚本,改配置后运行
#!/bin/bash
sshd -t || { echo "配置有语法错误,已终止"; exit 1; }
systemctl reload sshd
sleep 1
if ssh -o BatchMode=yes -o ConnectTimeout=5 -p "${PORT:-22022}" \
     deploy@127.0.0.1 true 2>/dev/null; then
  echo "OK: 新配置下密钥登录正常"
else
  echo "WARN: 新配置下登录失败,立刻回滚 sshd_config.bak"
fi

一个高频误区:关掉密码登录后,为什么日志里还在「验证失败」

很多站长按教程关了 PasswordAuthentication,第二天看 auth.log 发现还是一堆 Failed password for root,于是怀疑配置没生效。这里有两件事要分清:

  • 日志里的失败记录不代表密码认证还开着。攻击者仍然可以发起密码认证尝试,sshd 仍然会记录这次失败——只不过它无论如何都不会成功。你可以理解为「门把手还在被拧,但锁芯已经换成钥匙孔了」。
  • 想确认配置真的生效,要看实际协商结果,而不是看失败次数。用 sshd -T(大写 T,输出最终生效配置)比看 sshd_config 文件可靠得多,因为 include 进来的文件和 Match 块都可能覆盖你写的那一行。
# 查看真正生效的值,而不是文件里写了什么
sshd -T | grep -E "passwordauthentication|permitrootlogin|pubkeyauthentication"

# 给某个用户单独放行密码登录(常见于内网跳板机),用 Match 块
# 注意:Match 块必须放在文件末尾,后面的全局配置都会失效
Match User legacyuser
    PasswordAuthentication yes

Match 块的顺序陷阱值得单独记:Match 之后的所有指令都属于该条件块,直到下一个 Match 或文件结束。如果把 Match 写在中间又随手在后面追加了全局配置,那些配置只会对匹配到的用户生效,其他用户完全拿不到——现象就是「改了配置但只有某个人受影响」,非常费解。sshd -T 加上 -C user=xxx,host=yyy,addr=zzz 可以模拟特定连接来验证。

日志该看哪些,怎么看出来是「有人在针对性打你」

SSH 日志的价值不只是排查故障,它也是服务器的「入侵前哨」。有两种量级完全不同的攻击,处理方式不一样:

# 谁在试(按来源 IP 聚合)
grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -20

# 试了哪些用户名(判断是泛扫还是针对性字典)
grep "Failed password" /var/log/auth.log | grep -oE "for (invalid user )?[a-zA-Z0-9_-]+" | sort | uniq -c | sort -rn | head

# 有没有「尝试成功」的痕迹(这是真正需要立刻处理的信号)
grep -E "Accepted (password|publickey)" /var/log/auth.log | tail -20

怎么解读这三种输出:

  • 来源 IP 极其分散、用户名集中在 root/admin/test:全网泛扫,属于背景噪声,加固到位就无需处理。这种量级可能一天几千次,不必惊慌。
  • 来源 IP 集中在少数几个、用户名是你的真实用户名:针对性打击,说明你的用户名已经泄漏(可能来自某个被拖库的论坛或 Git 提交记录)。这种情况除了加固,还要考虑换用户名并检查其他服务的凭据是否有复用。
  • 出现 Accepted 且不是你自己的 IP/时间:这已经不是预防问题,是应急响应问题——立即下线、抓取证、轮换所有凭据、检查 ~/.ssh/authorized_keys 是否被追加了陌生公钥。
# 快速检查有没有被追加陌生公钥(后门最常见的形式之一)
cat /root/.ssh/authorized_keys /home/*/.ssh/authorized_keys 2>/dev/null
# 对照每行的注释字段与创建时间;同时检查 known 的 cron 与 systemd 单元
ls -la /etc/cron.d/ /etc/systemd/system/ | head -40

小结:SSH 加固的正确顺序

把上面的内容压成一份可执行清单:

  1. 先验证密钥登录(开新会话测),再关 PasswordAuthentication;
  2. 关 PermitRootLogin 的密码路径,建普通用户 + 白名单 sudo;
  3. 加 MaxAuthTries/MaxStartups 做基础限速,需要时再叠 fail2ban 并配好 ignoreip;
  4. 改端口属于可选降噪,注意防火墙/SELinux/socket 激活三个前置;
  5. 每一步都保证自己还有路进来:控制台密码、tmux、配置备份、新会话验证。

对个人站长而言,这套加固的性价比极高——大概半小时的工作量,换来的是把「服务器被爆破」从一个高频概率事件降成一个近乎不可能事件。而唯一需要小心的,是别在加固过程中把自己锁在外面。把「我还能进去吗」当成每一步的验收条件,这份清单就不会反噬。

Last modification:September 24th, 2026 at 07:28 pm

Leave a Comment