这不是一篇理论文章,而是我为自己博客的真实加固记录。博客用 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_headerserver 层定义后作用于该 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 项检查:

  1. Nginx → Halo 转发正常

  2. 安全响应头存在(XFO / CSP / XCTO)

  3. Server 头不泄露版本号

  4. 未认证访问 /console 返回 401(Basic Auth 生效)

  5. 带凭据访问 /console 放行

  6. PostgreSQL 可连接

  7. 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 个安全工具"更能体现对安全的理解。