做 Nginx 配置这几年,要说哪个环节最容易被忽略但又最值钱,安全头绝对排得上号。很多人把证书一配、反向代理一开就觉得万事大吉,结果网站上线没几天就被安全扫描工具标了一堆“低级问题”,比如点击劫持、MIME 类型嗅探、明文传输警告。这些问题看着不痛不痒,真被利用起来还是挺麻烦的。这篇文章我就把 Nginx 里最常用、最该加的十几个安全头一次讲清楚,包含配置模板、参数选择逻辑、验证方法和踩坑记录,照着抄就能用。
先说清楚安全头到底解决什么问题。HTTP 响应头是服务器和浏览器之间传递元信息的方式,安全头就是其中一类专门约束浏览器行为的指令。比如告诉浏览器“这个页面不允许被 iframe 嵌入”“只能通过 HTTPS 加载”“不要猜测文件类型”,每一句话对应一类攻击面的收缩。配置安全头不需要改业务代码,不需要动数据库,在 Nginx 层面加几行配置就能覆盖全站所有响应,这也是我强烈建议优先在 Nginx 做这件事的原因。
这篇文章适合谁看?只要你的服务是 Nginx 对外提供访问,不管是静态站点、前端单页应用还是后端 API 网关,都值得花十分钟把这些头加上。如果你是运维、后端开发或者独立开发者,看完可以直接复制配置,再根据自己业务的实际情况调整参数。
1. 为什么要在 Nginx 层面配置安全头
1.1 安全头是浏览器和服务器之间的“安全约定”
先打个比方。你把网站比作一栋办公楼,HTTP 响应头就是入口处的安保指示牌。没有这些指示牌,访客(浏览器)只能靠自己的默认习惯行事,而浏览器的默认习惯往往不够安全。比如老版本的浏览器看到一个未知类型的文件,可能会自作主张去猜测它的真实格式,这个行为就叫 MIME 嗅探,攻击者可以利用它把恶意脚本伪装成图片上传,再诱导用户打开。安全头就是给浏览器下发明确指令:“这个楼不允许翻窗进,所有包裹必须走安检机。”
安全头的本质是“客户端安全策略”,它们的执行者是浏览器而非服务器。服务器只需要在响应头里声明策略,浏览器收到后会按照策略约束自己的行为。正因如此,配置安全头有一个天然的好处:只要浏览器遵循标准,攻击面就会直接缩小,而且这种收缩对服务器性能几乎没有影响。
1.2 在 Nginx 配置相比在应用层配置有什么优势
我在实际项目中见过不少团队把安全头写在应用代码里,比如 Java 的 Filter、Node.js 的中间件。这种做法不是不行,但有几个明显的短板:
第一,覆盖面容易有漏洞。应用层一般只能管到自己返回的动态响应,如果架构里有静态文件服务、图片裁剪服务、下载服务或者独立的网关,这些路径很容易漏掉安全头。在 Nginx 层配置是一次性覆盖所有经过它的响应,包括静态资源、错误页面、重定向响应,覆盖面完整得多。
第二,应急响应速度不一样。如果某天爆出一个新的安全头需要全网加,在 Nginx 改一行配置 reload 一下就能生效,在应用层要改代码、走发布流程、多节点滚动重启,效率差了一个量级。我在一次护网行动中就是靠 Nginx 层快速加了几个头,半小时内解决了所有相关告警。
第三,性能开销几乎为零。Nginx 加响应头是纯内存操作,不涉及业务逻辑,对 QPS 的影响可以忽略不计。相比之下,应用层每次请求都要跑一段中间件逻辑,虽然通常也不重,但总归有额外开销。
当然,应用层配置也有它的价值,比如可以根据用户角色动态生成不同的 CSP 策略。但我的建议是:以 Nginx 层为默认配置,应用层只在需要精细化控制的地方做补充,不要反过来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 最常用的几个安全头逐一拆解
2.1 Content-Security-Policy:内容安全策略
CSP 是安全头里边最复杂、也最能体现功底的一个,它的作用是限制浏览器可以加载哪些资源。以前开发者习惯用 X-Content-Security-Policy 或者 X-WebKit-CSP 这种兼容性写法,现在主流浏览器全部支持标准头 Content-Security-Policy,直接用这个就行。
CSP 的核心逻辑是“白名单”。你告诉浏览器:脚本只能从哪些域名加载、图片只能从哪些域名加载、表单可以提交到哪里、页面能不能被嵌入 iframe。如果页面里出现了白名单之外的资源请求,浏览器直接拦截并在控制台报错。
一个常见的起点配置是这样:
nginx复制add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self';" always;
这里每个指令都值得说清楚:
default-src 'self':所有没单独指定的资源类型,默认只允许从同源加载。这是 CSP 的地基,先把默认策略收紧,再逐个放宽。script-src 'self' 'unsafe-inline' 'unsafe-eval':脚本允许同源加载,同时允许内联脚本和 eval。说实话unsafe-inline和unsafe-eval是 CSP 里最敏感的两个词,如果业务代码里没有内联脚本,没有用new Function()或者 JSONP,建议果断删掉。但现实中很多老项目离不开它们,删了之后页面可能直接白屏,所以这里给的是一个“能跑起来的兼容方案”。img-src 'self' data::图片允许同源和 data URI。这个基本是标配,因为很多小图标和占位图会转成 base64。style-src 'self' 'unsafe-inline':样式允许同源和内联。内联样式几乎没办法完全禁掉,很多前端框架会在运行时往 DOM 里插 style 标签,所以这里默认放行。frame-ancestors 'self':这个指令和 X-Frame-Options 是同类,用来防点击劫持,后面会细说。它的优先级比 X-Frame-Options 更高,modern 浏览器都认这个。object-src 'none':禁止加载 flash 等插件资源。这个指令我建议所有站点都加上,因为 object 标签的利用面很广,但正常业务几乎用不到它。base-uri 'self':限制<base>标签的 URL。攻击者如果能篡改 base 标签,可以让页面上所有相对路径指向恶意域名,这个指令能堵住这个风险。form-action 'self':限制表单提交的地址,防止钓鱼表单把数据提交到第三方。
CSP 的配置难点不在语法,而在“怎么找出业务里所有需要放行的资源域名”。我的经验是先开 Content-Security-Policy-Report-Only 模式跑几天,这个模式只上报违规不改行为,能在不影响业务的情况下摸清资源加载的实际情况。等确认规则没问题了再切换成强制模式。
2.2 HTTP Strict Transport Security:强制 HTTPS 访问
HSTS 的作用是告诉浏览器:这个域名只能通过 HTTPS 访问,以后你看到这个域名就直接走 HTTPS,不用等服务器重定向。它的价值在于消除“首次访问是 HTTP”的窗口期。如果没有 HSTS,用户输入域名时浏览器默认走 HTTP,服务器再 301 跳转到 HTTPS,这个跳转过程可能被中间人劫持,用户在跳转前看到的可能是一个伪造页面。有了 HSTS,浏览器在缓存有效期内会直接发起 HTTPS 请求,完全不给 HTTP 机会。
Nginx 里的配置很简单:
nginx复制add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
参数含义:
max-age=31536000:单位是秒,31536000 秒就是一年。意思是浏览器在一年内都认这个策略。第一次配置时我一般建议先用较小的值,比如 86400(一天),确认没有影响后再逐步加大到半年或一年。原因后面在常见问题里细说。includeSubDomains:子域名也强制 HTTPS。如果你有子域名还在跑 HTTP,比如图片服务器或者旧的 API,这个参数会直接把它们“判死刑”,浏览器访问这些子域名也会强制走 HTTPS。所以加 includeSubDomains 之前务必确认所有子域名都支持 HTTPS。preload:允许域名加入浏览器的 HSTS Preload 列表。加入预加载列表后,浏览器甚至在用户第一次访问之前就知道这个域名必须用 HTTPS。这个参数本身不是标准的一部分,但几乎所有主流浏览器都认它。提交域名到 HSTS Preload 列表有严格的审核条件,必须全站 HTTPS 且所有子域名也支持,提交之后想撤销非常麻烦,所以不要轻易加。
还有一个容易忽略的点:HSTS 只在 HTTPS 响应里生效,浏览器收到 HTTP 响应时会直接忽略它。Nginx 里如果同时监听 80 和 443,需要确保 443 的 server 块里配置了这个头,而不是只在 80 的跳转块里配。
2.3 X-Frame-Options 与 CSP 的 frame-ancestors 联合防点击劫持
点击劫持的攻击方式是攻击者把一个透明 iframe 盖在诱导按钮上,用户以为点的是“领取优惠”,实际点的是 iframe 里的“删除账号”。防止这种攻击的方法就是告诉浏览器“不允许别的网站用 iframe 嵌入我的页面”。
X-Frame-Options 是传统方案,有三个取值:
DENY:任何情况都不允许嵌入。SAMEORIGIN:只允许同源页面嵌入。比如https://a.com的页面可以嵌入https://a.com/other。ALLOW-FROM https://example.com:允许指定站点嵌入。注意这个值兼容性很差,如果业务需要指定多个白名单域名,不要用 X-Frame-Options,直接用 CSP 的 frame-ancestors。
Nginx 配置:
nginx复制add_header X-Frame-Options "SAMEORIGIN" always;
我在实际项目里同时配置 X-Frame-Options 和 CSP 的 frame-ancestors,两者并不冲突。浏览器对这两个指令的优先级规则是:如果响应头里同时存在两者,浏览器遵循 frame-ancestors 的规则,忽略 X-Frame-Options。但为了兼容那些还不支持 frame-ancestors 的旧浏览器,X-Frame-Options 值得保留。
有个反直觉的坑我要提醒一下:不要在企业内部管理系统里无脑加 DENY。很多后台管理系统有“门户嵌入子应用”的需求,父页面和子应用往往不同域名,加了 DENY 会直接导致 iframe 白屏。这时候应该用 ALLOW-FROM 指定可信域名,或者用 frame-ancestors 列出白名单。
2.4 X-Content-Type-Options 和 Referrer-Policy
X-Content-Type-Options 只有一个取值:nosniff。它的作用是禁止浏览器对响应类型进行 MIME 猜测。举个例子,正常情况下服务器返回 Content-Type: text/html,浏览器就按 HTML 解析。如果没有 nosniff,即使 Content-Type 说的是 text/plain,浏览器也可能因为响应内容长得像 HTML 而直接按 HTML 解析。攻击者可以先把恶意脚本伪装成普通文本上传,诱导用户访问后,浏览器“帮忙”把它解析成 HTML 执行。
配置:
nginx复制add_header X-Content-Type-Options "nosniff" always;
这个头我建议无条件加上,几乎不会有任何副作用。真要说副作用,可能是某些老旧的浏览器插件或者不规范的资源访问会受到影响,但现在基本可以忽略。
Referrer-Policy 控制的是浏览器在跳转时,Referrer 请求头里携带多少来源信息。默认情况下,用户从你的网站跳转到外部站点时,浏览器会把完整的 URL 带上,包括路径和查询参数。如果 URL 里带了 session id、token 或者用户标识,这些信息就泄露给了第三方站点。
安全推荐配置:
nginx复制add_header Referrer-Policy "strict-origin-when-cross-origin" always;
strict-origin-when-cross-origin 是当前浏览器默认值,也是一个比较平衡的选项:同源请求带完整 URL,跨源请求只带源(比如 https://example.com),HTTPS 降级到 HTTP 时不带 Referrer。如果你的业务对隐私要求极高,可以用 same-origin,跨源一律不带;如果业务依赖 Referrer 做外链统计,那 strict-origin-when-cross-origin 是最不会出问题的选择。
2.5 Permissions-Policy 和 X-XSS-Protection 的现状
Permissions-Policy(以前叫 Feature-Policy)是用来控制浏览器特性的,比如地理位置、摄像头、麦克风、通知、支付等。如果你的站点只是个内容展示站,完全不需要这些功能,可以统一禁掉,缩小被恶意脚本调用的攻击面。
配置示例:
nginx复制add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), fullscreen=(self)" always;
这里的语法是:权限名=允许列表。() 表示不允许任何人使用,(self) 表示只允许当前站点自身使用。你可以按需调整,比如视频网站需要摄像头和麦克风,那就改成 camera=(self), microphone=(self)。
X-XSS-Protection 这个头在几年前很流行,写法是 X-XSS-Protection: 1; mode=block,作用是启用浏览器的内置 XSS 过滤器。但现在主流浏览器的态度已经变了:Chrome 76 之后禁用了这个过滤器,原因是过滤器的启发式检测代码本身存在被绕过和利用的风险,有时候反而制造漏洞;Firefox 从来就没实现过它;现代浏览器的 XSS 防护更多依赖 CSP 和浏览器的其他安全机制。所以现在的普遍建议是不再依赖这个头,甚至不推荐设置。如果出于兼容性考虑非要设置,可以设为:
nginx复制add_header X-XSS-Protection "0" always;
0 的意思是显式关闭过滤器,反而比 1; mode=block 更安全,因为不会触发那些有缺陷的过滤器逻辑。这个结论很多人不知道,值得记一下。
2.6 Cross-Origin-Opener-Policy 和 Cross-Origin-Resource-Policy
这两个头属于比较进阶的跨源隔离配置,近两年越来越重要。
Cross-Origin-Opener-Policy(COOP)控制的是“用 window.open 打开另一个源时,两者之间是否保持 opener 关系”。如果设置成 same-origin,你的页面打开其他源的窗口后,新窗口拿不到对旧窗口的引用,能有效防御一类叫“跨源窗口污染”的攻击。
配置:
nginx复制add_header Cross-Origin-Opener-Policy "same-origin" always;
需要注意,这个头如果设置不当会影响性能和功能。比如某些支付场景需要从第三方弹窗拿回执,弹窗和父页面之间可能需要 opener 通信。遇到这种情况要评估后再决定是否加限制。
Cross-Origin-Resource-Policy(CORP)比 COOP 更直接,它告诉浏览器这个资源“只允许哪些来源读取”。比如:
nginx复制add_header Cross-Origin-Resource-Policy "same-origin" always;
加了这个头之后,其他来源的页面就不能通过 fetch 或者 script 标签加载你的资源。这对 API 服务尤其有用,可以防止恶意网站利用用户浏览器向你的接口发起跨域请求。
3. 完整的 Nginx 安全头配置模板
3.1 一个可以直接套用的 server 块模板
下面这个配置是我在自己的项目里用的模板,兼顾了安全性和兼容性,适合大多数 HTTPS 站点:
nginx复制server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.com.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
# 安全头配置
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; frame-ancestors 'self'; object-src 'none'; base-uri 'self'; form-action 'self';" always;
# 反向代理示例
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 静态资源缓存示例
location /static/ {
alias /var/www/static/;
expires 7d;
add_header Cache-Control "public";
}
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
注意这里有一个很关键的点:add_header 指令是有继承规则的。如果当前 server 块里没有任何 add_header 指令,那么它会继承上一级(http 块)的 add_header;但如果当前块里出现了一条 add_header,上一级的 add_header 全部失效,必须在本块里重新写全。这个坑我踩过不只一次,后面在常见问题里专门展开。
3.2 参数选择背后的逻辑和场景适配
模板是死的,参数是活的。我讲讲几个关键参数怎么根据业务场景调整。
HSTS 的 max-age 和 includeSubDomains 要谨慎。如果是首次上线 HTTPS,建议先用 max-age=86400 跑一周,确认所有页面都能正常通过 HTTPS 访问,再逐步加大到 max-age=31536000。至于 includeSubDomains,如果有一个老项目的子域名还在用 HTTP,加了这个参数后用户访问会被浏览器直接拦截,那个子域名的访问量会瞬间变成零。真遇到这种情况,要么先把子域名升级成 HTTPS,要么就先不加 includeSubDomains。preload 参数更要慎重,不是所有站点都需要加入预加载列表,只有对传输安全性有极致要求的站点才值得做。
CSP 里的 script-src 是最容易出问题的指令。如果业务里用了 Vite 或者 Webpack 构建,打包出来的 JS 文件是外链形式,script-src 'self' 就够了。但很多老页面里有内联脚本,比如统计代码直接写在 HTML 里,这时候不加 unsafe-inline 页面就废了。我的建议是分环境处理:生产环境尽量做到不用内联脚本,实在做不到的,至少把 unsafe-inline 的范围缩小。
Permissions-Policy 的调整要看业务形态。做了在线会议系统的,需要开摄像头和麦克风;做电商的,支付接口可能要用到摄像头扫码。不要把上面模板的参数无脑复制到所有站点,按需删减才是正确的做法。
4. 验证安全头是否生效的几种方法
4.1 用 curl 快速检查响应头
配置完 Nginx,第一件事就是 reload:
bash复制nginx -t && nginx -s reload
然后直接检查响应头:
bash复制curl -I https://example.com
-I 参数只发送 HEAD 请求,拉取响应头。如果能看到这样一段:
code复制HTTP/2 200
server: nginx/1.26.0
content-type: text/html
strict-transport-security: max-age=31536000; includeSubDomains
x-content-type-options: nosniff
x-frame-options: SAMEORIGIN
referrer-policy: strict-origin-when-cross-origin
content-security-policy: default-src 'self'; ...
说明配置已经生效。如果看不到,先检查 Nginx 是否真的 reload 成功,再看是否因为 add_header 继承问题导致配置丢失。
如果只想检查某个特定头,可以配合 grep:
bash复制curl -sI https://example.com | grep -i 'strict-transport'
注意 curl -I 在个别场景下可能只显示部分响应头,如果怀疑被 CDN 或上游代理处理过,可以用 curl -sD - -o /dev/null https://example.com 拉出完整响应头再做判断。
4.2 使用在线扫描工具和浏览器开发者工具
在线工具方面,比较常用的是 securityheaders.com 和 Mozilla Observatory。前者会给出一个从 A 到 F 的安全头评分,并列出缺失的头。后者除了安全头,还会做更全面的 HTTPS 配置扫描。我一般在部署上线前后分别扫一次,上线前看问题清单,上线后确认修复效果。
浏览器开发者工具的 Network 面板也很有用。打开 F12,刷新页面,点任意一个请求,在 Response Headers 区域就能看到返回的安全头列表。这个方法的优势是能看到真实环境下的响应头,包括 CDN、WAF 等中间环节是否做了一些修改。
4.3 自动化巡检的基本思路
如果站点数量比较多,手动验证效率太低。可以考虑写一个简单的巡检脚本,定期跑一遍所有域名的安全头检查。基本思路是:用 curl 拉取响应头,解析出每个安全头的值,和预期配置做比对,不匹配的域名告警。
一个简单的判断逻辑示例:
bash复制#!/bin/bash
domain="example.com"
expected_hsts="max-age=31536000"
headers=$(curl -sI "https://$domain")
if echo "$headers" | grep -qi "strict-transport-security.*$expected_hsts"; then
echo "HSTS OK"
else
echo "HSTS MISSING"
fi
这种脚本不用太复杂,能覆盖“头是否存在、关键参数是否正确”就够了。有 CI/CD 流程的团队,还可以把它集成到发布流水线里,在上线时自动校验,避免配置了但没生效的情况流到生产环境。
5. 常见问题与排查技巧实录
5.1 add_header 继承导致配置“神秘失效”
这是 Nginx 安全头配置里最常见的坑,没有之一。Nginx 的 add_header 指令遵循“就近原则”和“整块覆盖原则”。意思是:如果在当前配置块里写了一条 add_header,这个块里就不会继承上级块的任何 add_header 指令。
举个例子,你如果在 http 块里配了 X-Content-Type-Options、HSTS 等一堆安全头,然后在某个 server 块里为了给某个接口加一个自定义头写了:
nginx复制add_header X-Custom-Header "hello" always;
结果就是这个 server 块下所有请求都只返回 X-Custom-Header,其他安全头全部消失。这不是 Nginx 的 bug,是设计如此:add_header 不是“追加模式”,而是“当前块有就用自己的,没有才继承上级”。
排查方法很简单:检查每个 server 块、location 块里是否含 add_header 指令,只要有,就要确认这个块里已经写全了所有需要的安全头。我之前就是在一个静态资源 location 里加了一个 Cache-Control 头,结果整个站点的安全头全部失效,线下扫了快两个小时才反应过来。
5.2 配置了 HSTS 后网站突然无法访问
问这个问题的人十个里有九个是加了 includeSubDomains 或者 preload,但某个子域名还在用 HTTP。加 includeSubDomains 后,浏览器对这个域名和它的所有子域名都执行“只能 HTTPS 访问”的策略。如果有一个子域名比如 img.example.com 没有部署 HTTPS,浏览器访问它的时候会直接拦截,报错提示“您的连接不是私密连接”或者直接拒绝访问,连 HTTP 跳转的机会都不给。
处理办法分两种情况:如果只是刚配置还处于试运行阶段,可以调低 max-age,比如改成 60 秒,让浏览器很快忘记这个策略,再排查子域名;如果已经上了 31536000 并且用户已经累积了缓存,那就只能等待 max-age 过期,或者等用户手动清除浏览器站点数据。所以再次强调:includeSubDomains 和 preload 一定要在确认所有子域名都支持 HTTPS 之后再加。
5.3 CSP 配置后页面白屏、功能不可用
CSP 是安全头里最容易引发“副作用”的一个,表现通常是:加了之后页面部分资源加载不出来,或者点击按钮没反应,控制台报一堆 Refused to load... 错误。
这个问题的根源是 CSP 白名单没有覆盖到业务实际使用的资源域名。比如页面引用了第三方统计脚本、字体 CDN、图片 CDN,而你的 script-src、img-src、font-src 里没放行这些域名,浏览器自然就拦了。
排查流程:
- 打开浏览器开发者工具,看 Console 里被拦截的资源 URL。
- 把域名逐个加到对应的指令白名单里。
- 刷新页面继续看是否有新的拦截项。
在正式切到强制模式之前,建议先在服务器上把 CSP 头改成 Report-Only 模式跑几天。Report-Only 模式下浏览器只上报违规不发拦截,能帮你摸清业务里所有需要放行的资源。
5.4 为什么安全扫描报告里还是显示“检测到 SSL 漏洞”
配置安全头解决的是“应用层安全策略”,如果 TLS 配置本身有问题,比如还在用 TLS 1.0 或 1.1、使用了弱加密套件,扫描报告照样会亮红灯。安全头和安全传输协议是两件事,都要做。
Nginx 里可以调整协议版本和加密套件:
nginx复制ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
注意 ssl_ciphers 的写法不能照抄模板不加验证,不同的 OpenSSL 版本支持的算法不同,配置完必须 nginx -t 验证。TLS 1.2 以下现在已经不建议启用了,除非你有特殊的旧客户端兼容需求,否则就不要开。
5.5 静态资源和 API 场景的安全头差异
静态资源和 API 的安全头配置应该是不一样的,不能共用一个模板。API 场景通常要考虑跨域问题,需要在响应头里设置 Access-Control-Allow-Origin,而静态资源往往需要设置缓存头。如果这些需求和安全头混在一起写,很容易出现 add_header 覆盖的问题。
我自己的习惯是分层处理:http 层放全局通用的安全头,server 层放站点特有的参数调整,location 层放资源类型相关的头。这样配置结构清晰,排查问题也快。
6. 零信任思维下的安全头扩展
前面讲的主要是“基础安全头”,属于看到就该加的那种。但如果安全要求更高,可以再往前推一步,按零信任的思路对响应头做精细化控制。
零信任的一个核心原则是“默认拒绝,按需放行”。对应到安全头的配置上,就是 CSP 不要用“先给一个大白名单再做减法”的思路,而应该“从最小可用开始,按需一点点加”。比如一个新页面只需要加载同源脚本和图片,那就只写 default-src 'self',别的都别加,等运行一段时间根据报错再慢慢放开。
还有一个经常被忽略的地方:错误页面。很多站点的 404、500 页面是在 Nginx 层直接返回的,如果安全头只写在应用层的响应里,这些错误页面就没有安全头。Nginx 的 error_page 指令在返回自定义错误页时,同样会经过当前 server 块的 add_header 处理,所以只要配置在 server 层,错误页也会带上安全头,这点不用额外处理。
另外,跨域场景下的安全头也不能忽视。如果你提供了 CORS 接口,不要简单粗暴地配置 Access-Control-Allow-Origin: *。配合 CORP 头可以把跨域读取范围收窄到可信来源,避免 API 被恶意站点当作“公共代理”使用。
7. 最后再分享一个小技巧
如果你经常要帮不同项目配 Nginx,强烈建议把安全头配置单独抽成一个文件,比如 /etc/nginx/conf.d/security-headers.conf,里面放一组基准配置,然后在各个 server 块里 include 进来:
nginx复制include /etc/nginx/conf.d/security-headers.conf;
这样改一处,所有站点同步更新。而且单独文件方便对比版本差异,也比在每个 server 块里复制粘贴一大段 add_header 清爽得多。唯一要记住的就是:如果某个 server 块里自己额外写了 add_header 指令,记得也要把这个 include 放在它后面,不然 add_header 的“整块覆盖”规则会让 include 进来的所有头全部失效。这个配置结构我在生产环境用了一年多,几十个站点统一管理,省了非常多的事。
