初代个人博客安全加固实战:Nginx 反代的防御纵深设计
这不是一篇理论文章,而是我为自己博客的真实加固记录。博客用 Docker Compose 部署(Nginx → Halo 2.x → PostgreSQL 15),我把它当作一个安全作品来做:不追求复杂的自动化工具,而是把每一层防线设计清楚、踩过的每个坑记录下来。
背景:为什么给个人博客做安全加固
很多人的博客是"能用就行",可一旦部署到公网,互联网上的扫描器会在几分钟内扫到你的后台、尝试默认口令、探测 /actuator、/console 这些敏感路径。对网安学习者来说,把博客本身做成一个安全的作品,比单纯写文章更有说服力——因为它证明你真的理解攻击面。
我的架构非常简单:
浏览器 ──► Nginx(唯一对外端口 80)
├──► Halo 2.x(:8090,不对外)
└──► PostgreSQL 15(:5432,不对外)只有 Nginx 暴露在公网,Halo 和数据库都在 Docker 内部网络里。这个"缩小攻击面"的思路贯穿始终。下面是我叠加的四道防线。
防线一:后台 /console 的防御纵深
由于Halo 2.x 的后台路径是固定的 /console,不像 WordPress 那样能改随机路径字符串。路径隐藏这条路走不通,那就用"防御纵深":让攻击者即使猜到了后台,也进不来。
nginx/snippets/admin-access.conf:
# 必须同时满足: 命中白名单 AND 通过 Basic Auth (satisfy all)
satisfy all;
# ---- IP 白名单 ----
allow 127.0.0.1; # 本机回环
allow 10.0.0.0/8; # 私网段 A / 阿里云 VPC 内网
allow 172.16.0.0/12; # 私网段 B / Docker 默认网桥
allow 192.168.0.0/16; # 私网段 C / 家用局域网
deny all;
# ---- HTTP Basic Auth ----
auth_basic "Restricted Area";
auth_basic_user_file /etc/nginx/.htpasswd;
# 非白名单 IP 的 403 伪装成 404, 隐藏后台对公网的存在
error_page 403 =404 /404.html;三个关键设计:
satisfy all(与关系):必须同时命中 IP 白名单并且通过 Basic Auth 才放行。两道认证叠加,漏一个也进不来。非白名单 IP 返回 404 而不是 403:扫描器看到一个 404,会认为这个路径"不存在",从而放弃。后台对公网就像不存在一样。这个
error_page 403 =404是精髓。401 必须保留:白名单内的 IP 没带凭据时,Nginx 返回 401 挑战,浏览器才会弹出 Basic Auth 登录框。如果这里也伪造成 404,你就永远无法登录了。
生产环境里,把这几个私网段 allow 注释掉,只保留你运维出口的公网 IP 即可。
防线二:全站与后台限流(limit_req)
防暴力破解和 CC 攻击靠限流。Nginx 的 limit_req 是经典的令牌桶实现。
nginx/nginx.conf 里的两个 zone:
# 全站限流: 每 IP 20r/s, 突发 40
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=20r/s;
# 后台更严格: 每 IP 5r/s, 突发 5
limit_req_zone $binary_remote_addr zone=admin_limit:10m rate=5r/s;location 里的用法(burst 允许短暂的突发,nodelay 表示超出即直接拒绝而不是排队):
location / {
limit_req zone=general_limit burst=40 nodelay;
proxy_pass http://halo_backend;
}
location ~* ^/(console|uc)(/|$) {
limit_req zone=admin_limit burst=5 nodelay;
include /etc/nginx/snippets/admin-access.conf;
proxy_pass http://halo_backend;
}
# 登录接口单独加严, 防撞库/爆破
location = /api/auth/token {
limit_req zone=admin_limit burst=3 nodelay;
proxy_pass http://halo_backend;
}我实测过:对登录接口连续快速发 20 次请求,其中 8 次被直接返回 503。自动化爆破工具在这种限流下基本废掉。
防线三:CSP 与安全响应头
HTTP 响应头是浏览器侧的防线,成本极低但收益明显:
add_header X-Frame-Options "SAMEORIGIN" always; # 防点击劫持
add_header X-Content-Type-Options "nosniff" always; # 防 MIME 嗅探
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()" always;
add_header X-XSS-Protection "0" always; # 已被废弃, 显式置 0 是推荐做法其中最重要的是 CSP(内容安全策略)——它告诉浏览器"这个页面只能加载哪些来源的资源":
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; font-src 'self' data: https:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; connect-src 'self' https://www.halo.run; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;这里有个安全与兼容的权衡:Halo 默认主题 theme-earth 用 Alpine.js,它靠字符串求值实现交互,所以 script-src 必须放开 'unsafe-inline' 'unsafe-eval'。个人博客可以接受这个风险;如果换用 CSP 友好的主题,可以把 script-src 收紧成 'self'。我把"严格版"也注释在配置里,随时可以切换。
connect-src 是这次踩的一个坑:控制台的"应用市场"要从 www.halo.run 拉插件/主题列表,如果只写 'self',浏览器会在发出请求前直接拦截(网络日志里显示 [FAILED] csp),页面就报"网络错误,请检查网络连接"——哪怕你的网络明明是通的。所以 connect-src 只额外放行这一个官方域名。
还有个 add_header 继承陷阱(面试高频考点):add_header 在 server 层定义后作用于该 server 的所有响应,但一旦某个 location 内再出现 add_header,该 location 会局部覆盖、丢掉 server 层的所有安全头。所以我把所有安全头统一放在 server 层,location 内一律不写。
防线四:屏蔽 Spring Boot Actuator
Halo 基于 Spring Boot,Actuator 是它的监控端点。默认配置下 /actuator/env、/actuator/beans、/actuator/mappings 可能泄露环境变量、Bean 结构和路由映射——对攻击者来说就是一张内部地图。全部屏蔽:
location = /actuator/globalinfo {
# 唯一例外: 控制台引导依赖它, 内容不敏感
proxy_pass http://halo_backend;
}
location ~* ^/actuator(/|$) {
return 404;
}踩坑记录:加固策略本身也会引入 Bug
这部分是最有价值的——一个加固方案,如果不懂业务依赖,每一个策略都可能变成新的漏洞或故障。
Bug 1:控制台登录后无限转圈。 我把 /console 的限流设成 admin_limit burst=5,结果控制台登录后 SPA 一次性并行加载 20+ 个 JS/CSS 资源,超出 burst 的部分全被 503 拒绝,前端白屏转圈。修复:静态资源 /console/assets/ 单独走宽松的 general_limit burst=60,页面请求才走严格的 admin_limit。
Bug 2:屏蔽 Actuator 后控制台所有页面 403。 Halo 控制台在登录引导时要请求 /actuator/globalinfo,用它聚合用户的权限;我把所有 /actuator/* 屏蔽后,控制台的权限列表变成空,路由守卫把所有内容页面全拦了。修复:精确放行 /actuator/globalinfo(内容不敏感),其余仍屏蔽。
Bug 3:应用市场报"网络错误"但网络正常。 就是上面提到的 CSP connect-src 拦截。
这三个 bug 的共同教训是:安全策略要先搞清业务依赖,再动手写配置。每加一层防护,都要用健康检查验证一次,而不是"加完就算"。
验证:一键健康检查
加固到底有没有生效,不能靠感觉。我写了一个 health-check.sh,一键跑 7 项检查:
Nginx → Halo 转发正常
安全响应头存在(XFO / CSP / XCTO)
Server头不泄露版本号未认证访问
/console返回 401(Basic Auth 生效)带凭据访问
/console放行PostgreSQL 可连接
Halo readiness 就绪
全部 PASS 才算这次加固闭环。
生产迁移还要做什么
上线阿里云时,除了改域名,我还会再做几件事:
HTTPS 强制:通过
map $scheme在 HTTPS 下自动注入 HSTS,HTTP 层整体 301 到 HTTPS。IP 白名单收敛:把 admin-access.conf 里的私网段换成运维出口公网 IP,后台彻底只对"我自己"开放。
换 CSP 友好主题后收紧 script-src。
数据库和 Halo 端口保持不对外,安全组只开 80/443。
结语
防御纵深不是堆工具,而是每一层都假设"上一层已经失守"。IP 白名单防住扫描器,Basic Auth 挡掉撞库,限流压住爆破,CSP 拦住注入和 XSS,Actuator 屏蔽收敛信息泄露——每一层独立作战,又相互兜底。
这篇文章里最值钱的不是这些命令,而是那几个 bug 的复盘:一个安全方案踩了三个坑,最后通过健康检查闭环。这比"我用了 20 个安全工具"更能体现对安全的理解。