Nginx反向代理:公网访问内网后端的安全架构实践

Nginx反向代理:公网访问内网后端的安全架构实践
最近在处理一套前后端分离项目上线时碰到了一个很典型的需求网站要能被公网访问但后端服务必须留在内网不能直接把端口映射到公网。最开始我也想着图省事直接把公网 IP 的某个端口映射到后端端口后来仔细一想这种做法等于把后端自己暴露在公网上安全风险太大。最后我用 Nginx 反向代理把整套流程跑通了公网 IP 只开放 Nginx 的 80/443 端口前端静态资源和 API 请求统一由 Nginx 接收再转发到内网后端后端监听地址始终不出内网。如果你正好也卡在这个场景里手头只有一个公网 IP前端要上 HTTPS后端想缩在内网防火墙后面这篇文章应该能帮你少走不少弯路。1. 先想清楚Nginx 做“前门”后端做“内院”1.1 这个场景为什么不能靠“端口映射”糊弄过去很多人的第一反应是把公网 IP 的某个端口映射到内网后端端口比如公网 8080 映射到内网 192.168.1.10:8080。这个做法确实简单但也等于把后端服务直接晾在了外网。端口扫描工具一扫发现 8080 端口开着接下来就会尝试各种框架漏洞、弱口令、未授权访问。后端的鉴权逻辑、限流策略再完善也不适合直接暴露在公网流量下毕竟它默认是给内网调用方用的。还有更麻烦的如果前端和后端各开好几个端口公网安全组要放行一大批端口排查问题的时候也搞不清哪里是入口。用 Nginx 反向代理之后公网只开放 80 和 443Nginx 作为统一入口接收用户请求后按规则转发给内网后端。对用户来说他们的所有请求都只发给 Nginx后端真实地址完全隐藏在 Nginx 后面这才是“前端映射到公网后端不映射公网”的正确姿势。1.2 反向代理比“纯端口转发”多守住了哪些东西我在这套方案里体会到几个很实在的收益都是生产环境里能直接感知到的。第一公网入口收敛到一两个端口以后防火墙和安全组的规则变得非常简单不会再出现“为了某个小接口又临时放行一个端口”的情况。第二Nginx 在转发之前可以先做一层过滤像扫描后台路径、请求异常文件、带明显恶意 UA 的访问都能被挡在到达后端之前。第三如果某个管理后台只允许固定 IP 访问我可以在 Nginx 层直接加 allow/deny后端完全不用改代码。第四HTTPS 证书只需要在 Nginx 上挂一份后端继续走内网 HTTP省去了给每个后端服务维护证书的麻烦。第五将来后端要做横向扩容比如从一台变成两台我只要在 Nginx 里加一个 upstream 配置公网入口不动后端机器也不用对外网暴露任何东西。2. 部署前的网络规划先把后端藏好2.1 一张简单拓扑看懂公网和内网的边界画个最简拓扑大家感受一下请求链路公网用户 ↓ 公网 IPx.x.x.xNginx 监听 80/443 ↓ 内网服务192.168.1.10:8080后端 API ↓ 前端静态文件可以放在 Nginx 本机也可以放在内网另一台机器这里的关键是公网防火墙只放行 80/443其它到 Nginx 所在机器的新入连接一律拒绝。后端那台机器就更严格了它对公网不可见防火墙只允许内网网段访问 8080 端口并且只允许内网里 Nginx 所在机器的 IP 访问。这样就算公网入口被扫描所有流量最终也只会落在 Nginx 这台机器上后端始终藏在“内院”里。2.2 后端监听地址别图省事绑定 0.0.0.0有多少后端服务上线时是草率绑定 0.0.0.0 的我知道很常见但这里建议改掉这个习惯。如果 Nginx 和后端部署在同一台机器上后端监听 127.0.0.1:8080 就够了外部网卡根本不会开这个端口安全级别最高。比如 Spring Boot 可以在 application.yml 里写server.address127.0.0.1Node.js 启动时要用 host 参数绑定 127.0.0.1。如果 Nginx 和后端不在同一台机器后端监听内网私有 IP比如192.168.1.10:8080然后配合防火墙限制来源。以 Ubuntu 上的 ufw 为例# 只允许内网 Nginx 机器访问后端端口 sudo ufw allow from 192.168.1.10 to any port 8080 proto tcp # 其余来源全部拒绝 sudo ufw deny 8080/tcp如果是云服务器还可以在安全组规则里只放行 Nginx 机器所在网段访问 8080 端口。这个过程不要偷懒后端一旦绑定 0.0.0.0等于告诉内网所有机器“你们可以直接来连我”如果内网有其他机器被攻破后端会很快被横向攻击。3. Nginx 核心配置实操前端和 API 共用一个入口3.1 安装和目录规划Ubuntu 上安装 Nginx 最简单sudo apt update sudo apt install nginx -y sudo systemctl enable --now nginxCentOS/RHEL 系列用 yum/dnf 装也差不多。装好后我不建议直接去改/etc/nginx/nginx.conf的主配置更推荐在/etc/nginx/conf.d/或者/etc/nginx/sites-available/下单独建一个站点配置文件然后软链到 sites-enabled或者直接把 conf 文件放 conf.d 下。这样以后每个站点一个文件改哪里、删哪里都清楚也不容易全乱套。3.2 最小可用的 server 配置假设前端是一个用 Vue 或 React 打包后的静态目录/data/www/frontend后端 API 地址是内网192.168.1.10:8080那么一个最基础的 Nginx 配置长这样server { listen 80; server_name www.example.com; root /data/www/frontend; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://192.168.1.10:8080; 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; } }这里最关键的是location /api/这个块。它负责把以/api/开头的请求转发到内网后端。比如用户访问https://www.example.com/api/userNginx 会把请求原样代理到http://192.168.1.10:8080/api/user然后后端返回的数据再原路回到用户浏览器。前端静态资源由 Nginx 直接读取磁盘返回不经过后端所以后端压力小很多。3.3 proxy_pass 带不带斜杠的“细节魔鬼”关于proxy_pass有一个非常容易踩的坑URL 后面到底带不带斜杠。proxy_pass http://192.168.1.10:8080;和proxy_pass http://192.168.1.10:8080/;虽然只差一个斜杠但转发逻辑完全不同。我把差异列出来proxy_pass 写法请求/api/user时转发到后端的路径http://192.168.1.10:8080;/api/user保留 location 前缀http://192.168.1.10:8080/;/user去掉 location 前缀这个细节取决于后端接口的路由设计。如果后端本身就有/api前缀比如 Spring Boot 设置了 context-path那么代理配置用不带斜杠的写法最省事。如果后端接口是/user、/order这种没有/api前缀但前端统一走/api入口那就得用带斜杠的proxy_pass http://192.168.1.10:8080/;把前缀在 Nginx 层剥掉。否则后端收到一个自己不认识的/api/user路径直接返回 404。这条经验我建议所有前后端联调的同学都记住。3.4 WebSocket 场景的几行配置现在很多项目不只是 HTTP 接口还有 WebSocket比如在线聊天、消息推送、实时协同编辑。Nginx 默认不会处理 WebSocket 的握手升级头部如果不加配置前端会一直连接不上。在 location 里加上这几行location /ws/ { proxy_pass http://192.168.1.10:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; }这里proxy_http_version 1.1是必须的因为 WebSocket 长连接依赖 HTTP/1.1 协议Upgrade和Connection是握手时需要的特殊头proxy_read_timeout可以适当调长不然默认 60 秒没有数据交换连接就会被掐断。但要注意这个长超时只用在 WebSocket 的 location 里不要全局乱套否则正常接口的闲置连接占着不放容易拖垮 Nginx。4. 上 HTTPS公网侧加密内网后端不用动4.1 Certbot 一键签发和自动续期如果只是用 HTTP 访问配置到这里其实已经能跑了。但正规上线一定要上 HTTPS尤其现在浏览器对 HTTP 站点的提醒越来越重用户信任度也会受影响。免费证书首选 Lets Encrypt配合 Certbot 自动配置 Nginx 非常方便。在 Ubuntu 上sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d www.example.com前提是你的域名已经解析到这台机器的公网 IP而且 80 端口暂时能被公网访问因为签发过程中 Lets Encrypt 会做一次域名验证。Certbot 会自动帮你改 Nginx 配置生成证书路径还会顺手把 80 跳 443 的规则写上。签发成功后要确认自动续期定时任务是否在跑。多数发行版装了 Certbot 后会有 systemd timer可以用systemctl status certbot.timer检查也可以手动执行一次sudo certbot renew --dry-run验证续期链路是否正常。4.2 HTTPS 落地后内网后端要注意什么HTTPS 只负责公网到 Nginx 这一段加密Nginx 到内网后端通常还是 HTTP这个设计没问题因为内网链路相对可信。但有一个点要理解X-Forwarded-Proto头已经从 Nginx 传过去了后端在生成跳转链接、判断请求是否来自 HTTPS 时应该优先读这个头的值而不是直接判断当前协议。很多后端框架默认就会认这个头不需要额外处理如果你自己写中间件记得不要把它丢了。另外证书续期后 Nginx 会自动 reload但如果有用 Docker 部署 Nginx记得把证书目录挂载到容器里并确保容器内能读到新证书否则续期后容器里的 Nginx 还在用旧证书浏览器会出现证书过期提示。5. 常见故障与实战排查手册5.1 502 Bad Gateway先按这四步走Nginx 配好之后最常见的问题就是刷新页面直接 502。502 的本质是 Nginx 作为代理没法从上游后端拿到有效响应。排查思路我总结成四步先看后端进程是否真的在监听端口ss -lntp | grep 8080再检查 Nginx 所在机器能不能访问后端端口curl http://192.168.1.10:8080/health如果 curl 不通接着查防火墙规则和安全组。有时候不是没放行而是 ufw/iptables 规则顺序不对导致内网请求也被拒绝。还要留意 CentOS 上的 SELinux它可能默认禁止 Nginx 发起网络连接需要执行sudo setsebool -P httpd_can_network_connect 1这对一堆卡在“Nginx 没报错但就是 502”的案例非常管用。5.2 504 Gateway Timeout延长读超时504 比 502 要“温柔”一些表示 Nginx 已经连上了后端但后端在超时时间内没有返回完整响应。如果你的接口里有批量导出、报表生成这类耗时操作很可能接口跑到一半就被 Nginx 掐掉了。可以在 server 或 location 里调大时间限制proxy_connect_timeout 5s; proxy_read_timeout 120s; proxy_send_timeout 120s;proxy_connect_timeout一般不要设太长如果后端连不上快速失败比较合理真正需要调的是proxy_read_timeout它会控制 Nginx 等后端响应体的时间。调大之后注意代理连接占用的时间会变长如果并发高要结合 worker 配置和系统文件句柄一起看。5.3 前端页面打开了但接口提示跨域当你把前端和 API 都放到同一个 Nginx 域名下之后跨域问题理论上不应该存在。因为浏览器里页面地址和接口地址是同一个域名和端口。如果还是出现跨域报错先看看页面实际请求的 API 地址是不是走了另一个域名或者 IP比如页面是https://www.example.com但前端代码里写死了http://后端IP:端口这就会导致跨域。正确的修法是改前端代码让请求地址只写相对路径/api/...走 Nginx 同源转发。如果某些场景确实需要跨子域可以在 Nginx 里加add_header Access-Control-Allow-Origin https://another.example.com always; add_header Access-Control-Allow-Methods GET, POST, OPTIONS always; add_header Access-Control-Allow-Headers Authorization, Content-Type always;但要注意 Nginx 的add_header有继承规则同一层级出现多个add_header只有最后一个会生效。如果你在 location 里同时设置了多个响应头记得都写在一个块里不要分散写否则很容易出现“某几个 header 不生效”的怪现象。5.4 用 curl 和日志快速定位问题边界排错的时候我习惯先用 curl 把问题边界分清楚。直接测 Nginx 入口curl -I https://www.example.com/api/user再绕过 Nginx在 Nginx 所在机器上直测内网后端curl http://192.168.1.10:8080/api/user对比两个返回结果就能判断问题是出在 Nginx 配置、网络链路还是后端本身。如果直测后端正常但通过 Nginx 访问异常重点看 Nginx 的 error.log。实时跟踪日志很有用tail -f /var/log/nginx/access.log tail -f /var/log/nginx/error.logerror.log 里出现upstream timed out、connect() failed这类关键词基本就能锁死在代理连接环节。日志默认路径可能因为发行版不同有差异Debian/Ubuntu 通常是/var/log/nginx/CentOS 也是这个位置。平时我建议把 error_log 的级别设置为 warn避免 info 级别刷太多无关信息真要排查的时候再临时改成 debug。另外一个小细节改完配置文件后别急着 reload先跑一下nginx -t检查语法再systemctl reload nginx。很多手滑改坏的配置都是因为在 reload 之后才发现格式错误线上服务已经受到影响了。最后分享几条我个人总结的小习惯。每台机器上的站点配置尽量单独放文件不要堆在主配置里后端监听地址永远不要图省事绑 0.0.0.0这是底线每次上线前用nginx -t验证配置再用curl测试关键接口最后看一眼 error.log。这套 Nginx 反向代理方案我用过很多次稳定性和安全性都很有保证后面就算要加域名、加后端实例也基本不用动公网入口的架构。

最新新闻

日新闻

周新闻

月新闻