ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

反向代理HTTPS降级为HTTP:Nginx配置陷阱与排查指南

反向代理HTTPS降级为HTTP:Nginx配置陷阱与排查指南 半个月前帮一个团队排查故障现象非常典型他们的Nginx反向代理配置了正规的HTTPS证书浏览器打开主域名没有任何安全警告但页面里的图片、样式却全部加载不出来登录状态也保不住。我第一反应是证书链没配全结果用curl一测入口证书完全正常。再往下追才发现问题根本不是“入口HTTPS没生效”而是从反向代理往后端转发的过程中协议被悄悄降级成了HTTP——明文流量在内网里跑来跑去后端程序自己也不知道外界其实是HTTPS进来的。这个场景在反向代理的实际使用中太常见了。很多团队会在HTTPS入口上卡很久却往往忽略反代和后端之间的链路。这篇文章要聊的就是这种“隐形陷阱”明明外层是HTTPS请求却降级为HTTP的各种原因、表现和排查方法。无论你是负责Nginx、HAProxy的运维还是经常处理上线问题的全栈开发下面这些案例大概率能帮你省下半天踩坑时间。1. 先说清楚反向代理到底在什么环节弄丢HTTPS1.1 两种工作姿态SSL终结与SSL透传先厘清一个基础概念不然后面全是糊涂账。反向代理和上游服务之间通常有两种工作模式。第一种叫SSL终结SSL Termination。在这种模式下浏览器和反向代理之间建立TLS加密链路证书部署在反代上反代解密之后再以普通HTTP重新发往后端。绝大多数网站都是这么干的因为反代可以做负载均衡、缓存、七层路由后端服务不感知TLS开发调试也简单。但代价是从反代到后端那一段流量是明文。第二种叫SSL透传SSL Passthrough。反代不碰TLS握手只做四层TCP转发浏览器直接和后端完成加密协商。这种模式安全上最省心但反代无法理解HTTP协议做不了路径路由和精细的负载均衡只适合部分特殊场景。“HTTPS请求降级为HTTP”在绝大多数情况下发生在第一种模式里。反代和后端之间那段明文HTTP如果你有意为之并且内网隔离足够好可以接受但如果是因为配置错误本该走TLS的流量却走了明文那就不只是安全隐患的问题很多诡异故障也会从这里长出来。1.2 “降级为HTTP”的三种真实表现“降级”这个词听起来很抽象实际表现无非这三种你可以对照自己遇到的情况第一种浏览器报Mixed Content警告。HTTPS页面里加载了HTTP资源图片、脚本、接口被现代浏览器直接拦截。页面功能残缺但地址栏的锁还在最容易让人误以为是后端代码写死了协议。第二种用户被重定向到http://开头的地址。登录成功后跳转到明文链接浏览器地址栏的安全锁消失如果Cookie带有Secure属性登录态可能直接丢失表现为“登录失败”或“反复掉线”。第三种反代日志里出现大堆502或400。反代用明文HTTP去访问一个只接受TLS的上游端口上游直接拒绝错误五花八门但根因都是同一件事。这几种表现你单看某一种很容易往“应用代码问题”或“网络问题”方向排查但实际上元凶都集中在反代配置、代理头处理、重定向处理这几处。接下来我把最常见的几个“隐形陷阱”逐个拆开讲。2. 第一个隐形陷阱proxy_pass的scheme写错TLS握手上游直接不认2.1 最典型的错误配置长什么样先看一段很多人写过、也踩过坑的Nginx配置server { listen 443 ssl; server_name api.example.com; ssl_certificate /etc/nginx/certs/server.crt; ssl_certificate_key /etc/nginx/certs/server.key; location /api/ { proxy_pass http://backend_service; proxy_set_header Host $host; } }看代码完全没问题对吧域名证书配得好好的proxy_pass指向一个叫backend_service的内部服务按常规理解请求从这里转发出去还是给到后端完成。问题恰恰就出在这个http://上。如果backend_service实际监听的是443端口、并且该服务自己也是用TLS包装的那么这个反代就是在用明文HTTP请求去打一个TLS端口。后端TLS层收到的不是ClientHello而是一串普通的“GET /api/ HTTP/1.1”文本它根本认不出来直接断开连接或返回400。反代那边拿不到有效响应就给用户返回502。还有一种更隐蔽的写法location /api/ { proxy_pass https://backend_service/; }这个scheme写对了但如果你没写端口而上游刚好监听在非标准端口上同样会连不上。Nginx做HTTPS上游转发时默认走443做HTTP上游转发时默认走80这两个默认值很容易让人忽略端口的存在。2.2 现象与根因明文请求打在TLS端口上很多人在这一步会犯迷糊我明明配了证书访问域名也是https为什么反代还会往上游发明文这就是对“SSL终结”理解不深导致的。Nginx在作为反向代理转发请求时它自己同时扮演“服务器”和“客户端”两个角色。对浏览器那边它是服务器承担TLS握手、证书校验对上游那边它是客户端负责建立新的连接并发送请求。这个新连接用什么协议完全由proxy_pass里的URL scheme决定——写http://就是明文写https://就带TLS。入口的HTTPS配置跟出口的协议没有任何自动关联。如果后端服务配置了强制TLS明文请求打过去通常会遇到两种情况后端只监听TLS端口收到明文后直接报错日志里会看到类似http request line parsing failed或ssl_error的记录后端做了“双模式监听”80和443都开着甚至自动识别协议这种情况下有时能通但行为不稳定今天好明天坏。有个本地代理场景特别典型一个小工具监听了127.0.0.1:1572端口这个端口本身是HTTPS服务但某配置里把它写成了http://127.0.0.1:1572于是每次请求都返回unexpected status 502 bad gateway: unknown error。这类报错我在Docker配置镜像代理、内网服务接入网关时都见过追到底都是同一句话scheme和端口没有对齐。2.3 为什么502会和这个陷阱深度绑定502的本质含义是“网关从上游收到了无效响应”。大多数人对502的第一反应是“服务挂了”或“超时了”但在反代场景中502还有一个低频但高频出错的原因反代发出的请求协议上游根本没法解析。当一条明文HTTP请求发到TLS端口时上游TLS层根本不会把它当成合法的HTTP请求来对待而是直接丢弃连接。Linux下表现尤为明显Nginx很快收到一个connection reset于是立刻向上游重试如果上游配置了多个节点就逐个试一遍都失败后Nginx就把502返回给客户端。排查这类502不要一上来就重启Nginx、调大proxy_read_timeout这些操作基本没用。正确的第一步是看反代的error.logtail -f /var/log/nginx/error.log如果看到upstream prematurely closed connection while reading response header from upstream且浏览次数一多就要怀疑是不是协议层不匹配。然后确认三件事上游到底监听在哪个端口这个端口是HTTP还是HTTPSproxy_pass里的scheme跟它对得上吗提示遇到“HTTPS入口 502”的组合先对scheme和端口再动其他配置。这个顺序能省掉大量无效操作。3. 第二个隐形陷阱应用层重定向把用户踹回HTTP3.1 反代背后的Location劫持第二个隐藏得更深它发生在应用层的重定向逻辑里。场景是这样的内网某后端服务只监听HTTPNginx对外提供HTTPS入口。用户访问https://example.com/login提交表单后后端处理成功需要把用户跳转到/dashboard页面。这个跳转通常以HTTP响应头Location: /dashboard的形式返回。麻烦在于许多应用框架生成重定向地址时会用request.getScheme()这种方式来拼接完整URL。后端看到的原始请求是http://example.com/login因为Nginx转发时用的是明文HTTP它自然就会生成一个Location: http://example.com/dashboard。浏览器收到这个响应后不管最初访问的是不是HTTPS都会乖乖地按Location里的地址重新发起请求。于是用户明明是从HTTPS页面进来的登录跳转却直接变成了HTTP明文地址。地址栏的安全锁消失带有Secure属性的Cookie在HTTP下不会被发出去登录态当场丢失。用户的表现就是登录一次失败一次或者跳转后页面样式全乱。3.2 修复的两个关键absolute_redirect和proxy_redirect处理这个问题的关键是两层一是让Nginx自己不要生成绝对地址二是把上游返回的Location里的协议改回来。对于Nginx自身生成的重定向可以关闭绝对地址server { listen 443 ssl; server_name api.example.com; absolute_redirect off; ... }这个指令从Nginx 1.14.1开始支持关了之后Nginx生成的重定向会用相对路径比如/dashboard而不是http://api.example.com/dashboard。相对路径天然与当前协议保持一致浏览器访问的时候自然会用当前的HTTPS。对于上游返回的Location要用proxy_redirect指令做修正location / { proxy_pass http://backend; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_redirect http:// https://; }这一段的意思是上游返回的Location头里把http://开头的地址重写成https://。注意这个重写是临时性的修正治标不治本。如果后端应用框架本身支持“告诉它原始协议”优先从应用层解决下一章会讲否则proxy_redirect就是你最后的兜底手段。3.3 一个Nexus仓库反向代理的真实案例这类重定向问题在某些依赖“绝对URL”的业务场景里特别致命比如制品仓库。有一次配置Nexus 3.40.1的raw仓库做HTTPS对外发布客户端推拉包时不断报错错误日志里指向的路径总是不对本该落在https://repo.example.com/repository/raw/下面的包实际请求却被打到了http://repo.example.com/repository/raw/。排查后发现是两处叠加。第一Nexus本身配置了Base URL但写的是内网HTTP地址导致它生成的页面链接和重定向全是内网HTTP地址第二Nginx反代没有对X-Forwarded-Proto做透传Nexus根本不知道外界是HTTPS进来的。修复也不复杂第一步把Nexus的Base URL改成https://repo.example.com第二步在Nginx的location里把proxy_set_header X-Forwarded-Proto $scheme;加上第三步用proxy_redirect把重定向头里的HTTP改写成HTTPS。三步做完仓库的路径解析和推拉包就都正常了。这类案例在依赖“绝对URL”做业务逻辑的系统里比比皆是Docker Registry、GitLab、Artifactory等自托管服务都容易踩排查思路完全一致。4. 第三个隐形陷阱后端不信任X-Forwarded-Proto自己生成了一堆HTTP链接4.1 代理头如何影响应用生成绝对URL先说清楚X-Forwarded-Proto是干什么的。反向代理转发请求时后端看到的实际HTTP请求是由反代发起的这个请求的协议形态取决于反代和上游之间怎么通信而不是浏览器和反代之间怎么通信。为了让后端了解“真实客户端用的什么协议”反代需要在转发请求时带上X-Forwarded-Proto头location / { proxy_pass http://backend; proxy_set_header X-Forwarded-Proto $scheme; }加了这一行之后后端读取请求头时就能发现X-Forwarded-Proto: https从而知道浏览器是通过HTTPS访问的。不少框架在生成绝对URL、判断安全Cookie、识别跨域策略时都会看这个头。但光反代加了还不够后端应用还必须“信任”这个头。很多框架出于安全考虑默认不处理X-Forwarded-*头它们在拿到请求后依然用schema://host这种形式拼接地址而这里schema取的是请求自身的协议即HTTP。于是后端生成的页面里大量静态资源URL和接口地址全部是http://开头的。浏览器在HTTPS页面里看到这些HTTP资源请求直接按Mixed Content规则拦截掉页面就白屏或各种功能异常。4.2 主流框架信任代理头的配置不同框架的信任方式不太一样给你列一份对照框架信任代理头的配置方式Spring Bootserver.forward-headers-strategyframework或配置ForwardedHeaderFilterDjangoSECURE_PROXY_SSL_HEADER (HTTP_X_FORWARDED_PROTO, https)配合USE_X_FORWARDED_HOST TrueFlask使用ProxyFix中间件app.wsgi_app ProxyFix(app.wsgi_app, x_proto1, x_host1)Express / Node.jsapp.set(trust proxy, true)注意生产环境建议限制为具体代理IPASP.NET Core使用ForwardedHeaders中间件并配置ForwardedHeadersOptions配置生效后后端生成的所有绝对地址都会自动变成HTTPS问题在源头就解决了比在Nginx层做proxy_redirect修正更干净。这里要特别提醒信任代理头是有安全代价的。如果你把后端直接暴露在了不可信网络里客户端完全可以伪造X-Forwarded-Proto: https来欺骗应用层逻辑。正确的做法是后端只监听内网地址只允许反代的IP访问在这个前提下再启用代理头信任。4.3 区分“对外降级”和“内部降级”讲了这么多其实可以把“降级”梳理成两个维度排查时先分清是哪种对外降级指的是浏览器和反代之间的HTTPS被打破典型表现是地址栏锁消失、Mixed Content、重定向到HTTP地址。问题出在重定向处理、HSTS缺失、代理头传递不正确等环节。内部降级指的是浏览器到反代这段还是HTTPS但反代到后端这段变成了明文HTTP。这既可能是配置问题proxy_pass写错scheme也可能是设计问题内网链路有意不加密。问题出在反代的转发逻辑、上游服务的TLS配置上。判断是哪种也很简单看浏览器地址栏锁是否还在再看后端日志里请求的来源协议。浏览器锁没了优先查对外环节浏览器锁还在而后端收到的是明文HTTP优先查反代到后端的转发配置。这样一分排查范围立刻缩小了。5. 绕不开的握手协议SNI、ALPN与HTTP版本协商的交叉影响5.1 当上游强制HTTP/1.1HTTP/2请求会怎样scheme的问题解决了还有一个容易忽视的环节协议版本的协商。默认情况下Nginx到上游的转发用的是HTTP/1.0或HTTP/1.1除非你显式配置了proxy_http_version和upstream的HTTP/2支持location / { proxy_pass https://backend; proxy_http_version 1.1; }如果上游服务只接受HTTP/2比如某些部署成h2-only的gRPC或WebSocket服务反代拿HTTP/1.1请求打过去轻则功能不正常重则握手直接失败。反过来也一样浏览器和反代之间通过HTTP/2顺畅通信但反代内部把HTTP/2翻译成HTTP/1.1明文转发到后端依赖HTTP/2特性比如流优先级、服务端推送的东西就全部失效。这个“翻译”过程本身不是错误但当你看到浏览器侧一切正常、后端侧却报协议错误时要能联想到问题可能出在“HTTP/2入口 HTTP/1.1出口”的组合上。5.2 SNI与ALPN这些TLS细节在反代场景中的表现TLS握手时有两个扩展经常在反向代理场景里搞事。SNIServer Name Indication是客户端在TLS握手时带上的目标域名反代根据它来选择证书和后端路由。如果域名和server块匹配不上Nginx会fallback到默认证书浏览器可能弹证书错误更麻烦的是如果SNI识别到的域名被路由到了错误的后端用户看到的是“明明访问A站却打开了B站的内容”。排查这类问题时先确认server_name、证书文件名、以及反代后端的路由规则三者之间是否对齐。ALPNApplication-Layer Protocol Negotiation是在TLS握手内协商“隧道里跑什么协议”的机制客户端和服务器共同决定是用h2还是http/1.1。如果反代的上游TLS配置里没有正确支持ALPN某些对协议敏感的客户端会降级尝试HTTP/1.1而h2-only的客户端则可能直接拒绝连接。举个例子你在反代配了proxy_pass https://backend;但上游Nginx只开了ssl_early_data之类的花哨配置没有监听HTTP/2ALPN协商出来的是http/1.1。客户端如果是旧版可能没有感知如果是新版且偏好HTTP/2它连接时发现ALPN没有h2就认为服务器不支持可能直接放弃。表现就是“偶尔能通、偶尔连不上”。5.3 多域名共用反代时的典型乱象多域名共用同一个反代服务器的时候问题会被放大。最常见的是复制粘贴导致的错配多个server块共用了同一个proxy_pass或者证书路径写串了。症状很典型访问a.example.com却出现了b.example.com的内容Cookie串域导致登录态混乱甚至HTTPS证书链都正常但页面接口一直401。排查方法并不复杂用openssl s_client分别查每个域名的证书和SNI路由echo | openssl s_client -connect a.example.com:443 -servername a.example.com 2/dev/null | grep -E subject|issuer|ALPN再看反代日志里实际转发的upstream地址确认域名、证书、backend三者是否一一对应。这类问题往往不是“技术不够”造成的而是配置管理太随意。6. 我的完整排障链从浏览器到后端的逐跳验证6.1 复现与初步定位纸上谈兵再多不如完整走一遍排障流程。说一个我实际遇到的场景你跟着走一遍思路。故障现象访问https://help.example.com/docs页面能打开但样式全丢控制台一堆Mixed Content警告登录后会跳转到http://help.example.com/login然后登录态丢失。第一步永远是复现把现象固定下来。我打开浏览器无痕窗口输入HTTPS地址F12切到Console看到“Mixed Content: The page at https://... was loaded over HTTPS, but requested an insecure resource http://...”。这一步基本可以确定页面里有HTTP绝对URL。但HTTP URL是谁生成的反代后端得逐跳排查。6.2 用openssl和curl拆解每一跳先验证入口curl -sI https://help.example.com/docs看返回的响应头。如果Location是http://开头那就是重定向问题如果Content-Type正常且返回200再看HTML源码里有问题的资源URL是哪来的。接着验证后端curl -sI http://127.0.0.1:8080/docs -H Host: help.example.com这里直接绕过反代用HTTP地址访问后端。如果后端返回的Location是http://help.example.com/dashboard说明后端只处理了HTTP的scheme压根不知道外面其实是HTTPS。再用openssl验证后端端口是不是真的不跑TLSecho | openssl s_client -connect 127.0.0.1:8080 -servername help.example.com 21 | head -20如果这条命令直接报no peer certificate或ssl handshake failure说明8080端口没有启用TLS反代到后端走的是明文HTTP。到这里内部降级已经实锤。6.3 抓包确认流向后端时确实是明文为了让证据链更完整直接在反代服务器上抓包tcpdump -i eth0 -nn -A port 8080然后从前端重新触发一次HTTPS请求观察tcpdump输出。如果你看到的是完整的HTTP明文行GET /docs HTTP/1.1 Host: help.example.com User-Agent: curl/8.0.1那就说明浏览器和反代之间的TLS确实已经终结在反代身上反代到后端的确是明文。这正是“内部降级”的铁证。6.4 修复与回归测试根因清楚后修复方案就很明确了。这个案例里是两处叠加Nginx没有传X-Forwarded-Proto后端应用也没信任代理头。我在Nginx加了location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect http:// https://; }同时在后端应用的配置文件里把对应框架的“信任代理头”配置打开。重新加载后再做一轮回归测试curl -sI https://help.example.com/docs | grep -i locationLocation确认是https://开头再用浏览器无痕模式重新走一遍登录流程登录态稳定样式、接口全部正常。到这里才算真正修复完毕。提示有些遗留系统没法快速改配置临时用proxy_redirect顶住也OK但一定要在问题单里注明“临时方案”避免后续接手的人被误导。7. 防止再次踩坑全链路HTTPS保障清单7.1 不要把所有证书都堆在反代一层先说一个很多人不爱听但又必须提的观点反代终结TLS之后反代到后端那一段能不能永远走明文取决于你对自己的网络有多少信心。如果你只是在自己笔记本上跑个Demo明文链路无所谓。但如果这条链路会跨网段、跨云VPC、经过共享物理网络或者你的合规要求本来就要求“传输中加密”那建议给后端也配上TLS。别怕自签证书Nginx对上游TLS有一套完整的校验配置upstream backend_secure { server 10.0.0.5:8443; } location / { proxy_pass https://backend_secure; proxy_ssl_server_name on; proxy_ssl_verify on; proxy_ssl_trusted_certificate /etc/nginx/ssl/internal-ca.crt; }这里的关键是proxy_ssl_trusted_certificate指向内网CA的根证书后端使用该CA签发的证书反代就能完成双向信任。成本并不高但能把整条链路的“加密一致性”兜住。7.2 HSTS和重定向规则的双保险即使你修复了所有配置只要浏览器还允许用户用HTTP访问你的域名就存在被截获再跳转的风险。HTTPS降级攻击利用的正是这个缝隙。启用HSTS可以让浏览器记住“这个域名只能用HTTPS访问”add_header Strict-Transport-Security max-age31536000; includeSubDomains always;注意includeSubDomains是个双刃剑。加上它意味着所有子域名都必须支持HTTPS否则一个子域名没配证书整站都会被浏览器强制拦截。我习惯先只对主域名启用max-age先从300秒开始观察确认全站HTTPS无死角后再逐步拉长到31536000秒一年。同时在80端口强制跳转server { listen 80; server_name api.example.com; return 301 https://$host$request_uri; }这两层合起来等于告诉浏览器和所有中间设备这个站点没有HTTP入口。7.3 监控与告警如何发现“静默降级”比故障更可怕的是降级一直在发生但没人发现。因为很多降级并不导致页面挂掉只是“能用但不安全”这种状态可以维持几个月。分享一个我用过的小技巧在反代的访问日志里把原始协议和代理头信息也打印出来。log_format secure $remote_addr [$time_local] $request $status scheme$scheme xfproto$http_x_forwarded_proto upstream_addr$upstream_addr; access_log /var/log/nginx/secure_access.log secure;线上跑一段时间后查看日志里schemehttps但xfproto为空或为http的请求这些就是“入口加密、转发明文”的活样本。再配合一个每天扫描站点HTML中HTTP绝对链接的定时任务基本就能把静默降级消灭在低级别风险状态。另外JMeter这类压测工具录制HTTPS脚本时如果也是通过反代录制录下来的脚本里很容易混入原始HTTP请求事后回放就会因为协议不一致而出错。这类问题本质相同中间链路篡改了协议信息且没有在应用层把头传对。我在实际维护这套体系几年下来最大的感触是HTTPS的问题绝大多数不是“不会配”而是“只配了入口”。当你把目光从证书文件上移开顺着一次请求完整地走一遍浏览器到后端的链路时很多隐蔽问题自己就会暴露出来。反向代理本身不复杂复杂的是链路里那些默认又安静的转换。如果你现在就在被类似的HTTPS诡异问题困扰别急着怀疑证书商也别急着改后端代码先按这条链路逐跳验证一遍大概率能找到那个真正藏起来的“降级点”。
返回列表