跟随一个请求,从火车上的手机一直到只存在于私有网络内部的后端。手机解析公开域名,连接到 443 端口上的公开地址,并完成一次普通的 TLS 握手。Nginx 接受这条连接,然后向一个只能经 VPN 接口到达的地址另开一条完全独立的连接。手机侧没有任何变化,也永远不会知道中间有一层隧道。

跟随一个请求穿过两条连接

入站这一段是公开的 HTTP 事务。它有公开域名、公开证书,以及一条必须对访客当时的任意地址都能工作的回程路径。任何扰动这条路径的改动都会一次性把所有访客弄坏。正因如此,公开这一段才是最不该拿来做实验的地方。

出站这一段是私有的。Nginx 变成客户端,解析内部域名或使用固定地址,去访问一个会拒绝来自互联网连接的服务。

Public client  -> frontend public address:443   (TLS, public name)
Frontend       -> VPN private backend 10.40.0.20:8443  (TLS, private name)

在脑子里把两者分开,配置就变得显而易见。把它们混在一起,你最终会试图让公开客户端参与一条隧道,而这个设计的大部分混乱正来源于此。私有这一段可以脆弱。公开那一段不行。

动手之前先确认:把应用发布出去是有意为之。能用的代理同时也是一次能用的暴露,认证模型必须是一个决定,而不是副作用。

网关域名必须穿过隧道存活

域名解析里藏着一个不易察觉的陷阱。如果私有 DNS 覆盖了 VPN 配置文件为自身网关使用的同一个域名,重连时就可能试图穿过它本该建立的那条隧道去访问该网关。客户端需要这个域名在隧道存在之前就能解析。一旦它只在隧道内解析,连接从一开始就建不起来。

为网关和后端使用不同的、已批准的域名,并约束解析器策略,让每个域名都解析到其使用者需要的地址。改应用的 Host 头对此毫无帮助,因为做查询的是 VPN 客户端。

接着考虑范围。前端需要一条通往后端地址的路由,同时还要继续应答公开客户端。在该主机上开全隧道可能满足前者却破坏后者。

在策略允许的地方,定向隧道是更干净的设计。在策略强制要求全隧道的地方,前端应当放进独立的网络上下文,而不是为已知访客临时打补丁加路由。

把 server 块写成两段都做校验

假设后端为这个域名出示证书: backend.corp.example 并在私有地址上应答。在连地址的同时校验域名,正是 proxy 模块的 SNI 与域名选项存在的意义。

upstream private_app {
    server 10.40.0.20:8443;
}

server {
    listen 443 ssl;
    server_name app.example.com;

    ssl_certificate     /etc/nginx/tls/app-fullchain.pem;
    ssl_certificate_key /etc/nginx/tls/app-key.pem;

    location / {
        proxy_pass https://private_app;
        proxy_ssl_server_name on;
        proxy_ssl_name backend.corp.example;
        proxy_ssl_trusted_certificate /etc/nginx/trust/backend-ca.pem;
        proxy_ssl_verify on;
        proxy_ssl_verify_depth 3;

        proxy_set_header Host app.example.com;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $remote_addr;
    }
}

上面每个路径和地址都是占位符。真正重要的是让校验保持开启的那几项:要校验的域名、用来校验的信任包,以及与被批准链匹配的深度。

proxy_set_header Host app.example.com 是有意为之。后端应当配置为接受这个公开来源,并且只信任来自这个前端的转发头。把 X-Forwarded-For 覆盖成客户端地址,对单级代理是对的,对链条是错的,所以多级代理部署需要自己的一套可信地址安排。

让公开来源表现得像个公开来源

应用应当为公开来源输出绝对 URL、跳转和 Cookie。如果它把访客跳到 backend.corp.example,那个访客就被送到一个自己解析不了的域名上。

要通过应用支持的外部 URL 设置和经过评审的跳转策略来修,而不是去改响应体里的主机名。对任意内容做字符串替换会破坏已签名的负载,还会制造难以追查的 bug。

Cookie 也值得同样对待。绑定在私有域名上的 Cookie,只认识公开域名的浏览器永远不会发送。

还要确认 Nginx 真的能占用公开套接字。443 上已有的 Web 服务必须挪走,或者前端必须放到别处,因为两个进程无法绑定同一地址和端口。

在每一段真正运行的地方检查它

任何 reload 之前都用已安装的解析器校验配置,别让一个笔误弄掉正在服务的监听者。

然后从前端主机本机、在 Nginx 使用的同一网络上下文里测试后端这一段。穿过隧道的 ping 只能证明可达,证明不了 TLS、授权或应用本身。

最后用正常的证书校验向真实主机名发一次全新的公开请求,并观察回程路径,而不只是响应体。

把两次观察和已部署的配置放在一起,并记录隧道不可用时会发生什么。前端应当返回受控的上游错误,而不是悄悄回落到一个没人记录过的后端。带清晰日志行的 502 是好结果。 来自非预期来源的缓存页面 则不是。

相关文章