跳到正文
Nginx 反向代理:最容易错的不是端口,而是代理边界
Nginx 反向代理:最容易错的不是端口,而是代理边界

Nginx 反向代理:最容易错的不是端口,而是代理边界

从 proxy_pass 的 URI 替换、真实客户端地址、外层 HTTPS、容器网络到 nginx -t 与 reload,整理我实际踩过的反向代理边界。

速读#

反向代理的价值不只是把 :8000 换成域名,而是把公开入口、路径路由、TLS、安全头、限速和后端进程分开管理。真正容易出错的地方也不在 proxy_pass 后面的端口:末尾 URI 会改变上游收到的路径,转发头只有在代理链可信时才代表真实访客,Nginx 位于 Cloudflare Tunnel 后面时,$remote_addr$scheme 看到的还可能只是上一跳与本机 HTTP。

我现在改 Nginx 会做三类验证:nginx -t 检查语法与引用文件,回环 curl 检查主机名和路径怎样路由,外部请求再确认 Cloudflare、Tunnel、缓存和协议语义。reload 只是让新 worker 接受配置,不能证明上游可达、Header 正确和业务没有重定向循环。

正向代理与反向代理到底替谁工作#

正向代理代表客户端访问外部资源,可以用于企业出口、缓存、内容控制或隐私网络,不应只用“翻墙”概括;目标服务器通常看到的是代理,而不是原客户端。反向代理代表一组服务器接收入口流量,客户端只知道公开域名,由 Nginx、HAProxy 或其他入口决定请求交给哪个后端。

反向代理并非所有服务的强制要求。一个 API 完全可以自己正确终止 TLS、认证和限流后直接暴露;只是当同一主机有多个服务、需要统一域名与策略,或者希望后端只监听内网时,入口代理通常更容易维护。当前站点中,Nginx 只监听 127.0.0.1:80,Cloudflare Tunnel 把请求送到它;普通页面由 try_files 读取 Astro 静态产物,只有 /oauth/deploy/github 等路径才反代到本机 Node 服务。

最小配置成立需要明确前提#

在“Nginx 直接接收访客 HTTP/HTTPS、后端运行在同一宿主机”的简单场景里,配置可以是:

server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
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;
}
}

这里 $remote_addr 是直接连接 Nginx 的客户端,$scheme 也是 Nginx 当前接收的外层协议,所以这些值有明确意义。后端收到的 TCP 连接确实来自 Nginx,原客户端信息只能通过代理协议或 Header 显式传递;应用还必须配置“信任哪些代理”,不能无条件相信任何互联网客户端自己提交的 X-Forwarded-*

一旦前面还有 Cloudflare、负载均衡器或 Tunnel,这份模板就不能原样复制。$remote_addr 可能是边缘代理或本机 cloudflared$scheme 可能是 Tunnel 到 Nginx 的 http,而访客实际使用 https。应先限制源站只能由可信代理到达,再使用 Nginx 的 Real IP 模块或供应商提供的受信 Header 恢复客户端地址;外层协议也要由可信入口清理和传递,不能既接受任意客户端 Header,又拿它做重定向、Cookie 或安全判断。

当前站点的 Web 入口只监听回环并由 Tunnel 到达,因此 CF-Connecting-IP 可以用于限流键;如果未来重新开放公网直连,同名 Header 就可能被伪造,必须同步调整信任来源。代理头是否可信取决于网络边界,不取决于变量名看起来多正式。

proxy_pass 末尾斜杠改变的是 URI 映射#

最常见的例子是普通前缀 location:

location /api/ {
proxy_pass http://127.0.0.1:8000;
}

请求 /api/users?active=1 会把原 URI 交给上游,即上游看到 /api/users?active=1。如果改成:

location /api/ {
proxy_pass http://127.0.0.1:8000/;
}

proxy_pass 带了 URI /,Nginx 会把匹配的 /api/ 前缀替换为 /,上游看到 /users?active=1。这不是“带斜杠总会删除路径”的通用口诀,而是 proxy_pass URI 替换规则在普通前缀 location 中的结果;正则 location、命名 location、变量和 rewrite 会带来额外规则,必须按当前配置验证。

