安全响应头是最便宜的安全投资:一次配置,全站生效,且完全可以在无侵入的前提下逐步收紧。本文以“Node 应用 + Nginx 反向代理”的个人站点拓扑为例,给出一份经过验证的头清单与配置顺序。
分层原则:谁产生内容,谁负责应用级头;边缘负责协议级头
- 应用(Node/中间件):与内容相关的策略——CSP(是否允许 inline script)、缓存语义(no-store 的接口)、X-Content-Type-Options。
- 边缘(Nginx):与传输相关的策略——HSTS、HTTPS 重定向、点击劫持兜底、指纹屏蔽。
- 两边都可以设的头(如
X-Frame-Options),以边缘为准、应用为辅,不要在两层设置互相冲突的值。
逐项清单
Strict-Transport-Security(HSTS)——只在确认 HTTPS 稳定后启用,从小 max-age 开始:
add_header Strict-Transport-Security "max-age=300; includeSubDomains" always;
预检通过后再升到 max-age=31536000。一旦上大 max-age,测试环境用 HTTP 将被浏览器强制跳 HTTPS 难以回退;若要让浏览器可预加载进内置列表,需要 ≥1 年 + includeSubDomains + preload 三条件齐备,慎之又慎。
Content-Security-Policy——对个人站最常见的折中(Astro 会大量产出 inline 运行时脚本):
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
'unsafe-inline' 是务实起点而非终点:后续可用 nonce/hash 收紧脚本层。关键是先把 frame-ancestors、base-uri、form-action 这类零副作用的指令拉满——它们不关心 inline 与否,收益立现。
X-Frame-Options: DENY——老浏览器的点击劫持兜底,与 frame-ancestors 并存不冲突。
X-Content-Type-Options: nosniff——防止浏览器把响应猜成错误类型。零成本,必开。
Referrer-Policy: strict-origin-when-cross-origin——对外链只泄露 origin,站内保留完整路径。
Permissions-Policy——按实际功能关闭不需要的能力:
camera=(), microphone=(), geolocation=(), payment=()
Cache-Control 分层——静态产物长缓存要配内容哈希文件名;SSR 页面 public, max-age=0, must-revalidate;API 响应(尤其健康检查、订阅状态)一律 no-store,否则 401/429 会被缓存住造成“幽灵故障”。
顺序陷阱与验证
- 重定向链上的头:
add_header默认只挂在 2xx/3xx 的部分响应上,Nginx 里给跳转响应带头要用always参数,否则 HTTP→HTTPS 的 301 上没有 HSTS(浏览器此时还收不到 HSTS——这正确,HSTS 首次生效本来就要求一次成功 HTTPS)。 - 中间件写头 vs 不可变响应:若应用框架给响应对象加头(如 Astro middleware 里
response.headers.set(...)),注意某些运行时里Response头是不可变的,需要克隆后修改,否则线上直接 500——这类错误只在真实流量触发中间件时出现,构建期毫无征兆。 - 验证三件套:
curl -sI https://…逐头核对;Mozilla Observatory 打分回归;上线前用 staging 域名试 HSTS(别拿主域直调 max-age)。
小结
响应头没有高深技巧,只有“清单 + 分层 + 渐进收紧 + 每次都验证”。它保护不了所有攻击面,但它消灭的是一整类最低级也最常见的洞——而且账单只有几行配置。