SSH 被敲了 10 万次之后:关掉密码登录,再用 ipset 挡掉高频 IP(2026 实操)

这台 2 核 1.9G 的小机器在公网上躺了快两年,我一直觉得改过端口就算安全了。今天翻了一遍 /var/log/secure,从 9 月 20 号到现在不到 25 天,Failed password 出现 101,093 次,平均每天 4,086 次。而 sshd -T 里那行 passwordauthentication,还是 yes。

意思很直白:这些尝试不需要成功很多次,只要有一次撞对了密码,服务器就归别人了。

所以先量,再动手。两条命令就能看清自己家的门被敲成什么样。

先量:25 天 10 万次,是谁在敲

zgrep -h "Failed password" /var/log/secure* | wc -l
zgrep -h "Failed password" /var/log/secure* | grep -oE "from [0-9.]+" | awk '{print $2}' | sort | uniq -c | sort -rn | head -10

本站的输出:

101093
  18450 172.239.20.36
  14225 5.231.242.178
  12242 5.231.242.53
  10788 163.5.210.171
   6368 104.105.17.43
   6325 172.238.114.181
   5305 45.148.10.46
   4626 5.231.242.198
   4593 5.231.242.52
   2226 77.239.124.120

前十名加起来 85,148 次,占 84%,剩下的一万五千多次散在几百个地址上。再看它们敲的是谁的账号:

zgrep -h "Failed password" /var/log/secure* | awk '{print $9}' | sort | uniq -c | sort -rn | head -5
  54303 invalid
  45993 root
    141 mysql
    108 www
     94 ftp

五万四千次在赌 admin、user、ubuntu、test、oracle 这类可能不存在的用户名,四万六千次直接怼 root。没人需要知道我的用户名,脚本自己会一个一个试。

一句实话:封 IP 不是底线,关认证才是

动手之前我先想清楚了封 IP 能换来什么。

头号选手 172.239.20.36 从 10 月 11 号早上 6 点开始敲,到 12 号晚上 8 点半收工,38 个小时里塞进 9,321 次,之后再没露面。今天还在活跃的是 45.148.10.46,单日 449 次;另外一批地址每个只试 20 次就走,比如 147.90.182.25、14.103.213.90 这几个。

封 IP 封掉的是这一波人,明天换一批 IP 照样来。让十万次尝试变得毫无意义的动作只有一个:把密码认证关掉,脚本再敲一百万次,也永远拿不到那道门。

两件事都值得做,顺序别反。关认证是底线,封 IP 的收益是日志干净、少几万次无谓的 fork,附赠一点心理安慰。

动手前:确认密钥能登,再挂一颗后悔药

这一步比后面所有步骤都重要。要求两个前提:/root/.ssh/authorized_keys 里至少有一把你能用的私钥,而且云控制台还能兜底(阿里云 ECS 的 VNC 远程连接不需要 SSH 通)。

wc -l /root/.ssh/authorized_keys
cut -d' ' -f1,3 /root/.ssh/authorized_keys

本站三把,其中一把是日常在用的:

3 /root/.ssh/authorized_keys
ssh-rsa 1503255237@qq.com
ssh-ed25519 root@LittlesheepUAU
ssh-ed25519 hermes-automation

然后挂一条 15 分钟后自动还原的保险。改坏了它会自己把配置还回去,你还有机会重新连上:

STAMP=$(date +%Y%m%d-%H%M)
cp -a /etc/ssh/sshd_config /root/sshd_config.bak-$STAMP

setsid nohup bash -c "sleep 900; cp -a /root/sshd_config.bak-$STAMP /etc/ssh/sshd_config; systemctl reload sshd" </dev/null >/dev/null 2>&1 &
echo $! > /root/ssh-rollback.pid

新配置验证通过之后,kill $(cat /root/ssh-rollback.pid) 把它撤掉。十秒钟的事,能救回一整个下午,别省。

改哪一行:sshd_config 是先出现的赢

OpenSSH 的配置不是后面覆盖前面,而是第一条生效。所以往文件末尾追加一行 PasswordAuthentication no 完全没用,上面那行 yes 照样赢。必须把原来的行改掉,或者把旧行注释掉。

用编辑器手工改没问题,想照抄就用这段 python:

import re, pathlib

p = pathlib.Path('/etc/ssh/sshd_config')
s = p.read_text()

def setkv(s, key, val):
    pat = re.compile(r'^[ \t]*' + key + r'[ \t]+.*$', re.M)   # 只匹配未注释行
    if pat.search(s):
        return pat.sub('%s %s' % (key, val), s, count=1), 'replaced'
    return s.rstrip('\n') + '\n%s %s\n' % (key, val), 'appended'

for k, v in [('PasswordAuthentication', 'no'),
             ('PermitRootLogin', 'prohibit-password'),
             ('LoginGraceTime', '30'),
             ('MaxAuthTries', '3'),
             ('AllowUsers', 'root')]:
    s, how = setkv(s, k, v)
    print('%-24s -> %-18s (%s)' % (k, v, how))

p.write_text(s)

改完文件里这几行长这样:

PermitRootLogin prohibit-password
PasswordAuthentication no
AuthorizedKeysCommand /usr/bin/timeout 5s /usr/local/share/aliyun-assist/plugin/config_ecs_instance_connect/ecs_config_instance_connect %u %f
LoginGraceTime 30
MaxAuthTries 3
AllowUsers root

