服务器被爆破 2.5 万次后,我搭了一套自动防御与攻击地图
先看一组数字。
我这台小小的云服务器,从监控上线到现在,累计记录了 25215 次失败登录;光是最近 24 小时,就有 477 次。榜单第一名的 IP(51.89.42.*)对着 SSH 端口,试了 4400 多次密码。
而这台服务器上:不跑电商、不存数据、没有任何"值钱"的东西。它只是有公网 IP。
有公网 IP,就等于站在广场中央
很多人对攻击的想象是"被盯上了":某个人查到你,然后对你发起进攻。真实的互联网不是这样运作的。
全网有无数扫描器在批量遍历 IP,像发传单一样把常用端口挨个试一遍。SSH 的 22 端口、数据库的 3306、面板的 8888……谁家开着门,谁就被记进一张"待处理清单"。整个过程没有人在针对你,你只是恰好被扫到了。
不是你被盯上了,而是整张网都在被翻。
这也是我做上一轮整改的原因:密码进保险箱、SSH 改成仅密钥、管理面板端口从公网关掉。那些解决的是"进不来"的问题。但还有另一半问题没解决:门外的噪音。
失败登录日志每天在涨、慢速爆破像水滴一样磨、分布式扫描从几十个 IP 轮流来。手动一条条封,根本封不过来。所以我决定给它配一整套自动防御。
第一层:fail2ban,快刀斩乱麻
fail2ban 干的事非常朴素:盯着登录日志,同一个 IP 短时间失败多次,就把它扔进防火墙黑名单。
我的配置是 5 次失败 / 10 分钟触发,首次封禁 1 天,再犯就翻倍,最长 4 周。这层负责处理最没技术含量的那批攻击——高频爆破。它们量最大,但最好处理:封得够快,它们连门把手都摸不到。
# 看看当下的战况
sudo fail2ban-client status sshd输出里的"Currently banned"就是当下被拒之门外的数量。有意思的是它经常只有个位数——不是没人攻击,而是攻击者一旦被秒封,就不再产生新的失败记录了。
第二层:CrowdSec,盯住"滴水"的人
fail2ban 的短板也很明显:它只会数次数。如果有人每 10 分钟试 1 次,永远触发不了阈值,但对一台服务器来说,这依然很烦。
CrowdSec 补的就是这块。它同样看日志,但引入了"场景"的概念:高频爆破、慢速爆破、时间型爆破、端口探测、拒绝连接探测,还有针对 OpenSSH 那个 CVE-2024-6387 的利用尝试,一共 6 个 SSH 场景在同时跑。
它把每条可疑记录扔进对应的"水桶"里,桶满了就出告警、下决策。fail2ban 漏过的慢速与分布式攻击,会在这里慢慢积累成形。
第三层:bouncer,把决策写进防火墙
CrowdSec 只负责"想",不负责"拦"。真正落刀的是 crowdsec-firewall-bouncer:它每 10 秒从本地接口拉一次封禁决策,写进一个叫 ipset 的集合,再把集合挂到 iptables 的 INPUT 和 DOCKER-USER 链上。
为什么用 ipset?因为决策可能是几百上千条,逐条写进 iptables 又慢又乱;ipset 是一个哈希集合,防火墙一次匹配,开销几乎可以忽略。挂在 DOCKER-USER 上还带来一个额外好处:连容器里的流量也一起管。
除了本机检测,注册 CrowdSec Console 之后还能同步社区黑名单——全球受攻击的服务器把恶意 IP 共享出来,你的防火墙白捡一份"通缉名单"。
再加上云厂商自带的云镜,四层叠起来大概是这样:fail2ban 处理最快的那批,CrowdSec 处理最阴的那批,bouncer 负责执行,云镜兜底。
光封还不够:我想看见它
安全做久了会有一个隐忧:所有东西都在后台悄悄运行,你怎么知道它到底有没有在工作?
所以我加了两块"仪表盘"。
一块是 Grafana 看板(Prometheus 抓 CrowdSec 的指标),首页是"防护总览":封禁数、攻击趋势、Top 场景、实时封禁明细。趋势图会在夜里规律性地跳起来——那是扫描器们上夜班的时间。
另一块是我最喜欢的:实时攻击地图。脚本每 5 分钟统计一次登录日志里最近 24 小时的失败 IP,取前 100 名做地理定位,打点到一张世界地图上。哪里的 IP 在敲门、敲了多少次、属于哪种攻击类型,全在上面。
配合地图还有 TOP5 攻击源、攻击类型细分、国家排行。底图用的是高德暗色,加载失败会自动降级到备用地图。统计脚本和前端都是独立的,挂在定时任务里自己跑,不依赖面板。
把攻击画成地图之后,安全从一条条日志,变成了"看得见的天气"。
日常怎么管
整套系统平时不需要我操心,但有三个动作最常用。
看战况:
sudo docker exec crowdsec cscli decisions list手动封一个烦人的 IP,立即生效:
sudo docker exec crowdsec cscli decisions add --ip 1.2.3.4 --duration 24h --reason manual解封误伤:
sudo docker exec crowdsec cscli decisions delete --ip 1.2.3.4这里有个反直觉的点:告警列表经常是空的。原因不难理解——fail2ban 封得太快,IP 被拉黑之后连失败日志都产生不了,CrowdSec 自然收不到告警。想看它一直在干活,去看"场景指标"那个水桶统计,数字一直在涨。
顺便,把"公示栏"也配齐了
在国内跑网站有个绕不开的环节:备案。ICP 备案和公安联网备案办完之后,按规定要把备案号和图标挂在页面底部。
博客本站用主题原生设置就够了;另外几个子域(监控面板、AI 网关、运维面板)则是用 Nginx 的 sub_filter 注入了一条底部的备案细条,还做了个小判断:页面上有密码输入框(也就是登录页)时显示,登录后自动隐藏。
中间有段时间站点配置被面板重建,页脚丢了,于是我在服务器上留了个幂等脚本,重跑一次就补回来——已经补过的会提示"already patched",不会重复注入。这种"配置会被面板覆盖"的场景,留一个兜底脚本比记一堆手动步骤靠谱得多。
如果你想照做:技术实现清单
整套东西没有自研组件,都是现成的开源工具拼起来。下面是我这边的实际组合,基本可以直接抄。
fail2ban(宿主)——封得最快的那一层:
# /etc/fail2ban/jail.d/sshd.local
[sshd]
enabled = true
maxretry = 5
findtime = 10m
bantime = 1d
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 4wCrowdSec(Docker)——检测层。核心是把宿主机的 auth.log 只读挂进容器,并装上 sshd 场景:
# docker-compose.yml(节选)
services:
crowdsec:
image: crowdsecurity/crowdsec:latest
container_name: crowdsec
environment:
COLLECTIONS: "crowdsecurity/linux crowdsecurity/sshd"
volumes:
- ./acquis.d:/etc/crowdsec/acquis.d
- /var/log/auth.log:/var/log/auth.log:ro
- crowdsec-db:/var/lib/crowdsec/data
ports:
- "127.0.0.1:8088:8088" # LAPI 只给本机用
# acquis.d/sshd.yaml:告诉它读哪个日志
filenames:
- /var/log/auth.log
labels:
type: syslogfirewall-bouncer(宿主)——执行层。装好之后用 cscli 生成一把 key,填进 bouncer 配置,它就会自动维护 ipset 和 iptables 规则:
sudo apt install crowdsec-firewall-bouncer-iptables
sudo docker exec crowdsec cscli bouncers add firewall-bouncer
# 然后把生成的 key 填进:
# /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
# api_url: http://127.0.0.1:8088/
# api_key: <上面生成的 key>可视化——Prometheus 抓 CrowdSec 的 6060 指标端口,Grafana 挂官方 CrowdSec 看板;面板本身用 Nginx 反代加 acme.sh 签发的证书上 HTTPS。攻击地图则是三个脚本加一条定时任务:
/opt/attack-map/
├── extract_attack_ips.py # 解析 auth.log,取最近 24h 失败 IP TOP100
├── build_map.py # ip-api.com 定位 + 生成地图 JSON
└── update-attack-map.sh # 串起来,写出 webroot/attack-map.json
# /etc/cron.d/attack-map:每 5 分钟跑一次
*/5 * * * * root /opt/attack-map/update-attack-map.sh地图前端就是一张静态页加一点 JavaScript:读 JSON、在高德暗色底图上打点;高德挂了自动降级到备用底图。页面上只显示目标服务器的域名,不暴露 IP。
备案栏在子域站点上是这么注入的——不改页面源码,用 Nginx 的 sub_filter 在 </body> 前插一条:
sub_filter '</body>' '<div class="site-beian">备案号 / 公安备案图标</div></body>';
sub_filter_once on;再配一小段判断:页面里有密码输入框(登录页)才显示,登录后隐藏。整个方案还留了一个幂等补丁脚本,站点配置被面板重建后重跑一次就能补回来,补过的会提示跳过。
这套链路的特点是"松耦合":日志、检测、封禁、可视化各自独立,任何一层坏掉都不会拖垮其他层,也方便以后单独替换。
写在最后
做完这一整套,我最大的感受是:安全防护不是"换一把更贵的锁",而是把门口发生的事变成一套自动运转的系统——有人试钥匙,就有人记录、上锁、画图。
代价呢?几乎没有。整套东西跑在一台 3GB 内存的小服务器上,日常占用可以忽略,倒是让我养成了每天打开攻击地图看一眼的习惯。
当然,这些都是"亡羊补牢"式的建设。如果你的服务器还开着密码登录、面板端口裸奔在公网,先把那些关上,再来加这套自动防御。
最后留个问题:如果你也有一台带公网 IP 的机器,你知道它每天被扫多少次吗?评论区聊聊。
本文由个人实践整理,文中服务器与攻击者 IP 均已做模糊处理。