这台 VPS 上线不到两天。我本来只是想弄明白一个开源脚本是干什么的,顺手跑一遍。日志翻开之后,数字不太像「偶尔有人扫一下」——更像有人把 22 端口当成了公共练兵场。

机器是 AlmaLinux 10.2,1 核、1.7G 内存、30G 盘。防火墙开着,只放行 22 / 80 / 443。听起来不算离谱。真正离谱的是 SSH 这一层:

  • PermitRootLogin yes
  • PasswordAuthentication yes
  • 还在默认 22 端口
  • 还没配过公钥
  • 没装 fail2ban,也没装 CrowdSec
  • 没有自动安全更新

翻译成人话:root 密码摊在公网上,谁猜中谁就是这台机的主人。过去大约 25 小时里,他们试了 14183 次密码,还用了 7229 个根本不存在的用户名。成功登录的 6 次全是我自己。没有被打进来。但这个「没有」只取决于那串密码还没被字典撞上。

下面按时间把这件事写清楚:原本怎么被打、GitHub 上下了什么、执行了什么、解决了什么,以及今天下午为什么还要再封一个 IP。

时间线

北京时间发生了什么
8 月 14 日晚机器开机。sshd 开始接待全世界
8 月 16 日 00:21我问:GitHub 上那个 VPS Security Audit Script 是干什么的
00:23从官方仓库把脚本拉下来,在这台机上跑
00:35–00:43按顺序加固:写 SSH 公钥、装 fail2ban、换 root 密码、更新系统、收紧管理面
00:56再跑一遍脚本,核对还在不在被打
01:00 左右搞清楚私钥该在哪台电脑上生成
今天下午 17:02复查昨晚到现在的防护效果
17:11把一条躲规则的慢扫 IP 永久丢掉

机器自己的时区是 IST,比北京慢两个半小时。文里小时级图表用的是机器本地时间,方便和 /var/log/secure 对上。

1. 从 GitHub 拉下来的东西

仓库是 nuver-labs/vps-audit,大约 2.4k star。它不是杀毒,也不是渗透工具。作者的定位很老实:一条 不装依赖、不留常驻进程 的 Bash 脚本,给 Linux VPS 做一轮只读体检。查的都是真会把机器打下来的基础项:

  • SSH 是否允许 root、是否还开着密码
  • 防火墙有没有在跑
  • 有没有自动安全更新
  • Fail2ban / CrowdSec 在不在
  • 失败登录多不多
  • 端口是不是铺得太开
  • 磁盘、内存、可疑 SUID

打分三档:PASS / WARN / FAIL。跑完在终端出一份彩色报告,再写一个 vps-audit-report-<时间戳>.txt

它面向 Debian / Ubuntu。这台是 AlmaLinux,所以后文会反复出现一个情节:脚本说没事,日志里已经炸了。

下载和执行就这三行,官方 raw 地址,没有第三方套壳:

curl -fsSL -o /tmp/vps-audit.sh \
  https://raw.githubusercontent.com/nuver-labs/vps-audit/main/vps-audit.sh
chmod +x /tmp/vps-audit.sh
bash /tmp/vps-audit.sh

脚本版本是 v0.2.0。第一次跑完的报告在 /tmp/vps-audit-report-20260815_215321.txt,机器本地时间 8 月 15 日 21:53。

2. 脚本说「0 次失败登录」,日志里是一万四千次

脚本自己的成绩单很难看,但还不够准:

检查项脚本对着 AlmaLinux 复核
SSH 允许 rootFAIL属实。安装时留下的 01-permitrootlogin.conf
SSH 密码登录FAIL属实。当时只能靠密码
SSH 22 端口WARN属实,次要
防火墙PASS属实。firewalld 在跑,只放行 22/80/443
未装入侵防护FAIL属实。当时机器上没有 fail2ban
失败登录PASS(0 次)严重漏报。它去读 Debian 的 /var/log/auth.log,这台机日志在 /var/log/secure
系统更新PASS漏报。它调用 apt-get,AlmaLinux 没有这个命令。实际积了 17 个包,含新内核
自动更新FAIL结论对,检测方法是 Debian 的
开放端口过多FAIL(8 个公网端口)夸大了。真正对公网的是 22 / 80 / 443;其余有的只绑 localhost,有的是 Tailscale
磁盘 / SUID / 需重启PASS属实
内存偏紧、无 swapWARN属实。1.7G 用了六成

