← 返回博客

个人博客的安全防护实践:从被全网扫描到自动封禁

你以为建个博客就完了,但互联网上的扫描器可不这么想。


一、起因:一个静态博客,被扫出了 DDoS 的架势

上周我刚把这个个人博客上线——技术栈是 Astro + Nginx,跑在一台 Linux 云服务器上。纯静态站点,没有数据库,没有后端框架,连个登录框都没有。

然后我看了一眼 Nginx 访问日志。

三天,一千多次请求。独立 IP 两百多个。

对于一个刚上线、还没有在任何渠道推广过的个人博客来说,这个数字不太正常。


二、访问日志分析:谁在访问我的博客?

流量构成

我把 personal-blog-access.log 拉出来跑了一遍统计:

类型请求数占比
正常人类访问~66758.5%
爬虫/扫描器~47341.5%

其中”正常人类访问”里,我自己贡献了很大一部分,因为我在测试站点、浏览文章、检查部署效果。去掉我自己之后,真实外部读者的 PV 大概是每天 100 出头。

而扫描器那 41.5% 的流量,来自至少 20+ 种不同的扫描工具。

扫描器图鉴

从 User-Agent 和请求特征来看,主要有这几类:

1. 全网资产测绘平台

这些是合法的安全研究项目,但它们不分青红皂白地扫全网所有 IP:

  • zgrab/0.x — Censys 的全网扫描器,28 次请求
  • CensysInspect/1.1 — Censys 官方标识,29 次
  • Infrawatch/1.0 — 另一个资产测绘项目,19 次
  • Palo Alto Networks — 企业安全公司的扫描,UA 里还留了文档链接

2. 漏洞探测工具

这类就比较恶意了:

  • Nmap Scripting Engine — 12 次,经典端口扫描
  • l9explore/1.2.2 — 47 次,自动化漏洞扫描器
  • Python-urllib/3.11 — 272 次,大概率是自动化脚本在批量探测

3. 手动/半自动攻击尝试

有人(或脚本)在有针对性地探测敏感路径:

探测路径次数意图
/.env/api/.env/backend/.env18找环境变量里的数据库密码
/phpinfo.php/phpinfo/14探测 PHP 信息泄露
/SDK/webLanguage22海康威视设备漏洞探测
/login/boaform/admin/formLogin7路由器/后台登录爆破
/phpmyadmin/index.php2数据库管理后台探测
blog/..%2f.envblog/..%2f..%2f.env2路径穿越攻击

4. 非法协议探测

这部分最离谱——日志里充斥着大量的二进制垃圾请求:

"\x16\x03\x01\x00\x8C\x01\x00\x00\x88\x03\x03..." 400 150
"MGLNDD_182.92.218.162_80" 400 150
"" 400 0

这些是扫描器在 HTTP 端口上尝试 HTTPS 握手、SOCKS 代理、Redis 协议等各种非 HTTP 协议。它们的逻辑很简单:对全网 IP 的常用端口发一堆协议试探,哪个端口有响应就标记下来。


三、已有的防护层

在部署 fail2ban 之前,博客其实已经有了一层防护——Nginx 层面的规则拦截。

Nginx WAF 配置

我在 nginx.conf 里配置了这几层:

# 第一层:IP 黑名单(geo 模块)
geo $blocked_ip {
    default         0;
    x.x.x.x         1;  # 之前发现 Nmap 扫描,手动加入
}

# 第二层:恶意 URI 拦截
map $request_uri $block_uri {
    default             0;
    ~*\.env$            1;
    ~*phpinfo           1;
    ~*\.git             1;
    ~*webmanager        1;
}

# 第三层:恶意 UA 拦截
map $http_user_agent $block_ua {
    default             0;
    ~*sqlmap            1;
    ~*nmap              1;
}

# 第四层:请求注入检测
map $args $block_injection {
    default                 0;
    ~*(union.*select)       1;
    ~*(<script>)            1;
}

# 组合判定:命中任意一层 → 403
map "$blocked_ip:$block_uri:$block_ua:$block_injection" $blocked {
    default     0;
    ~.*1.*      1;
}

然后在 server 块里:

limit_req zone=global burst=20 nodelay;

if ($blocked) {
    return 403;
}

这层防护效果不错——所有针对 .envphpinfo 的请求都被拦截并记录到了 security.log 里。

但它有几个明显短板:

  1. 只能拦截已知的——IP 黑名单需要手动维护,新扫描器来了拦不住
  2. 不管 SSH——非标准端口的 SSH 登录爆破完全没有防护
  3. 不自动封禁——同一个 IP 可以反复扫描,每次只返回 403,但消耗服务器资源

四、fail2ban 部署:让防护自动化

为什么选 fail2ban

fail2ban 的逻辑很简单:监控日志 → 匹配正则 → 触发封禁 → 写 iptables。不需要复杂的架构,也不需要额外的数据库。

对于个人博客这种场景,它正好填补了 Nginx WAF 的短板。

安装

Rocky Linux / CentOS / RHEL 系统自带 EPEL 仓库,一行搞定:

dnf install -y epel-release
dnf install -y fail2ban fail2ban-systemd

自定义 filter

fail2ban 自带了一些 Nginx 过滤器(nginx-botsearchnginx-bad-request),但都不太贴合我的场景。我写了一个自定义的:

/etc/fail2ban/filter.d/nginx-scan.conf

[Definition]
# 匹配 400 错误请求(二进制垃圾、非法 HTTP 方法等)
failregex = ^<HOST> - \S+ \[\] "(?:[^"]*)" 400 \d+

