在 NGINX 的 stream 上下文中放行 TLS 直通连接
NGINX 可以在不终止 TLS 的情况下拒绝 TCP TLS 中继。把放行规则放在 stream 中,而不是 HTTP location 中,并确认到达的是哪个对端地址。
在不持有证书的前提下转发加密字节,正是 TLS 直通中继所做的事,这种安排既方便又略有迷惑性。由于没有任何解密,也就不存在 HTTP 形态的内容可供过滤,因此常规的 location 块及其访问规则在这里毫无用武之地。NGINX 能做的决定针对连接本身:哪个网络对端敲了门,以及是否应当放行该对端。
决定属于 stream,而不是 HTTP
在 stream 上下文中,NGINX 处理的是 TCP 会话。它能看到的连接的对端地址,并可以在任何字节到达后端之前,对其施加有序的 allow 与 deny 规则。这是一个完整的准入决定,不需要私钥、证书,也不需要解析 HTTP。
| Mechanism | What it decides | Where it lives |
|---|---|---|
| stream allow/deny | Whether this TCP peer is relayed | stream server block |
| ssl_preread map | Which backend a named connection goes to | stream, with the preread module |
| HTTP location rule | Whether an HTTP request is served | http server block |
这三行永远不会互相替代。把 HTTP 访问指令复制到任何提到 443 端口的文件里都不会有任何改变,因为 stream 服务器从不读取 http 上下文。
先准入连接,再转发
假设已有安装且 stream 与 stream-access 模块可用,VPN 客户端把源地址保持在 10.90.0.0/24 内,而 192.168.50.20:443 上的后端已经提供目标 HTTPS 身份。
stream {
server {
listen 443;
allow 10.90.0.0/24;
deny all;
proxy_pass 192.168.50.20:443;
}
}
请把它当作新 listener 的片段,而不是对现有可用配置的直接替换。先检查已有的 listener 与 include,因为在一个已被占用的端口上再加一个 listener,会在 reload 时失败,而不是在请求时失败。
注意缺了什么:没有 listen ... ssl,也没有证书指令。NGINX 转发加密流,后端终止 TLS 并出示客户端所校验的证书。未通过 allowlist 的客户端会被拒绝 TCP 会话,而不是收到 HTTP 403 页面,任何期待状态码的工具都会报出毫无用处的结果。
allowlist 覆盖什么、不覆盖什么
如果中继已经按服务器名选择后端,那么在模块可用的前提下,ssl_preread 与 map 可以继续承担这项工作。名字仍然由客户端提供,而仅仅出示一个名字并不能证明客户端加入了 VPN。
示例规则覆盖整个 stream listener。这使它不适合同时服务公开站点与仅限 VPN 站点的单一入口,那种场景需要按名字设置例外;分离的 listener 通常比无人能读懂的名字相关策略更清晰。
还有两个缺口值得点明。前缀匹配无法说明数据包从哪个接口进来,而别处的 HTTP 规则也无法约束这个 listener。这两点都说明应当把主机层面的 ingress 策略一并纳入视野,而不是把一行 allow 当作全部边界。把边界写成两个必须一致的控制项,这样改动其中任何一个都会进入评审。
NGINX 实际看到的是谁的地址?
负载均衡器、另一层代理或源 NAT 都可能在 NGINX 前面插入中间设备,因此它看到的对端并不是 VPN 客户端的内部地址。在选择前缀之前先检查真实路径,因为按错误地址写出的 allowlist 要么放行转换设备后面的所有人,要么谁都不放行。
PROXY 协议与真实 IP 处理 是正当的答案,但只能针对兼容且被明确信任的发送方进行配置。两种机制都相信发送方所说的话,这正是发送方名单必须很短的原因。信任来自任意互联网客户端的身份头并不能让源信息重新可信,只会把正确的拒绝变成伪造的放行。
reload 之前把两种结果都测一遍
校验配置、reload,然后测两次:一次是必须成功的新放行会话,一次是必须失败的被拒会话。在放行测试中保留常规的后端证书校验,因为能连上却无法校验后端的中继,等于修好了一个问题又制造了另一个。
nginx -t
ss -tnp | grep ':443'
还要分别检查到后端的路由与响应。通过 allowlist 只能证明 NGINX 接受了会话,它无法让不可达的后端作出响应,也无法修复在你开始之前就缺失的返回路径。