摘要

  • TLS server_name 是客户端在 ClientHello 中主动提供的 DNS 名称,服务器可据此缩小证书、TLS 上下文或透传目的地的候选范围;它不是客户端凭证。
  • 服务端身份校验、HTTP Host/:authority、代理上游 SNI、ECH 内外层处理以及应用授权各有独立证据。字符串相同,不等于权威相同。

一个完全正确的选择如何造成错误授权

事故记录里的每一步都“成功”了:解析器读出 SNI;回调找到租户 A;证书指纹与租户配置一致;TLS 1.3 握手完成。真正缺失的事实只有一个——对端是谁。

SNI 的目的,是让共享一个 IP 地址的多台虚拟服务在发送证书之前知道客户端想接触哪个名称。任何能构造 ClientHello 的程序都能填入一个语法合法的名称。服务器选中与之对应的证书,只是在准备让客户端认证服务器;它没有反过来证明客户端拥有该域名,更没有证明客户端属于某个租户。

这是一种典型的权威跃迁。底层信号只负责候选选择,上层却把“被选中”改名为“已认证”,再把它交给权限系统。配置表越准确,错误反而越像真。

server_name 的能力很窄

RFC 6066 规定,客户端在 ClientHello 的 ServerNameList 中发送名称。目前定义的 host_name 是 ASCII 形式的 DNS 主机名,不允许直接填入 IPv4 或 IPv6 地址;同一名称类型也不能出现两次。

这些约束解决的是互操作问题,不是信任问题。SNI 不证明客户端查询过 DNS,不证明它控制域名,不证明它持有私钥,也不证明它获准访问同名业务。测试工具可以直接连接某个地址并发送另一个名称;扫描器可以枚举已知名称;攻击者当然也会选择最有价值的那个。

服务器认识扩展却不接受该名称时,可以发出致命 unrecognized_name。部署也可能按明确规则继续使用默认配置。因此审计不能只问“是否启用 SNI”,而要逐项验证缺失、未知、非法名称分别进入哪个分支,默认虚拟主机又获得了什么能力。

握手完整性不会产生名字所有权

在 TLS 1.3 中,ClientHello 进入握手转录,最终由 Finished 保护。正常握手完成后,中间人不能悄悄改掉 SNI 而不破坏端点看到的转录。

但这一保证只说明:服务端收到的确实是客户端在本次握手里提交的值。它不说明客户端有资格提交这个值。主张的完整性不是主张的权威。

如果部署没有客户端证书或其他客户端认证,服务端即使完成了一次完全合规的握手,也没有因此获知租户身份。服务端证书的认证方向恰恰相反——是客户端根据自己的参考身份去判断服务端。

字段命名应当保留这条界线。记录 client_requested_server_name 是准确的;写成 verified_tenant、owned_domain 或 authorized_origin,等于在日志层先伪造了授权结论。

选出证书不等于验证证书

SNI 常被用来选择证书,但选择动作发生在服务端,验证判断通常发生在客户端。两者不能合并。

RFC 9525 要求客户端独立构造可接受的参考身份,再与证书中的呈现标识匹配。证书链、有效期、撤销策略和服务名匹配都属于验证账本。服务端因为 SNI 拿出了正确候选证书,并不能证明客户端执行或通过了这些检查。

OpenSSL 的接口把区别写得很清楚:客户端用 SSL_set_tlsext_host_name() 设置 SNI 时,还应另外设置证书校验所期待的 DNS 名称。只设置前者的程序,可能帮助服务器返回“看上去正确”的证书,却从未完成严格的名称验证。

多名称证书也不会改变边界。一张证书可以合法覆盖多个域名,因此两个名称都可能通过密码学校验;这不意味着两个租户合并,更不意味着任何一个请求获得另一个业务的权限。

HTTP 名称是另一笔账

TLS 终止后,HTTP 才提交请求目标。HTTP/1.1 主要使用 Host,HTTP/2 与 HTTP/3 常用 :authority。RFC 9110 把这项信息视为关键的应用路由输入,也明确提醒它经常成为攻击面。

普通客户端往往从同一个目标 URI 生成 SNI 与 HTTP authority,所以二者经常相等。经常相等不是协议证明。它们位于不同层、在不同时间被处理,也可能因为连接复用、代理映射、测试、错误配置或攻击而不同。