脚本最危险的那条误报,是「失败登录 0 次」。我当时改去读 /var/log/secure,从开机到第一次体检大约 25 小时:

指标数量
Failed password14183
Invalid user7229
成功的密码登录6,全是我自己的家里宽带
成功的公钥登录0,因为根本还没配钥匙

平均下来,这台 1 核小机器每分钟要接待大约 9 次猜密码。sshd 自己有一点 penalty,挡不住这种量。

打得最凶的四个地址:

开机 25 小时里打得最凶的四个地址

110.173.190.221 一个人砸了五千多次。它不是认识我,也不是冲着博客来的。公网 22 端口对扫描器来说就是一份待处理队列,扫到就丢进去,按字典慢慢试。目的一般是:撞进之后装挖矿、做成肉鸡再去打别人,或者把壳子卖掉。

成功登录当时只有我自己的几个出口 IP。没有多余 root 账号,没有奇怪的 crontab,没有可疑 SUID。还没失陷。 风险是一直暴破下去,密码一旦被撞上就是 root。

80 端口还有另一处不舒服:nginx 把管理面板反代在 HTTP 上。面板自己的端口没进防火墙,外网直接打进不来,但走 80 就能摸到。公网 HTTP 没有 TLS。443 上跑的是我自己的代理,不是脚本说的「神秘高危端口」。

3. 按顺序做,先保证不会把自己锁外面

脚本跑完,处理顺序其实比「立刻关掉密码登录」更重要。当时这台机 没有一把能用的公钥。先关密码,下一秒我就进不来了。

所以当晚只做不锁人的六步,SELinux 和关密码留到钥匙确认能登上去以后。

第一步:写下 SSH 公钥,密码先留着

机器上生成了一把 ed25519,写入了 authorized_keysPasswordAuthentication 没动。指纹、公钥内容这类东西不会写在这里。

这件事后来专门又聊了一轮,因为「在哪台机器上生成私钥」很容易搞反:

位置该放什么不该放什么
我的电脑私钥 id_ed25519不要把私钥拷到服务器
这台 VPS公钥,追加进 authorized_keys不要在服务器上为「我连回来」这件事 ssh-keygen

服务器凭什么认我的公钥?它不会在网上看到一把钥匙就自动装上。它只认已经写进 ~/.ssh/authorized_keys 的那几行。谁能改这个文件,谁才能加新钥匙。外人自己生成一对密钥,没有这台机的写入权,加不进来;只有公钥没有私钥,也连不上。

更稳的做法是在 电脑上 ssh-keygen,再用 ssh-copy-id.pub 推过来。服务器上那把只是过渡,等电脑上的钥匙配对成功就可以不用它。

第二步:装 fail2ban,对准真正的日志

dnf install -y fail2ban fail2ban-firewalld fail2ban-systemd dnf-automatic

jail.local 按这台机改过,不是照抄 Debian 示例:

  • /var/log/secure,不用不存在的 /var/log/auth.log
  • 15 分钟内失败 4 次 → 封 24 小时
  • 重复再犯,封禁时间加长,最长一周
  • 白名单:本机、组网网段、家里常用出口
  • 动作走 firewallcmd-rich-rules,和现有 firewalld 叠在一起
  • 顺手装上的 exim 立刻关掉并 mask,避免多开一个邮件端口

fail2ban 在机器本地时间 22:08 起来。启动当秒就封掉了当时还在砸的那几个,包括 110.173.190.221167.233.166.95

第三步:换掉正在被打的 root 密码

按这个暴破规模,旧密码必须当成已经出现在别人的字典里。当晚用 chpasswd 换成一串 24 位随机口令,磁盘上的明文副本随即删掉。旧密码立刻失效。

密码登录此时 还开着。新密码只是把「正在被砸的那把锁」换掉,不是最终方案。最终方案是钥匙能登上去之后,关掉密码。

第四步:补上 17 个更新,打开自动补丁

脚本说系统已是最新,那是因为它在喊 apt-get。实际 dnf check-update 有 17 个待更新包,包括内核 6.12.0-211.47.1libnghttp2。当晚 dnf update 跑完了,但 没重启,所以当时还在跑旧内核 6.12.0-211.46.1

同时启用 dnf-automatic-install.timer,大约每天凌晨自动装更新,不会自动重启。1.7G 内存扛更新容易 OOM,加了 1G swap/swapfile)。现在 swap 还在,已经真正用上了几百兆。