PermitRootLogin prohibit-password 保留 root 用密钥登录,只是不许密码。MaxAuthTries 3 把一次连接能试的密码数从默认 6 次压到 3 次,LoginGraceTime 30 把等待时间从 60 秒压到 30 秒,公网机器上这两项比装什么工具都实在。AllowUsers root 会让五万多次 invalid user 在进认证之前就被拒,代价是以后新建用户要先把名字加进这一行,忘了就是你自己连不上。

阿里云那行 AuthorizedKeysCommand 一个字符都别动,控制台的远程连接和会话管理靠它注入临时公钥,删了就等于把自己的逃生通道拆了。

验证:三条命令

sshd -t                    # 先验语法,不通过就别 reload
systemctl reload sshd      # reload 不踢已连上的会话,你现在这条 ssh 不会断
sshd -T | grep -Ei "^(passwordauthentication|permitrootlogin|allowusers|maxauthtries)$"

本站 reload 之后:

logingracetime 30
maxauthtries 3
permitrootlogin without-password
passwordauthentication no
allowusers root

prohibit-password 在 sshd -T 里会显示成 without-password,两者是同一个意思,别以为没生效。

最后换一台机器,用密码方式硬试一次:

ssh -p 19122 -o PreferredAuthentications=password -o PubkeyAuthentication=no root@你的服务器IP true

本站现在的返回:

root@47.115.212.51: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).

服务端连 password 这个方法都不再提了,这才叫关干净。如果它还在问你密码,说明你改的那行不是文件里第一条,回去检查有没有重复项。

用 ipset 把高频 IP 挡在 sshd 之前

iptables 一条规则写一个地址也行,但十万次攻击背后是几百个 IP,逐条写会把自己写烦。ipset 一个集合搞定匹配,宝塔面板自己也在用这套(这台机器上有 bt_ip_filter 和 monitor_filter 两个集合)。

ipset create ssh_ban hash:ip hashsize 4096 maxelem 262144 timeout 604800 -exist

zgrep -h "Failed password" /var/log/secure* | grep -oE "from [0-9.]+" \
  | awk '{print $2}' | sort | uniq -c | sort -rn | awk '$1>=2000{print $2}' > /root/ssh_ban_ips.txt

while read -r ip; do ipset add ssh_ban "$ip" -exist; done < /root/ssh_ban_ips.txt

iptables -I INPUT 1 -p tcp -m set --match-set ssh_ban src -m tcp --dport 19122 -j DROP

阈值我取的是 2000 次,本站命中 10 个地址。地址进集合之后连 TCP 握手都完不成,sshd 里连一条日志都不会留下。

验证别拿自己的公网 IP 试,那是自杀。把回环地址临时塞进去就行:

ipset add ssh_ban 127.0.0.1 -exist
timeout 12 ssh -p 19122 -o ConnectTimeout=5 127.0.0.1 true; echo "退出码 $?"
ipset del ssh_ban 127.0.0.1
timeout 15 ssh -p 19122 -o ConnectTimeout=8 -o BatchMode=yes 127.0.0.1 true; echo "退出码 $?"

本站被封时卡在 connect to host 127.0.0.1 port 19122: Connection timed out,退出码 255,规则计数器同时在涨,说明确实有包撞在规则上;把地址移出集合后同一条命令立刻能连上,退出码 0。

让封禁活过重启:别指望 rc.local

ipset 和 iptables 都不写盘,重启就没了。网上教程都让你改 /etc/rc.d/rc.local,先跑这两条再说:

systemctl is-enabled rc-local
systemctl is-active rc-local

本站返回 static 和 inactive,/etc/systemd/system/multi-user.target.wants/ 里也没有它,意思是这个文件根本不会被执行。crontab 反而是 active,所以恢复脚本挂 cron 更稳:

# /root/ssh-ban-restore.sh 里就是上面那三条命令,幂等,跑几遍都没事
( crontab -l 2>/dev/null | grep -v ssh-ban-restore; echo "@reboot /root/ssh-ban-restore.sh" ) | crontab -

脚本自己会先 ipset create ... -exist 再灌地址、再补规则,重启后跑一遍就恢复原样。删掉名单里某个地址用 ipset del ssh_ban <IP>,整条规则下线用 iptables -D INPUT -p tcp -m set --match-set ssh_ban src -m tcp --dport 19122 -j DROP。

回滚与两个附带发现

配置回滚就一句:cp -a /root/sshd_config.bak-* /etc/ssh/sshd_config && systemctl reload sshd,加上那颗十五分钟的自动保险,理论上你不会失联。

顺带说一个隐形大文件:这台机器的 /var/log/btmp 已经 38M、102,867 条失败登录记录,它是 binary 格式,lastb 才能读。日志轮转没管它的话会一直长,磁盘告急的时候值得翻一眼,清理思路在服务器磁盘被谁吃掉了那篇里写过。另外那种「每个 IP 只试 20 次就换地址」的慢速访问,恰好是各类自动封禁工具的盲区,别指望一条阈值规则能全兜住。

SSH 这一层收完之后,还有半边在 WordPress 里,登录页同样天天被人踹,动作都写在WordPress 安全加固实操那篇,抓异常请求的姿势可以看Nginx 日志排查。两年前我改了端口就觉得安全了,今天才知道门一直开着。你要是也从没看过自己的 /var/log/secure,花两分钟跑第一条命令,数字大概会让你今晚想动手。

© 版权声明
THE END
喜欢就支持一下吧
点赞15 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容