LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

你的Nginx正在裸奔:配了HTTPS却从没真正安全过

admin
2026年3月27日 15:57 本文热度 51

本文是「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 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2026 ClickSun All Rights Reserved