第五步:SELinux 这次没开

当前是 Disabled。AlmaLinux 默认应该是 Enforcing。从关到开必须改配置、整盘 relabel、再重启。代理、nginx 反代、管理面板都可能被策略拦住。SSH 钥匙还没从我自己的电脑验证过,这个窗口不适合拿来赌开机。

第六步:管理面板不再对公网裸奔

nginx 里这个 server 改成只允许组网网段和本机,其他来源访问对应 Host 直接 403,当时测过。手机走内网主机名仍然 200。别的站点没动。

面板进程当时还听在所有地址上,但防火墙没放行那个端口,外网打不进。配置已改成下次重启只绑本机回环。当晚没重启面板,避免把正在用的页面踢掉。

刻意没做的,也列清楚:

没做的事原因
关闭 SSH 密码 / 禁止 root 远程等我确认自己电脑上的钥匙能登上
改 SSH 端口次要,而且要同步改 fail2ban
打开 SELinux要 relabel + 重启,容易弄挂现有服务
立刻重启吃新内核会中断代理、面板和当时的会话

4. 装完 20 分钟后再跑一遍:还在打,但量掉了一个数量级

本地时间 22:27,同一条脚本又跑了一次。它仍然报「没装 fail2ban」「失败登录 0 次」——还是那个 Debian 视角。对照 journal 和 fail2ban-client status sshd 才是真的。

按小时看 Failed password,转折就在 22 点:

SSH 失败登录按小时,22:08 是分水岭

本地时间失败登录状态
17 时1083裸奔
18 时1101裸奔
19 时1043裸奔
20 时909裸奔
21 时1196裸奔
22 时13822:08 fail2ban 起来,当小时后半段被掐住
刚启动后 20 分钟26新 IP 试满 4 次就被封
最近 5 分钟(当时)0门外暂时清净

当时已经封了 10 个 IP。22 端口没有 ESTABLISHED / SYN-RECV。结论写成一句话:互联网扫描还在继续,但已经被挡在门外;没有看到已经登录成功的陌生地址。

脚本第二次还在报 FAIL 的几项,对照如下:

检查脚本第二次实际
自动更新FAIL误报。dnf-automatic-install.timer 已经 enabled
Fail2banFAIL误报。它用 dpkg 查,AlmaLinux 上查不到
失败登录PASS(0 次)仍然漏报。真实累计已经过万
SSH root + 密码FAIL属实,当晚故意没关

5. 今天下午再查:狂砸被按住了,有人改成了慢扫

防护加上之后大约 16.5 小时。把这段和装之前的 8.5 小时放在一起,差别不用解释:

防护前后失败登录总量

更细的小时曲线:

  • 15 日 18–21 点(含 Invalid user):每小时 2500–3400
  • 22 点 fail2ban 启动后立刻掉到 407,里面很大一部分是启动时一次性清算旧日志
  • 15 日 23 点之后:每小时 6–37
  • 今天下午 14 点这一小时:12

启动当秒封掉的那几个没有再打进来。之后不断有新 IP 过来试,试满 4 次就被封。今天下午复查时,名单已经堆到 28 个;再过一会儿封慢扫的时候,是 30 个。

没有失陷的依据也还站得住:

  • Accepted 记录里,密码登录仍是我自己的家里宽带
  • 公钥登录只有本机回环,以及 CI 用来部署博客的那把钥匙
  • 28 个封禁 IP 全部是 SSH 失败后封的,没有「解封后再打进来」
  • 订阅接口昨晚到现在只有我自己拉过

两处现有规则没挡住。

第一处是慢速爆破。 130.49.213.228 从今天 00:40 起到下午 14:34 还在打,大约 每 16–17 分钟试 1 次,每次换一个用户名。规则是「15 分钟内 4 次才封」,它卡在窗口外面,所以一直没被自动封。这不是手滑,是故意躲 fail2ban。

它只碰 22 端口,网站、订阅、Xray 都没碰。试过大约 32 个账号,几乎全是泄露密码表里的业务默认用户,不是我的名字:

类型它试过的账号
数据库 / 中间件oracle postgres grid applmgr emqttd dirsrv
运维 / 部署ubuntu deploy git admin root dev developer patch
厂商默认号dspace duzon filmlight dell hp
常见人名号mahesh amit
随便猜的test user user2 support mobile node

root 它只试过 1 次。更想撞的是别人忘了改密码的 Oracle、Git、Ubuntu 默认用户。全部失败。