我不再用“简单场景永远不带斜杠”代替理解。更稳的做法是先写出后端期望的 URI,再选择保留前缀还是替换前缀,并准备一个能回显请求路径的测试后端:

Terminal window
curl -i -H 'Host: app.example.com' \
'http://127.0.0.1/api/users?active=1'

同时看 Nginx access log 与后端日志,确认路径、查询参数和编码结果。一个字符改变语义的地方不适合靠记忆,但非常适合用可重复请求固定行为。

127.0.0.1 只属于当前网络命名空间#

宿主机上的 Nginx 可以访问宿主机 127.0.0.1:8000;如果后端容器把端口发布成 127.0.0.1:8000:8000,宿主机 Nginx 也能通过这个回环映射访问。可是一旦 Nginx 自己也在容器里,它的 127.0.0.1 只指向 Nginx 容器,不是宿主机,更不是另一个应用容器。

同一 Compose 项目中的容器通常应加入专用网络,并通过服务名访问:

proxy_pass http://app:8000;

应用不需要发布公网 portsexpose 可以记录容器端口,却不负责访问控制,网络成员与应用认证才是边界。还要理解 Nginx 对 DNS 解析的时机:后端容器 IP 会变化时,静态启动时解析可能无法自动跟随,生产配置需要使用适合当前 Nginx 版本的 resolver、动态 upstream 或稳定的服务发现,而不是重建容器后靠运气继续连接旧地址。

Header 要按用途和信任链配置#

Host 影响虚拟主机、绝对 URL 和多租户路由;客户端地址影响日志、审计和限流;协议影响重定向、Secure Cookie 和应用生成的链接。它们不是“常用四行模板”,而是入口与应用之间的契约。前面每增加一跳,都要问这一跳会保留、覆盖还是追加什么,后端又信任到第几层。

Cloudflare HTTPS 到 Tunnel HTTP 的 WordPress 重定向循环就是典型例子。如果 Nginx用 $scheme 覆盖 X-Forwarded-Proto,它传下去的是内层 http,后端便不知道访客已经在外层使用 HTTPS;反过来,如果源站公开可达却无条件信任客户端提交的 X-Forwarded-Proto: https,攻击者又能伪造安全上下文。正确修复是先收紧入口、确定可信代理,再逐跳传递外层协议,并让应用只信任这条已知代理链。

WebSocket、gRPC、大文件上传和流式响应还需要各自的 upgrade、grpc_pass、buffering、超时与 body size 配置。没有这些需求时不提前抄一套“万能 Nginx 模板”,等真实协议出现再增加,配置更短也更容易审计。

nginx -t && reload 只是第一道门#

Terminal window
sudo nginx -t && sudo systemctl reload nginx

nginx -t 会解析配置并检查引用文件是否可读,失败时不会执行后面的 reload;它不能连接每个上游、发送真实请求,也不会发现错误的 URI 映射和重定向逻辑。Reload 通常让旧 worker 继续处理已有连接、新 worker 使用新配置,但新请求仍可能立即遇到 502、404 或安全头错误,所以命令成功后必须继续检查状态和日志。

我现在使用的最小发布顺序是:保留当前管理会话,备份或用 Git 记录配置,执行 nginx -t,reload 后查看 systemctl status nginx 与 error log,再从本机带正确 Host 请求静态和动态路径,最后从外部检查 HTTPS、缓存、重定向和认证边界。高风险变更先放到测试主机名或独立端口,不在唯一生产 443 上边猜边改。

Nginx 真正教会我的不是一份 server block,而是入口配置必须同时理解路径、网络命名空间和信任链。端口写对只解决“能不能连”,请求到了以后走哪里、携带什么身份语义、出错如何回退,才决定这层代理是否可靠。

版权许可

CC BY-NC-SA 4.0 本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

相关文章

s1oopX

登录 s1oopX