← 返回文章流

博客被人刷了一晚上,我用 fail2ban 把这事收了个尾

那天早上起来,博客打不开了。 浏览器转圈,转了很久之后给了一个 502。我一边刷牙一边用手机连上去看,uptime 显示 load average 是 34。 我那台机器是 1 核 2G,正常负载在 0.1 左右。34 这个数字意味着有东西在疯狂占 CPU。 先看是什么在打 1 ss -tn state established '( sport = :443 )' | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn | head 输出里有一批 IP,每个都有几百个连接。再看 nginx 的访问日志: 1 awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20 排名第一的 IP 在十分钟里请求了三万多次。 1 grep '1.2.3.4' /var/log/nginx/access.log | tail -5 请求的路径全是 /search?q=<随机字符串>,User-Agent 是一个老掉牙的浏览器标识。典型的 CC 攻击,拿一个搜索页反复刷,因为这个页面每次都查数据库,最耗资源。 临时的止血 先把那个 IP 拉黑,让机器能喘口气: 1 iptables -I INPUT -s 1.2.3.4 -j DROP 立刻见效,负载从 34 掉到 2。但十分钟后,另一个 IP 开始用同样的方式打。 这就说明手动拉黑没用,攻击方手里有一批 IP,封一个换一个。 那就上 fail2ban fail2ban 的思路很简单。它盯着日志文件,按你写的规则匹配失败记录,同一个 IP 在时间窗口里超过阈值就调用防火墙把它封一段时间。 装: 1 2 apt update apt install fail2ban -y Debian 12 装完默认就是启用的,但里面除了 sshd 那个 jail,别的都没开。 配置不要改 /etc/fail2ban/jail.conf,那个文件在升级的时候会被覆盖。新建一个 jail.local: 1 cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local 先把 SSH 那边接上 虽然这次的攻击是打 HTTP,但 SSH 那边的爆破一直没停过。顺手一起处理。 jail.local 里找 [sshd] 段,或者干脆在文件末尾新加: 1 2 3 4 5 6 7 8 9 10 [DEFAULT] bantime = 1h findtime = 10m maxretry = 5 backend = systemd [sshd] enabled = true port = ssh logpath = %(sshd_log)s backend = systemd 这个设置值得说一下。Debian 12 默认没有 /var/log/auth.log 了,认证日志都进了 journald。用 systemd 这个 backend,fail2ban 会通过 journalctl 去读,不用依赖文件存在。 我第一次配的时候没改这个,fail2ban 起不来,日志里写着 Have not found any log file for sshd jail。找了半天才明白。 再配 nginx 的 jail 这个要自己写,因为每个站的日志格式和攻击特征都不一样。 先看日志格式。我的 nginx 用的是默认的 combined,长这样: 1 1.2.3.4 - - [30/Aug/2024:03:12:44 +0800] "GET /search?q=xxx HTTP/1.1" 200 5123 "-" "Mozilla/5.0 ..." fail2ban 的 failregex 用正则去匹配这一行,<HOST> 是个占位符,会自动替换成 IP 的匹配模式。 针对这次攻击,我写了个 filter,放在 /etc/fail2ban/filter.d/nginx-cc.conf: 1 2 3 4 5 [Definition] failregex = ^<HOST> -.*"(GET|POST|HEAD) /search\?.* HTTP/1\.[01]" 200 ignoreregex = 意思是,请求 /search 且返回 200 的,算一次。 然后在 jail.local 里加: 1 2 3 4 5 6 7 8 [nginx-cc] enabled = true port = http,https filter = nginx-cc logpath = /var/log/nginx/access.log maxretry = 30 findtime = 1m bantime = 2h maxretry = 30 和 findtime = 1m 意味着,一分钟内请求同一个路径超过三十次就封。正常用户不可能这么干,但搜索引擎的爬虫有可能。所以我还得加个白名单。 别把自己的爬虫封了 Google 和 Bing 的爬虫会被误伤。虽然它们一般不会一分钟请求三十次,但保险起见,还是加个 ignoreip。 先确认爬虫的真实 IP。注意不能信 User-Agent,得用反向解析: 1 dig +short -x 66.249.66.1 返回 crawl-66-249-66-1.googlebot.com. 就说明是真的。伪造的 UA 反解出来不会是这个域名。 把这个网段加进白名单: 1 2 [DEFAULT] ignoreip = 127.0.0.1/8 ::1 66.249.64.0/19 40.77.167.0/24 前者是 Googlebot,后者是 Bingbot。这两个网段官方都有公布,定期看一眼有没有变。 让 Cloudflare 的真实 IP 传进来 这一步很重要,我差点漏了。 我的博客前面挂了 Cloudflare,所以 nginx 的访问日志里,$remote_addr 全是 Cloudflare 的节点 IP。如果不处理,fail2ban 会去封 Cloudflare 的节点,一封就是一大片用户打不开。 要在 nginx 里先把真实 IP 还原: 1 2 3 4 5 6 7 # /etc/nginx/conf.d/cloudflare-realip.conf set_real_ip_from 173.245.48.0/20; set_real_ip_from 103.21.244.0/22; set_real_ip_from 104.16.0.0/13; # ... 其余网段省略 real_ip_header CF-Connecting-IP; real_ip_recursive on; Cloudflare 官方有个 IP 列表页面,也可以直接拉: 1 curl -s https://www.cloudflare.com/ips-v4 把这些网段全部写进 set_real_ip_from。配好之后 nginx -t && nginx -s reload,再看日志,$remote_addr 就是真实访客的 IP 了。 这一步做完,fail2ban 封的才是攻击者,不是 Cloudflare。 防火墙后端用哪个 fail2ban 支持好几种后端,iptables、nftables、firewalld。Debian 12 默认是 nftables,但 fail2ban 的默认配置可能还在用 iptables。 看当前用的是哪个: 1 fail2ban-client -d | grep -i banaction 如果是 iptables-multiport,而系统里其实是 nftables,封禁规则会写进 iptables 的兼容层,虽然也能生效,但 nft list ruleset 里看到的格式很乱,排查的时候不方便。 改一下: 1 2 3 [DEFAULT] banaction = nftables-multiport banaction_allports = nftables-allports 然后重启 fail2ban。 验证和观察 1 2 systemctl restart fail2ban systemctl status fail2ban 看 jail 有没有都起来: 1 fail2ban-client status 应该能看到 sshd 和 nginx-cc 两个。 看某个 jail 的详情: 1 fail2ban-client status nginx-cc 输出里有当前封了多少个、总共封过多少个、以及封禁列表。 我配完第二天看了一下,nginx-cc 封了 200 多个 IP,sshd 封了 60 多个。负载回到了 0.2。 几个后续调整 bantime 我一开始设的 1 小时,后来改成递增。fail2ban 支持这个: 1 2 3 bantime.increment = true bantime.factor = 2 bantime.maxtime = 4w 意思是第一次封 1 小时,再犯封 2 小时,再犯 4 小时,一直翻倍到四周。这个机制对那种反复来的 IP 很有效。 bantime.increment 需要 fail2ban 的数据库支持,默认是开着的,位置在 /var/lib/fail2ban/fail2ban.sqlite3。别把它删了,删了计数就归零。 还有个 findtime 的坑。它和 bantime 是独立的,findtime 只决定统计窗口,不影响封多久。我一开始以为 findtime 设长了封得久,结果不是。 也别忘了限流 fail2ban 是事后处理,等它反应过来,攻击流量已经打进来几十秒了。 所以我又在 nginx 里加了一层限流: 1 2 3 4 5 6 limit_req_zone $binary_remote_addr zone=search:10m rate=10r/m; location /search { limit_req zone=search burst=5 nodelay; limit_req_status 429; } 这个是事前的,请求超过频率直接返回 429,连 PHP 都不进。两层配合起来,效果比单独用任何一个都好。 写在最后 这次攻击持续了大概两天,之后就没再来了。可能是对方觉得我这个站不值得,也可能是成本太高。 回过头看,最该反思的是那个搜索页。它是动态查询,没有缓存,一个请求就要连一次数据库。这种页面天生就是被刷的目标。 后来我给搜索结果加了一层 Redis 缓存,同样的查询五分钟内直接返回缓存。这个改动比 fail2ban 更治本,因为它把攻击的成本降下来了。 安全这件事,工具只是兜底。真正要做的,是让被攻击的那个点变得不那么值钱。

来自 昭鸢笺作者主页
在原博客阅读全文 (新标签页打开)

以上内容来自博客订阅源,版权归原作者。关注作者后可以在 关注列表 第一时间查看作者文章。

一起聊聊 0

尊重不同观点

还没有评论。欢迎分享第一个想法。

RSS SUBSCRIPTIONS

选择 RSS 订阅

把博客汇的文章添加到你常用的阅读器。

关注文章 RSS 订阅本订阅需要登录后使用