这个地址归 AS207713 GLOBAL INTERNET SOLUTIONS LLC(GIR),机房标注在新西伯利亚,反解是批量 PTR 169368.ip-ptr.tech。能 ping 通,延迟大约 241ms。这家主机商在安全圈里口碑很差,同网段有过钓鱼和批量域名的记录。更像租来的扫描炮灰,不是某个认识我的人。

今天下午 17:11 手动封掉,做了两层:

  1. 丢进 fail2ban 的 sshd 黑名单(当时共 30 个 IP)
  2. firewalld 永久 rich rule:这个源地址全部 drop,重启也不丢

fail2ban 那条 24 小时后可能会解,防火墙那条会一直留着。它再按 16 分钟一次来试,包到不了 sshd。

第二处是 Web 扫描。 fail2ban 现在只有 sshd jail,80 端口没人拦。今天至少有两个地址在扫 /.env/.git/config/phpinfo.php、phpunit 老洞。日志里是 200,核对过了:那是默认站前端的 try_files ... /index.html,一律回同一份页面,不是真的环境变量或源码。订阅路径没有被外人拉走。Web 这边以后可以单独加一个 nginx 监狱,今晚没做。

6. 一张表看清「做了什么」和「得到了什么」

动作做了什么成果
跑 vps-audit v0.2.0官方脚本 + 人工对照 /var/log/secure发现 1.4 万次撞库,以及脚本自己的 Debian 盲区
写入 SSH 公钥ed25519 + authorized_keys有了关密码登录的前提,还没关
安装 fail2ban15 分钟 4 次 / 封 24 小时,读 secure小时失败量从 ~1100 掉到十几
更换 root 密码chpasswd,旧密码作废正在被砸的那把锁换掉了
系统更新 + 自动补丁dnf update + dnf-automatic17 个包补上;以后凌晨自动装,不自动重启
加 1G swap/swapfile1.7G 机器更新和日常不容易直接 OOM
收紧管理面板nginx 只放行组网网段公网 Host 访问变 403,管理面不再裸奔 80
关掉 eximmask 掉 fail2ban 顺带拉进来的邮件服务少一个监听端口
永久封 130.49.213.228fail2ban + firewalld drop躲 15 分钟窗口的慢扫不再能碰到 SSH

再压成前后对比:

昨晚加固前现在
25 小时 SSH 失败密码14183新流量按小时计是原来的 1%–3%
入侵防护fail2ban,当前封 30 个 IP
root 密码开在公网、正在被字典打已轮换
SSH 密钥没有已写入,密码登录仍开着
自动安全更新每天自动安装
管理面公网 HTTP 能摸到仅组网 / 本机
慢扫不存在「规则」可躲已对这条 IP 永久丢包
是否失陷未见陌生成功登录仍然未见

7. 还没做完的

这篇文章不是「已经安全了」。是「从裸奔变成能挡普通扫描」。还挂着的:

  1. 密码登录和 root 远程都还开着。 钥匙在服务器上有了,我自己电脑上的那把还需要完成配对。配对成功的下一件事就是:

    PasswordAuthentication no
    PermitRootLogin prohibit-password
    PubkeyAuthentication yes
    
  2. fail2ban 窗口仍然是 15 分钟 4 次。 今晚这条慢扫是手工封的。以后可以把 findtime 拉长,让「16 分钟一次」也会累计。

  3. 80 端口的路径扫描没人管。 现在靠前端兜底,不等于永远没事。

  4. SELinux 仍是 Disabled。 要单独挑一个维护窗口。

  5. SSH 还在 22。 改端口只是少吃一点垃圾流量,不能代替关密码。

给刚开好公网 VPS 的人,我现在会按这个顺序说:

  1. 先在 自己电脑ssh-keygen,公钥写进服务器,登上去确认
  2. 再关密码,再考虑关 root 远程
  3. 装 fail2ban,确认它读的是这台发行版真正的日志
  4. 换掉刚刚在公网上裸奔过的密码
  5. 打开自动安全更新
  6. 管理面板不要放在公网 80

vps-audit 值得跑。但在 AlmaLinux / Rocky / CentOS 上,不要相信它的「失败登录 0 次」和「系统已是最新」。那两行要自己再查一遍 /var/log/securednf check-update

第一晚的故事到这里。机器还在,博客也还在。门外那几万次敲门,大部分已经听不见了。