# 匹配 400/403/404 状态码
            ^<HOST> - \S+ .* ".*" 40[034] \d+

ignoreregex =

datepattern = {^LN-BEG}%ExY(?P<_sep>[-/.])%m(?P=_sep)%d[T ]%H:%M:%S(?:[.,]%f)?(?:\s*%z)?
              ^[^\[]*\[({DATE})
              {^LN-BEG}

核心就两条正则:

  • 第一条匹配空请求或畸形请求导致的 400(比如直接发二进制数据)
  • 第二条匹配所有 400、403、404 状态码的请求

jail 配置

/etc/fail2ban/jail.local

[DEFAULT]
# 白名单:本地 + 自己的 IP
ignoreip = 127.0.0.1/8 ::1 x.x.x.x/32

banaction = iptables-multiport
banaction_allports = iptables-allports
backend = polling

# 保守参数
bantime  = 24h
findtime = 10m
maxretry = 5

# ========== Jail ==========

# SSH 防爆破
[sshd]
enabled  = true
port     = <你的SSH端口>
filter   = sshd
logpath  = /var/log/secure
maxretry = 3
bantime  = 24h

# Nginx 高频扫描
[nginx-scan]
enabled  = true
port     = http,https
filter   = nginx-scan
logpath  = /var/log/nginx/personal-blog-access.log
maxretry = 10
findtime = 10m
bantime  = 720h    # 30 天

几个关键参数的选择逻辑:

参数原因
backendpollingNginx 写的是文件日志,不是 journald
maxretry10太低会误封(正常访问偶尔也会触发 404)
findtime10分钟10 分钟内 10 次异常请求才算扫描
bantime720小时扫描器直接封 30 天,不浪费精力
ignoreip本地回环 + 自己的 IP别把自己锁外面

验证测试

fail2ban-regex 测试 filter 匹配率:

fail2ban-regex /var/log/nginx/personal-blog-access.log \
  /etc/fail2ban/filter.d/nginx-scan.conf

结果:98 次匹配,61 行命中,0 行误报(200 响应全部跳过)。

然后手动模拟触发:

# 写入 12 条测试日志
for i in $(seq 1 12); do
  echo '99.88.77.66 - - [18/May/2026:13:00:00 +0800] "GET /test HTTP/1.1" 400 150 "-" "BadBot"' \
    >> /var/log/nginx/personal-blog-access.log
done

5 秒后查看状态:

Status for the jail: nginx-scan
|- Filter
|  |- Currently failed:	1
|  |- Total failed:	12
`- Actions
   |- Currently banned:	1
   `- Banned IP list:	99.88.77.66

iptables 规则已生效:

Chain f2b-nginx-scan
REJECT  all  --  99.88.77.66  0.0.0.0/0  reject-with icmp-port-unreachable

解封测试也正常:

fail2ban-client set nginx-scan unbanip 99.88.77.66

五、整体安全架构

部署完成后,博客的防护层级变成了这样:

┌──────────────────────────────────────────────────┐
│  Layer 1: 阿里云安全组                              │
│  只放行 80 端口,SSH 限制信任 IP                      │
├──────────────────────────────────────────────────┤
│  Layer 2: Nginx WAF                                │
│  geo IP 黑名单 + URI 拦截 + UA 拦截 + SQL 注入检测    │
│  limit_req 速率限制 10r/s                            │
├──────────────────────────────────────────────────┤
│  Layer 3: fail2ban                                 │
│  自动监控日志 → 10 分钟 10 次异常 → iptables REJECT   │
│  SSH 防爆破:3 次失败 → 封 24h                        │
└──────────────────────────────────────────────────┘

三层叠加的效果:

  • 安全组挡住端口级扫描(除了 80)
  • Nginx WAF拦截已知的恶意请求模式
  • fail2ban自动封禁未知扫描器,不需要手动维护黑名单

六、踩坑记录

坑 1:backend 选错了

一开始配置了 backend = systemd,以为日志会自动走 journald。结果 fail2ban 启动后 Total failed: 0——它根本没读到 Nginx 的日志文件。

改成 backend = polling 后才正常。如果你的 Nginx 没有配置写 journald,一定要用 polling

坑 2:datepattern 的百分号转义

.conf 文件里,% 需要用 %% 转义,但在 fail2ban-regex 命令行测试时又不用。一开始正则总报 InterpolationSyntaxError,排查了好久才发现是配置文件里多转了一层。

坑 3:正则不能太宽泛

最早写的 40[034] 正则是 .* 40[034] .*,结果把响应体里包含 “403” 字符串的正常 200 响应也匹配了。后来改成精确匹配状态码位置:".*" 40[034] \d+,确保只匹配 HTTP 状态码字段。


七、日常运维

几个常用命令:

# 查看封禁状态
fail2ban-client status nginx-scan
fail2ban-client status sshd

# 查看被封的 IP
fail2ban-client get nginx-scan banned

# 解封
fail2ban-client set nginx-scan unbanip <IP>

# 看实时日志
journalctl -u fail2ban -f

八、总结

一个个人博客的安全防护,做到三层就够了:

  1. 云厂商安全组——最外层,硬件级隔离
  2. Nginx WAF——应用层,拦截已知模式
  3. fail2ban——自动封禁,应对未知扫描

不需要上 WAF 商业产品,不需要搞 IDS/IPS 集群。对于一个静态博客来说,过度防御只会增加运维负担。

扫描器永远不会停止,但让它们每次扫完都被封 30 天,至少让它们知道:这个站不好惹。