博客上线没几天,我去看了一眼访问日志,整个人都不好了:每天都有几百次请求在扫根本不存在的路径,/wp-login.php、/admin、/phpmyadmin,轮着来。我的站是 Hugo 生成的静态站,压根没有这些后台,但扫描器不管,它就是挨家挨户敲门,看看哪家没锁。
那之后我把 Nginx 加固认真做了一遍。先说结论:静态站本身就很难被攻破——没有数据库、没有登录入口,攻击面天生就小。加固的目标不是把服务器做成堡垒,而是让扫描器觉得“这家不好惹”,去敲别人家的门。六道防线,花一下午,够用了。
第一道:别把底牌亮出来
Nginx 默认会在出错页面和响应头里报自己的版本号,比如 Server: nginx/1.24.0。这等于告诉扫描器:“我用的就是这个版本,去查它的已知漏洞吧。”关掉它,一行配置:
server_tokens off;
放在 http 块里全局生效。改完重启,用 curl -I 看一眼响应头,版本号没了,心里就踏实了一半。顺带把不必要的错误页面也收一收,别让 404 页面泄露目录结构。
第二道:限流,让暴力请求先累趴下
扫描器最爱干的事就是短时间内发几千个请求。Nginx 的 limit_req 就是给这种行为设个红绿灯:
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
server {
location / {
limit_req zone=one burst=20 nodelay;
# ……
}
}
意思是同一个 IP 每秒最多 10 个请求,突发允许 20 个。正常读者根本碰不到这个上限——人一秒点不了一次以上的链接;扫描器一撞上限就收到 503,爱扫就慢慢扫去。登录、管理后台这类敏感路径,可以再单独设一个更严的 zone,比如每分钟 5 次。
第三道:安全头,花五分钟加上
安全头是浏览器帮你挡子弹的配置,几行就能加上:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; style-src 'self' 'unsafe-inline';" always;
X-Frame-Options 防别人把你的站嵌进 iframe 里钓鱼;nosniff 防浏览器猜错文件类型;CSP 是大头,管的是“你的页面只许加载哪些来源的资源”。静态站的 CSP 很好写——资源基本都在自己域名上。加完用浏览器的开发者工具看一眼,没报错就说明没配错。别照抄别人的长 CSP 再回来修半天,静态站从宽松的开始,够用就行。
第四道:fail2ban,自动拉黑屡教不改的
Nginx 限流是“慢下来”,fail2ban 是“直接关门”。它读 Nginx 的错误日志,发现同一个 IP 短时间内大量 404、403 或者反复撞同一个不存在的路径,就用防火墙封它一段时间。配法是现成的:
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
maxretry = 5
bantime = 3600
findtime = 600
10 分钟内触发 5 次就封一小时。封多久可以按自己的脾气调——我后来改成累犯封一周,初犯只封 10 分钟,毕竟误伤正常读者的代价比放过扫描器大。之前 Ubuntu 加固那篇里写过 fail2ban 的基础安装,这篇算是它的 Nginx 续集。
第五道:只开必要的端口,其余的都关上
我的服务器上只应该对外开 80 和 443。检查一下现在到底开了什么:
sudo ss -tlnp
列表里如果有你不认识的端口,先弄明白它是谁再决定留不留。防火墙用 UFW 就行,默认拒绝入站,只放行 80、443 和你自己用的 SSH 端口:
sudo ufw default deny incoming
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 22/tcp
sudo ufw enable
SSH 端口能改就改,别用默认的 22 裸奔——改端口不是万能,但能挡掉九成随手扫 22 的脚本。配合密钥登录,密码登录直接关掉,暴力破解连门都找不到。
第六道:证书和文件权限,别留低级漏洞
HTTPS 现在是标配,Let’s Encrypt 免费证书加自动续期,反向代理那篇里写过完整流程,这里只补两个容易忽略的点:
证书续期要真能跑。 自动续期依赖 80 端口能通,我就吃过亏:防火墙把 80 挡了,续期失败,证书过期,站直接打不开。现在每次续期前后我都会看一眼证书到期时间,openssl x509 -enddate 一行命令的事。
文件权限别图省事给 777。 网站目录 755、文件 644 就够了,Nginx 的 worker 进程只需要读权限。之前我还踩过一个坑:用 root 复制文件进站目录,权限变成 640,Nginx 读不到,公网直接 403,文件明明在那儿就是打不开。部署完一定用公网 URL 实测一遍状态码,别信“文件在就等于能访问”。
一张表:六道防线怎么排优先级
| 防线 | 花多久 | 防什么 | 先做哪个 |
|---|---|---|---|
| 藏版本号 | 1 分钟 | 版本定向攻击 | 先做 |
| 只开必要端口 + UFW | 10 分钟 | 端口扫描、暴力破解 | 先做 |
| 安全头 | 5 分钟 | 点击劫持、MIME 嗅探 | 先做 |
| 限流 | 15 分钟 | 暴力请求、爬虫 | 再做 |
| fail2ban | 20 分钟 | 反复试探的扫描器 | 再做 |
| 证书续期 + 权限检查 | 10 分钟 | 过期宕机、低级失误 | 每次部署都做 |
加固这件事,最怕两种心态:一种是觉得“我的小站没人惦记”,一种是想一口气配成铜墙铁壁。前者是错觉——扫描器是全网扫的,不挑站;后者是过度工程,一下午配完六道防线,剩下的时间不如去写文章。我的站到今天还是 Hugo + Nginx 的老组合,没换过架构,搭站全过程里那套东西加上这六道防线,应付日常扫描绰绰有余。
最后说一句实在的:加固做完不是一劳永逸。日志每周扫一眼,看看有没有新花样;证书到期前看一眼,别等过期了才想起来。安全这件事,靠的不是一次配得多狠,而是别让它在你遗忘的角落里悄悄过期。
