本文是「Nginx 避坑」系列第三篇,本系列共四篇
后续篇目:进阶与生产实践
这些 Nginx 反向代理的坑,我替你踩完了(反向代理与负载均衡)
https://oa22.cn/bbs.asp?id=38197
你的Nginx正在裸奔:配了HTTPS却从没真正安全过
https://oa22.cn/bbs.asp?id=38196
写了几年Nginx配置,这些错误我见过N多次了
https://oa22.cn/bbs.asp?id=38195
同事改了一行Nginx配置,大促当天系统崩—这5个教训,每一个都是真金白银换来的
https://oa22.cn/bbs.asp?id=38194
上个月帮一个朋友看线上系统,他说安全扫描的报告出来了,有点懵,让我帮他看看。
打开一看:证书链不完整、TLS 1.0没禁、安全响应头一个没加、CORS配置有重复头……密密麻麻一页。
HTTPS证书倒是买了的,绿锁也亮着。
但"有证书"和"配得安全",差得远呢。
今天把这些坑连同背后的处理思路一起说清楚,不只是给配置,更重要的是讲明白每个坑该怎么从根上解决。
🪤 坑一:证书链不完整,手机用户报错你却看不出来
现象:Chrome访问完全正常,但用户反映Android老机器打不开,或者运维用curl验证时报"证书不受信任"。
根本原因:你只传了域名证书,没拼上中间CA证书。
TLS信任链是三层结构:你的域名证书 → 中间CA证书 → 根CA证书。根CA预装在操作系统里,中间CA必须服务端下发。Chrome会自动联网补中间证书,所以你看不出来——但Android低版本、curl、物联网设备、金融APP里的自定义HTTP客户端,它们不会帮你补,直接报错。
解决方式:
cat your_domain.crt intermediate.crt > fullchain.crt
ssl_certificate /etc/nginx/ssl/fullchain.crt; # 完整链
ssl_certificate_key /etc/nginx/ssl/your_domain.key;
验证一下:
openssl s_client -connect yourdomain.com:443 -showcerts 2>/dev/null | grep -A2 "Certificate chain"
# 应该能看到至少两层:域名证书 + 中间证书
💡 最佳实践:别等用户反馈再查。把这条验证命令加进发布流程的checklist,每次上线新证书必跑一遍。用Let's Encrypt的话,Certbot生成的fullchain.pem本来就包含完整链,直接用就对了,不要自己手动拼。
🪤 坑二:HTTP跳HTTPS死循环,重定向次数过多
现象:浏览器报ERR_TOO_MANY_REDIRECTS,一直在跳。
原因:架构上有CDN,CDN到Nginx走HTTP(CDN做了SSL卸载)。Nginx看到$scheme是http就跳HTTPS,结果每次都跳,永远跳不出去。
错误配置:
server {
listen 443 ssl;
if ($scheme = http) { # CDN场景下$scheme永远是http,死循环
return 301 https://$host$request_uri;
}
}
正确做法:通过X-Forwarded-Proto判断原始协议,这个头是CDN传下来的真实信息:
server {
listen 80;
listen 443 ssl;
if ($http_x_forwarded_proto = "http") {
return 301 https://$host$request_uri;
}
}
💡 最佳实践:与其在Nginx层处理这个,不如把协议跳转的职责彻底交给CDN——在CDN控制台配置"HTTP强制跳HTTPS",Nginx只监听443,对80端口返回444(直接关闭连接)或者干脆不监听。
职责分离,一个层只做一件事,避免CDN和Nginx的配置互相干扰。每次出现跳转问题,你都能快速定位是哪一层的问题。
🪤 坑三:TLS还在支持1.0/1.1,安全扫描年年挂
现象:安全扫描报"支持TLS 1.0/1.1,存在POODLE/BEAST等漏洞",高危。
PCI DSS 2018年就强制要求禁掉TLS 1.0了,现在还有生产系统开着——往往是因为"配置从来没改过"。
推荐配置:
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
💡 最佳实践:加密套件这块不用靠记忆,直接去 Mozilla SSL Configuration Generator(ssl-config.mozilla.org)生成,选"Intermediate"配置,覆盖绝大多数现代浏览器同时安全性够用。
上线前跑一遍 SSL Labs(ssllabs.com/ssltest)评分,目标A或A+,低于这个不让上线。把这个评分要求写进发布标准,比任何安全培训都管用。
🪤 坑四:SSL Session缓存没配,HTTPS握手白白多花时间
TLS握手有非对称加密,比较耗时,尤其是移动端弱网用户感知明显。没有Session复用,每次连接都要完整握手。
ssl_session_cache shared:SSL:10m; # 10MB共享缓存,约可缓存40000个会话
ssl_session_timeout 10m;
ssl_session_tickets off; # ticket密钥轮转管理复杂,有前向安全隐患,直接关掉
💡 最佳实践:把OCSP Stapling也一起配上,进一步减少握手时客户端验证证书吊销状态的RTT:
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
这两个加在一起,HTTPS握手延迟能降一大截,移动端用户体验明显改善。
🪤 坑五:证书过期才知道,业务中断40分钟
这不是技术问题,是流程问题。但它真实发生了太多次:某天早上9点,用户投诉网站打不开,一查是昨晚证书过期了。
三层保障,缺一不可:
第一层:自动续期(Let's Encrypt)
certbot --nginx -d yourdomain.com
# 加入cron,每天自动检查续期
0 2 * * * /usr/bin/certbot renew --quiet --post-hook 'nginx -s reload'
第二层:监控告警,提前30天预警
#!/bin/bash
DAYS=$(echo | openssl s_client -connect yourdomain.com:443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2 \
| xargs -I{} date -d "{}" +%s \
| xargs -I{} expr \( {} - $(date +%s) \) / 86400)
[ $DAYS -lt 30 ] && \
echo "证书还有 ${DAYS} 天过期!" | mail -s "【告警】SSL证书即将过期" ops@yourcompany.com
第三层:云托管证书(企业推荐)
阿里云SSL、AWS ACM,托管证书自动续期,监控告警都有,什么都不用操心。
💡 最佳实践:三层都建立还不够,应急预案要提前演练。很多团队第一次手动替换证书是在生产事故里——手忙脚乱,40分钟还没搞定。平时在测试环境演练一遍证书替换流程,把步骤文档化,真出问题时5分钟搞定。
🪤 坑六:版本号暴露,给攻击者递情报
默认响应头:Server: nginx/1.18.0。攻击者看到版本号,直接去CVE库查这个版本的已知漏洞,针对性攻击。
server_tokens off; # 变成 Server: nginx,不暴露版本
💡 最佳实践:信息最小化是安全的基本原则——攻击者拿不到的信息就没法利用。除了server_tokens,还要配自定义错误页,防止Nginx默认错误页暴露服务器信息:
error_page 404 /404.html;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
另外,敏感路径也要主动屏蔽:
location ~ /\.(git|env|htaccess|DS_Store) {
deny all;
return 404;
}
🪤 坑七:安全响应头一个没加,白给攻击面
跑了好几年的系统,安全扫描一出,全是medium和high——点击劫持、MIME嗅探、XSS防护全没有。
add_header X-Frame-Options "SAMEORIGIN" always; # 防点击劫持
add_header X-Content-Type-Options "nosniff" always; # 防MIME嗅探
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Content-Security-Policy "default-src 'self';" always;
always这个参数别漏——没有它,4xx/5xx错误响应不带这些头,攻击者专门触发错误来探测服务器信息。
💡 最佳实践:去 securityheaders.com 输入你的域名,它会给出安全响应头的评分和缺失项,目标A或A+。把这个评分纳入上线检查项,跟SSL Labs评分一样,低于标准不让发。
🪤 坑八:add_header继承陷阱,安全头配了等于没配
这是Nginx里最反直觉的行为,很多用了好几年Nginx的人都不知道:在子location块里加了add_header,父级块的所有add_header在这个子块里全部失效。
server {
add_header X-Frame-Options SAMEORIGIN; # ← 会在/api/里消失
add_header X-Content-Type-Options nosniff; # ← 同上
location /api/ {
add_header Access-Control-Allow-Origin *; # 加了这一行
# server块的安全头在这里全部不生效!
# 攻击者可以针对/api/路径发起点击劫持
proxy_pass http://backend;
}
}
解决方案:把通用安全头写进独立文件,每个有add_header的location都include:
# /etc/nginx/security-headers.conf
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# 每个location里
location /api/ {
include /etc/nginx/security-headers.conf;
add_header Access-Control-Allow-Origin $http_origin always;
proxy_pass http://backend;
}
💡 最佳实践:如果觉得每个location都要include太麻烦,可以安装headers-more-nginx-module,改用more_set_headers指令——它没有继承问题,父块设置的头不会被子块覆盖,行为更符合直觉,维护成本更低。
🪤 坑九:没有限流,接口裸奔
没有限流配置,任何IP可以无限速地打接口——暴力破解密码、爬虫、CC攻击,完全没有防御。
# http块定义限流区域
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# 普通接口
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_conn conn_limit 20;
limit_req_status 429; # 返回429而不是默认的503
proxy_pass http://backend;
}
# 登录接口要更严格
location /api/login {
limit_req zone=api_limit burst=5 nodelay;
limit_conn conn_limit 5;
limit_req_status 429;
proxy_pass http://backend;
}
💡 最佳实践:限流不是一刀切的,要差异化:登录、注册、验证码接口最严格(防暴力破解);普通API中等;文件下载/上传接口看带宽情况单独设。
限流超限要记日志,配监控告警——短时间内大量429,往往是攻击的信号,要能快速发现。
🪤 坑十:内网接口裸露在公网
/metrics、/actuator、管理后台这些接口,只要能访问互联网就能打——没有IP限制的话等于把运维接口暴露给了全世界。
location /metrics {
allow 10.0.0.0/8;
allow 172.16.0.0/12;
allow 192.168.0.0/16;
allow 127.0.0.1;
deny all;
proxy_pass http://backend/metrics;
}
💡 最佳实践:Nginx层的IP限制是防线,但不应该是唯一防线。最佳架构是网络层隔离——内网服务不绑定公网IP,或者用VPN/跳板机访问内网,Nginx根本不暴露这些路径。Nginx层的deny all是最后一道保险,不是第一道门。
🪤 坑十一:CORS头重复,浏览器直接拒绝
现象:控制台报 The 'Access-Control-Allow-Origin' header contains multiple values '*, *',跨域全失败。
原因:Nginx加了,后端也加了,同一个响应头出现了两个值,浏览器不认。
解决:明确职责——由Nginx统一处理CORS,后端不管。
location /api/ {
proxy_pass http://backend;
proxy_hide_header Access-Control-Allow-Origin; # 清掉后端带来的
proxy_hide_header Access-Control-Allow-Credentials;
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With" always;
if ($request_method = OPTIONS) {
add_header Access-Control-Max-Age 86400; # 预检结果缓存24小时,减少预检次数
return 204;
}
}
💡 最佳实践:CORS处理的职责归属要写进团队API开发规范——统一由Nginx处理,后端不加任何CORS头。这条规范不写清楚,随着团队扩大,每个人都可能"顺手"在后端加一行,然后出问题又要排查半天。
🪤 坑十二:通配符 + Credentials,天天踩天天不知道为啥
# 这样配是错的!W3C规范明确禁止
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Credentials true;
Allow-Credentials: true时,Allow-Origin必须是具体域名,不能是*,浏览器会直接拒绝。这是规范,没有商量余地。
正确做法:用map维护白名单,动态反射Origin:
map $http_origin $cors_origin {
default ""; # 不在白名单,返回空,浏览器拒绝
"https://yourapp.com" $http_origin;
"https://admin.yourapp.com" $http_origin;
# 开发环境域名(生产可删)
"http://localhost:3000" $http_origin;
}
server {
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Vary Origin always; # 告诉CDN按Origin分开缓存,防止缓存污染
}
💡 最佳实践:Vary: Origin这行别漏。如果前面有CDN,CDN可能把Allow-Origin: https://yourapp.com的响应缓存下来,返回给https://admin.yourapp.com的请求,导致CORS失败。加了Vary: Origin,CDN就知道要按Origin分开缓存了。
最后
安全配置的特点就是:配错了表面什么事都没有,直到有一天漏洞被发现或被利用。
这些坑修起来每个不超过十行配置,但背后的原则是相通的:信息最小化、职责分离、自动化替代人工、差异化而不是一刀切。
把上面这些全部检查一遍,再把最佳实践逐步落地,配了HTTPS才算真的安全了。
下一篇聊最后一篇,也是含金量最高的一篇:nginx进阶配置与生产实践。
如果这篇对你有帮助,转发给还在踩这些坑的同事吧。
阅读原文:原文链接
该文章在 2026/3/27 16:37:07 编辑过