密码学 18:mTLS 与 TLS 终止后,客户端、代理和后端分别信谁
客户端通过 HTTPS 把合成订单发给服务,服务器不仅给出证书,还要求客户端也出示证书,握手成功,于是配置里写了“已启用 mTLS”。如果 TLS 实际终止在反向代理,而代理使用 HTTP 明文把订单送到后端,哪一段受 mTLS 保护?后端的业务程序究竟在信任客户端、代理,还是某个随请求转来的 X-Client-Name 字符串?把“开了 mTLS”写成“整个链路端到端机密、用户订单授权正确”并不严谨。
本篇用回环地址的三个真实组件验证第一层事实:客户端与代理做 TLS 1.3 双向证书认证,代理收到已验证客户端证书,后端却是另起的一条明文 HTTP 连接。没有安装服务网格,后端的订单也没有做真实权限检查;实验只支持连接段和证书验证对象的判断。
三个端点,两段连接
假设客户端拿着一张由测试 CA 签发、扩展用途为 clientAuth 的合成证书;代理有另外一张 serverAuth 的 orders.test 服务器证书。客户端的 TLS 验证器把测试 CA 明确加载到本次上下文并检查服务器名;代理单独加载这张测试 CA 并要求客户端证书。签发者的 CA 私钥只用于发两张短期测试证书,不在两端的运行握手里充当通用流量密钥。
1 | |
客户端验证代理“它是我希望连接的服务吗”;代理验证客户端“这个连接的对端确实持有某张受信客户端证书的私钥吗”。后端看到的是代理作为自己的 TCP/HTTP 对端,不是同一条客户端 TLS 会话的对端。代理可以选择把经过验证的身份映射成可信元数据并传给后端,但怎样保证元数据不能被伪造、怎样把证书主体变成业务账号权限、后端是否只接受受信代理转发,都是需要额外设计和验收的系统边界。
这与 14 的服务端单向证书验证、15 的服务器 CertificateVerify、16 的记录保护一脉相承:mTLS 是双方都走一套证书身份校验,并没有抹掉代理终止点。它不是“后端可以看到客户端私钥”,也不是“客户端证书保证这笔订单已付款”。
每一种缺失都对应一个不同攻击
如果客户端不验证代理服务名,拥有另一张受信 CA 签发证书的恶意端点可能冒充目标服务;仅有“服务器出示了某张证书”不足以匹配 orders.test。如果代理在需要客户端认证时没有强制要求客户端证书,未经认证的连接也可能向内部后端提交相同格式的请求。即使双向 TLS 握手成功,若代理到后端的明文链路可被某个对手观察或改动,前一段的加密与认证标签不会保护这一新段;若代理把由客户端自填的 HTTP 用户头原样交给后端并让后端相信,又引出身份断言从何而来的问题。
如果后端只绑定本机回环地址,实验里的明文连接并不是公网可直接嗅探的可用攻击路径;现实风险要结合实际部署是否跨主机、共享网络、容器网络、被入侵的代理或后端等边界分析。这里的反例是“保护没有自动跨过 TLS 终止点”,不是编造一个公网拦截事件。
对钱包走 HTTPS RPC 的路径同样要画两段:钱包验证与之建 TLS 的 RPC 服务,而节点是否验证交易格式与账户授权还要看交易签名和链状态。攻击者即使能控制代理后段回复,也不能仅凭 HTTPS 端点的客户端证书伪造钱包签名;另一方面,交易签名有效也不代表 RPC 传输没被代理窥视或响应没被替换。两层必须分别验收。
本地组件实验的具体输入和输出
examples/cryptography/18_proxy_mtls.py 在每次运行的自动删除临时目录内生成一张教学 CA、代理证书和合成客户端证书;前端和后端都只监听 127.0.0.1 的随机端口。成功例中客户端指定 orders.test 与 CA,并出示 clientAuth 证书;代理设置 ssl.CERT_REQUIRED 并通过 getpeercert() 确认收到已经 TLS 校验过的对端证书。代理再用普通 http.client.HTTPConnection 将固定订单正文传给后端,后端明确看到原始 order=demo-001;quantity=1 字节。
1 | |
2026-10-06 UTC,在 Python 3.12.3 / OpenSSL 3.0.13 上两条命令退出码均为 0,1 项真实协议组件测试通过。成功输出同时包含 client_to_proxy_mtls_accepted=true、proxy_saw_verified_client_cert=true 与 backend_plain_order_seen=true;后端传输方式明写 HTTP over loopback (no TLS)。作为失败对照,另一客户端仍信任正确代理,但没有提供客户端证书,前段握手或其后读写被拒。实验结束后临时证书目录不存在;没有把合成私钥装进操作系统信任库或提交仓库。
这个正例只说明“指定 CA 信任下双向握手成立、后端看到明文”,负例只说明“缺证书不能通过代理要求的客户端认证”。没有执行代理→后端再加 TLS、真实 Istio 网格、业务权限映射、证书轮换、多实例重试与现实订单交付;若要声称后段也受保护,必须再为那一段配置和验证自己的 TLS/mTLS,记录各自的端点、受信根和错身份对照。
代理转发身份的边界
后端如果收到 X-Client-Cert、X-User-ID 之类的头,不能仅因为值看着像证书字段就相信它代表经过双向 TLS 验证的用户。需要限定只有受信代理能设置此头、客户端原始同名头被丢弃,代理与后端通信段受合适保护,后端按业务权限规则对证书身份作映射。否则攻击者在客户端请求里直接填写自己想要的用户名,前段 TLS 也只能认证“某证书持有人发出了这些字节”,不能保证头里的业务身份正确。
有的系统选择透传 TLS 到后端,有的系统在代理终止后重新建立代理到后端的独立 mTLS;两种路径保护范围不同,不宜套一句“全链路 mTLS”。工作负载证书、客户端证书、API 令牌和钱包交易密钥也不能混成一份“服务密钥”:签发权、验证权、取消权和过期策略各自不同。
两道带答案的练习
画图题。 前端 mTLS 成功、后端看到明文。分别写出客户端、代理、后端实际验证了谁,谁手上有哪一把证书私钥。若后端只凭代理发来的 X-User-ID 授权,要补怎样的信任与头部处理?
可核对答案: 客户端在前段验证代理服务名/证书;代理验证客户端证书持有人;后端本例只收到代理的明文 HTTP,不验证客户端 TLS 会话。CA 持签发私钥,代理持自己的服务私钥,客户端持自己的客户端私钥;后端不应得到这些私钥。必须保证只受信代理可发可信身份断言、客户端自填头被清理,并在后段建立适当访问/传输约束与业务权限映射。
变更题。 删除 18_proxy_mtls.py 中 server_context.verify_mode = ssl.CERT_REQUIRED,再运行脚本和单测。预期哪个负例不再成立?后端收到明文这一正例会不会因此变成密文?
可核对答案: 未带客户端证书的连接可能成功,missing_client_cert_rejected 应失败;代理前段不再证明“必须持有客户端证书”。后段仍是 HTTPConnection,是否明文只由第二段的实际配置决定,删除前段客户端认证不会把后段变成 TLS。
资料与导航
- RFC 9846,客户端证书请求与认证、RFC 5280,证书用途与路径:两套独立持有者与验证者的依据。
- 服务网格 20:mTLS 开启后哪些连接受到保护:旧系列的网格视角;本章没有复用其集群实验结论。
- 17 恢复连接、PSK 与 0-RTT · 14 证书绑定服务名与公钥 · 19 QUIC 如何复用 TLS 保护数据包。






