nginx两次代理ssl时的坑

最近遇到一个让我排查了好一阵子的问题。

问题描述:

我是用dokku来部署的,因此nginx 生成了应用的nginx.conf,这个应用工作于7000端口。
我自己手工写了一个配置文件,监听443端口来服务https请求,这个服务就是定制了一些转发,期中”/“路径是转发给7000端口的。
后端应用是rails写的,有大量的redirect请求的场景。
完整的路径是:

1
用户 => nginx 443 => nginx(7000)=> rails

nginx 443转发到7000端口的时候,是用的proxy_pass http://localhost:7000/ 这个设定,从https转到了http转发。
现在问题来了,当有转发的时候,浏览器页面是https://a.example.com/,收到的是location: http://a.example.com/somepage这个。这时候浏览器拒绝响应了,不执行这个来自服务端的重定向。可能是一个安全策略,https页面不允许重定向到http页面。
于是我把rails的 force_ssl 设置为true了。结果问题更严重了,所有请求都报错,说重定向次数太多。
排查最终发现问题出在Nginx配置的这一行:

1
proxy_set_header X-Forwarded-Proto $scheme;

nginx 443的host收到用户的https请求,将协议改成http转发给7000,同时加上了X-Forwarded-Proto: https 这个头。
nginx 7000端口的host收到的是一个http请求,带有一个X-Forwarded-Proto: https的头,但是整个请求是http的,不是https的。这个时$schemehttp,nginx 这时发给rails 的是 X-Forwarded-Proto: http这个头。
于是rails的强制转https的部分工作了,给客户端下发了一个Location: https://${host}/${path} 的响应。
但是请求本身已经是https了啊,于是就不断地循环这个过程了。

解决方案:

将第二层也就是nginx监听7000端口的配置,从proxy_set_header X-Forwarded-Proto $scheme; 直接改成
proxy_set_header X-Forwarded-Proto "https";。方案粗暴了一些, 但是好像也没有好的办法。
但是,dokku应用的nginx配置文件,在每次deploy之后都会重新生成,因此我们要找个稳妥的方案,保证生成的nginx配置就写的是https而不是$scheme 这个变量。

dokku的nginx模板,其实都是来自于/var/lib/dokku/plugins/enabled/nginx-vhosts/templates/nginx.conf.sigil 这个文件。
打开这个文件,找到

1
{{ else if eq $scheme "https"}}

这后面,将这一行

1
proxy_set_header X-Forwarded-Proto {{ $.PROXY_X_FORWARDED_PROTO }};

强制改一下,写死https。

1
proxy_set_header X-Forwarded-Proto 'https';

nginx两次代理ssl时的坑
https://404.ms/2024/03/27/ssl-proxy-bug/
作者
rocky.xander
发布于
2024年3月28日
许可协议