应用必须为不一致情况写出政策:严格虚拟主机可以拒绝;受控共享服务可以许可一组明确组合;HTTP/2 在连接不适合该 origin 时可以返回 421。最危险的做法,是默认让第一次握手的 SNI 为连接上后续所有请求授权。

即使 HTTP authority 与 SNI 相同,它仍然不是客户端身份。正确做法是先让两种路由信息各归其位,再用客户端证书、令牌、会话或其他应用凭证确定主体。

代理两侧没有一条继承的 SNI

TLS 终止代理结束下游握手,然后作为客户端建立另一条上游 TLS 连接。两侧各有 ClientHello、SNI、证书、参考身份、转录和失败状态。

上游 SNI 可以写死,可以从上游主机名派生,也可以来自下游 HTTP authority 或受控映射。Envoy 把这些来源做成不同选项,并把按上游 SNI 验 SAN 与按请求 authority 验 SAN 区分开来。选项之所以分开,正是因为路由输入与验证输入可能不同。

透传又是另一种证据。NGINX 的 ssl_preread 能在不终止 TLS 的情况下读取 ClientHello SNI 并选择上游。此时中间设备只能证明“解析了提示并选择了路线”,不能声称握手完成,更不能声称客户端接受了最终服务端证书。

只存一项 sni=example 的观测模型会丢失连接腿、来源和验证者。审计至少需要下游连接 ID、上游连接 ID、名称来源、所选上下文、校验策略与各自结果。

ECH 让“我看见了哪个名称”也成为证据问题

Encrypted Client Hello 把敏感参数放入 ClientHelloInner。外层 ClientHelloOuter 通常携带 ECH 的公共名称,用于把流量送到面向客户端的服务以及完成重试机制。

ECH 被接受后,TLS 参数处理基于经过认证的内层 ClientHello。ECH 被拒绝时,客户端可能只为公共名称认证一条连接,以取得重试信息。RFC 9849 明确规定:公共名称连接的认证不是 origin 认证,TLS 实现不能把它作为成功连接交给应用。

因此 outer SNI 是最直观的受限信号:它能正确完成前端路由,却被规范明确禁止冒充真实 origin。若监控只写“SNI”,便会混合普通明文名称、公共外层名称与获接受的内层名称。

ECH 感知的日志要保留处理状态、观察角色和名称来源,同时限制真实内层名称的扩散。为了可观测性重新建立一份广泛可读的明文目的地档案,会直接抵消 ECH 的隐私目的。

运行证据比回调名称更重要

OpenSSL 的早期 ClientHello 回调先执行,随后是 server-name 回调,再后是 ALPN 选择;server-name 回调还能切换 SSL_CTX。GnuTLS 也提供客户端设置和服务端读取名称的接口。

API 只能证明软件具备能力。它不能证明本次连接调用了哪个分支、输入如何规范化、选择了哪个上下文、是否落入默认项,或结果是否被上层覆盖。TLS 1.3 与旧版本恢复会话的名称处理时机也可能不同。

升级 TLS 库、代理或证书映射后,应当让恶意不一致测试走过真实运行路径。记录输入类别、选择原因、配置标识与终止状态,才能把声明的规则变成可复核证据。

用拒绝测试证明边界

第一项测试不需要复杂攻击:不给任何客户端凭证,却发送最高权限租户的 SNI。TLS 可以完成,正确证书也可以出现,但应用主体必须仍是匿名,管理动作必须被拒绝。

随后分别删除 SNI、发送未知名称、触发默认虚拟主机,再制造 SNI 与 HTTP authority 的跨租户组合。使用同时覆盖两个名称的证书复测,确保“证书也有效”不会掩盖授权缺陷。

在代理场景中,让上游 SNI 与下游不同,要求两侧选择与 SAN 校验分别出现在证据中。对透传路由,禁止生成“证书已验证”或“握手已完成”的标签。最后覆盖 ECH 成功、拒绝重试和无 ECH 三条路径,证明系统知道自己处理的是内层还是外层。

配置截图不是测试结果。只有运行中的错误输入被正确拒绝,才说明权威没有越界。