第05章 HTTPS 与 TLS 代理:终止 / 透传 / MITM / SNI / mTLS
学习目标
- 掌握代理处理 TLS 的四种方式:透传、终止、重加密、MITM,及各自的信任前提
- 理解 MITM 为什么需要"客户端信任代理的根证书",以及 HSTS/证书锁定如何反制
- 搞懂 SNI 在选证书与路由中的双重角色,及 ECH 带来的变局
- 理解 mTLS(双向 TLS)及它在 Service Mesh 里的位置
- 用 nginx
ssl_preread(透传)和mitmproxy(MITM)亲手验证
前置知识
- 第04章 HTTP 代理协议(CONNECT 隧道)、第02章 SNI 嗅探
- 网络手册 · 应用层协议(TLS 握手)
原理
TLS 是加密的,这给代理出了道难题:想处理内容就得解密,解密就得有证书,有证书就涉及信任。 围绕"解不解密、谁信任谁",代理有四种玩法:
① 透传 Passthrough Client ══TLS══▶ [代理盲转密文] ══TLS══▶ Origin 代理看不到明文
② 终止 Termination Client ══TLS══▶ [代理解密] ──明文──▶ Origin 客户端的TLS到代理为止
③ 重加密 Re-encrypt Client ══TLS══▶ [代理解密再加密] ══TLS'══▶ Origin 两段独立TLS
④ 中间人 MITM Client ══TLS(假证书)══▶ [代理解密偷看] ══TLS══▶ Origin 代理假冒源站
① TLS 透传(Passthrough)——不解密,盲转
代理把 TLS 当不透明字节流,原样转发,全程不持有任何证书。这正是 第04章 的 CONNECT 隧道,以及 第02章 的 SNI 嗅探路由(偷看明文 SNI 选后端,但不解密)。
- 优点:端到端加密不被破坏,代理无需证书,性能高(可 splice)
- 局限:代理看不到 URL/Header,做不了 L7 的事(缓存、改写、按 path 路由)
- 用在:反向代理按域名分流 HTTPS 又不想解密、TCP 直通
② TLS 终止(Termination / Offload)——解密成明文
代理终止客户端的 TLS:客户端与代理之间是加密的,代理解密成明文后处理,再转发给后端。这是反向代理的标配。
Client ══TLS══▶ [Nginx 持有 example.com 证书+私钥,解密] ──HTTP 明文──▶ 后端
- 前提:代理必须持有该域名的真实证书 + 私钥(合法持有,因为代理就是服务端的门面)
- 好处:把 TLS 计算从后端卸载到代理、集中管理证书、能做全套 L7 处理
- 注意:代理→后端如果是明文,要确保这段在可信网络内(否则内网也该加密,见下)
- 终止后,代理用
X-Forwarded-Proto: https告诉后端"原始请求是 HTTPS"——否则后端以为是明文 http,可能生成错误的跳转/Cookie(第04章 转发头)
③ 重加密(Re-encryption)——两段独立 TLS
终止后到后端再开一条新 TLS。客户端↔代理一段 TLS,代理↔后端另一段 TLS,两段独立。
- 用在:零信任网络(内网也全程加密)、合规要求、后端只接受 HTTPS
- 代价:两次 TLS 计算;代理需同时校验后端证书
④ MITM(中间人)——代理假冒源站偷看明文
这是正向代理想看 HTTPS 明文时的唯一办法。代理动态假冒源站:为每个访问的域名现场签发一张伪造证书,骗客户端"我就是 example.com",从而解密。
Client ──CONNECT example.com:443──▶ [代理]
◀── 代理用自己的 CA 现签一张 example.com 证书,假装是源站 ──
Client ══TLS(信了假证书)══▶ [代理解密看明文] ══真TLS══▶ example.com
- 关键前提:客户端必须信任代理的 CA 根证书(手动安装到信任库)。否则客户端会报"证书不可信"——这正是 MITM 的安全边界:没有你的同意(装根证书),别人 MITM 不了你的 HTTPS。
- 用在:调试抓包(mitmproxy,第18章)、企业上网行为审计与 DLP、恶意场景
- 反制:
- HSTS:网站声明"只接受真证书",浏览器拒绝任何证书异常,MITM 失败
- 证书锁定(Certificate Pinning):App 内置源站证书指纹,伪造证书过不了,移动端 App 常用——这也是为什么很多 App 在开了代理抓包后连不上
- CT(Certificate Transparency):伪造证书未上 CT 日志会被发现
SNI 的双重角色
SNI(ClientHello 里的明文域名)干两件事:
- 让服务器选对证书:一个 IP 上托管多个 HTTPS 站点(SNI 之前做不到),靠 SNI 区分
- 让 L4 代理路由:不解密就能按域名分流(第02章 的 SNI 嗅探)
ECH(Encrypted Client Hello) 把 SNI 也加密了,意味着:基于 SNI 的路由和审计会失效,企业/运营商的"看域名"能力被削弱——这是当前 TLS 代理领域最大的变量。
mTLS(双向 TLS)——客户端也要出证书
普通 TLS 只有服务端出证书(客户端验证服务端)。mTLS 要求双方都出证书互相验证,实现"零信任"——只有持合法证书的服务才能互相通信。
- 代理的角色:作为 mTLS 的终止/发起方。Service Mesh 的精髓就在这:Envoy sidecar 之间自动 mTLS,业务代码无感知,平台层就拿到了"服务间双向认证 + 加密"。详见 第26章、第29章。
TLS 握手状态机与会话复用(TLS 代理的性能命门)
代理做 TLS 终止/MITM 时,性能大头是握手。理解它才能优化:
完整握手(开销最大,含非对称运算):
- TLS 1.2:2-RTT(ClientHello → ServerHello+证书 → 密钥交换 → Finished)
- TLS 1.3:1-RTT(合并若干步),更快
每次完整握手都要做 ECDHE 密钥交换 + 证书验证;MITM 代理还要为每个新 SNI 现场签发证书(一次 ECDSA/RSA 签名),CPU 成本可观——这是 MITM/终止型代理的主要开销来源。
会话复用——避免重复完整握手,是 TLS 代理吞吐的命门:
| 机制 | 做法 | 服务端状态 |
|---|---|---|
| Session ID(TLS1.2) | 服务端缓存会话,客户端带 ID 复用 | 有状态(缓存) |
| Session Ticket(TLS1.2, RFC 5077) | 会话加密成 ticket 交客户端保存 | 无状态 |
| PSK + 0-RTT(TLS1.3) | 用上次预共享密钥,甚至 0-RTT 直发数据 | 无状态 |
完整握手 vs 复用握手吞吐可差一个数量级,生产代理必开 ticket/PSK。但两个坑:① 0-RTT 有重放风险,只能用于幂等请求(呼应 第10章);② 多副本代理要共享 ticket key,否则负载均衡换一台机器就复用失败、退回完整握手(这也是 TLS 代理横向扩展的隐藏成本)。
四种方式对比
| 方式 | 代理需要证书吗 | 看得到明文 | 端到端加密 | 典型场景 |
|---|---|---|---|---|
| 透传 | 不需要 | ❌ | ✅ 完整 | 反向 SNI 分流、TCP 直通 |
| 终止 | 需要真证书 | ✅ | 仅客户端→代理 | 反向代理、TLS 卸载 |
| 重加密 | 真证书 + 验后端 | ✅ | 两段独立 | 零信任内网 |
| MITM | 需客户端信任其 CA | ✅ | ❌ 被打断 | 调试、审计、(恶意) |
️ 实现 / 命令
实验一:TLS 透传 + SNI 路由(nginx stream,不解密)
# nginx.conf 的 stream 块——工作在 L4,按 SNI 分流但不解密
stream {
map $ssl_preread_server_name $backend {
api.example.com 10.0.0.11:443;
web.example.com 10.0.0.12:443;
default 10.0.0.10:443;
}
server {
listen 443;
ssl_preread on; # 只读 ClientHello 的 SNI,不解密
proxy_pass $backend; # 整条 TLS 连接盲转给选中的后端
}
}
ssl_preread on 是关键——nginx 偷看 SNI 选后端,但没有任何证书配置,因为它不解密。后端才持有证书、做真正的 TLS 终止。
实验二:TLS 终止(nginx 解密成明文转后端)
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/certs/example.com.crt; # 代理持有真证书
ssl_certificate_key /etc/nginx/certs/example.com.key;
location / {
proxy_pass http://127.0.0.1:8080; # 明文转后端(TLS 卸载)
proxy_set_header X-Forwarded-Proto https; # 告诉后端原始是 HTTPS
}
}
实验三:MITM 抓 HTTPS 明文(mitmproxy)
# 1. 启动 mitmproxy(默认听 8080,首次运行会生成自己的 CA)
mitmproxy --mode regular --listen-port 8080 &
# 2. 不装 CA 直接抓 → 客户端报证书错误(这是 MITM 的安全边界!)
curl -x http://127.0.0.1:8080 https://example.com/
# curl: (60) SSL certificate problem: unable to get local issuer certificate
# 3. 信任 mitmproxy 的 CA 后,才能解密(演示用,生产慎重)
curl -x http://127.0.0.1:8080 \
--cacert ~/.mitmproxy/mitmproxy-ca-cert.pem \
https://example.com/
# 200 —— 此时 mitmproxy 界面里能看到完整的 HTTPS 明文请求与响应
第 2 步的失败恰恰是好事:它证明没有你主动安装根证书,HTTPS 就 MITM 不了。第 3 步装了 CA 才成功,对应企业为何要在员工机器上预装根证书才能审计。
实验四:看代理给你的是不是伪造证书
# 看服务器证书的签发者。正常是公共 CA;若是 mitmproxy/企业 CA,就是被 MITM 了
openssl s_client -connect example.com:443 -proxy 127.0.0.1:8080 </dev/null 2>/dev/null \
| openssl x509 -noout -issuer
# 正常: issuer=C=US, O=Let's Encrypt, CN=R3
# 被MITM:issuer=CN=mitmproxy CA ← 警报!
排错
| 现象 | 根因 | 解决 |
|---|---|---|
unable to get local issuer certificate | 代理在 MITM 但客户端没信任其 CA | 装 CA(调试);或这是攻击,别信 |
透传时 ssl_preread 拿不到 SNI | 客户端没发 SNI,或用了 ECH | 退回 IP/端口路由 |
TLS 终止后网站跳转成 http:// | 后端不知原始是 https | 配 X-Forwarded-Proto https + 后端识别它 |
| App 开代理就连不上(网页正常) | App 做了证书锁定,拒绝伪造证书 | 锁定无法绕过;用未锁定的测试构建 |
| HSTS 站点 MITM 失败 | 浏览器对 HSTS 域拒绝证书异常 | 设计如此,无法绕过 |
mTLS 握手失败 certificate required | 客户端没出证书或证书不被信任 | 检查客户端证书与代理的 CA 配置 |
| 终止后后端 TLS 校验失败 | 重加密段后端证书不被代理信任 | 配后端 CA 或(仅测试)跳过校验 |
本章小结
- 代理处理 TLS 有四招:透传(不解密、可 SNI 路由)、终止(解密卸载,反向代理标配)、重加密(两段 TLS,零信任)、MITM(假证书偷看明文)。
- MITM 的安全底线是客户端必须信任代理 CA;HSTS 和证书锁定能反制 MITM。
- SNI 既选证书又供 L4 路由;ECH 会让 SNI 路由/审计失效。
- mTLS 双向认证是 Service Mesh 零信任的核心,由 sidecar 代理自动完成。
下一章 第06章 SOCKS 协议,我们离开 HTTP 世界,逐字节拆解 SOCKS4/4a/5 与 UDP ASSOCIATE——第21章 手写的那个 SOCKS5,原理就在这里。