4. mTLS 双向认证:原理、配置与运维实践
mTLS,说白了就是“你给我看你的证书,我也给你看我的证书”。
跟浏览器访问 HTTPS 网站那种单向认证不一样。单向认证只验证服务器身份,客户端是不需要掏证书的。但在企业内部 API 网关和微服务之间传数据,光验服务端不够。谁调用谁?得两头发誓。
我在前东家做内部核心交易网关时,就被攻破过一次——某个中间层服务伪造了调用方身份,往结算服务里塞了几笔假账。从那以后,我把所有服务间调用都强制切成了 mTLS。一步到位,省得后面补窟窿。
4.1 核心原理:一次握手的完整画面
mTLS 的握手仍然是 TLS 1.3 或 1.2 的流程,只多了一步:服务端也会请求客户端证书,并且验证它。
典型的握手步骤大概是这样的:
- Client Hello — 客户端发支持的密码套件、TLS 版本。
- Server Hello — 服务端选定参数,并带上自己的证书(Server Certificate)。
- Certificate Request — 服务端额外发一条,说“你的证书呢?拿来看看”。
- Client Certificate — 客户端把自己的证书发给服务端。
- 两端互相验证 — 各自校验证书链、有效期、吊销状态,并确认对方私钥正握在自己手里。
- 密钥协商 + 加密通信 — 正常走 ECDHE 或其它密钥交换,之后全加密。
这里容易踩坑的点,我记得最清楚的就是第 3 步。很多网关默认不启 ssl_client_certificate 或者 verify_client,结果双向认证变成“只验服务端、客户端随便传个空证书也放行”。
ssl_verify_client on(Nginx 里叫 ssl_verify_client optional_no_ca 也可以,但必须配合后续校验逻辑)。别设 off。
4.2 证书结构:哪几把“钥匙”必须到位
一套完整的 mTLS 需要四类证书/密钥:
| 角色 | 文件 | 说明 |
|---|---|---|
| Root CA | ca.crt / ca.key | 根证书,所有服务端和客户端证书的签发者。只有你自己持有私钥。 |
| Server 证书 | server.crt / server.key | 网关或服务端对外使用的身份凭证。 |
| Client 证书 | client.crt / client.key | 每个微服务/调用者生成一份,单独分发。 |
| CRL/OCSP | crl.pem / ocsp-responder | 吊销列表或在线状态协议,防止旧证书被滥用。 |
我曾在项目中见过有人把同一个 client.crt 给三个上游服务共用。三个月后,某个服务被下线,私钥没回收,结果被利用从外部钻进来。那之后我规定:每个服务甚至每个实例都用独立证书,用完就吊销。
4.3 配置实战(以 Kong Gateway + Nginx 反向代理为例)
API 网关通常作为 mTLS 的“入口点”。下面是一个典型的 Nginx 配置片段:
server {
listen 443 ssl;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# 根证书链,用来验证客户端证书
ssl_client_certificate /etc/nginx/certs/ca.crt;
# 强制客户端证书
ssl_verify_client on;
# 定义一个变量记录验证结果
ssl_verify_depth 2;
location / {
# 将客户端证书 DN 传给上游服务
proxy_set_header X-Client-Cert-Subject $ssl_client_s_dn;
proxy_set_header X-Client-Cert-Issuer $ssl_client_i_dn;
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_pass http://upstream_service;
}
}
注意 $ssl_client_verify 的值:SUCCESS、FAILED、NONE。你的上游服务必须检查这个头,如果返回 NONE 就拒绝请求,不然等于没开 mTLS。
如果是 Kong 网关,可以通过插件 mtls-auth 实现:
# 启用插件
curl -X POST http://localhost:8001/routes/{route_id}/plugins \
-H "Content-Type: application/json" \
-d '{
"name": "mtls-auth",
"config": {
"ca_certificates": ["your-ca-id"],
"revocation_check_mode": "CRL"
}
}'
4.4 日常运维:证书轮换、吊销与监控
证书有有效期,通常是 365 天。线上 mTLS 断了是重大事故。我习惯用以下套路:
- 自动轮转 — 每 60 天生成新证书,提前 30 天推送到网关和客户端。用 K8s 的 cert-manager 或自建的 CA 服务搞定。
- 白名单吊销 — 一旦发现某个客户端证书泄露,立马加入 CRL 并推送所有网关 reload。很多网关支持
ssl_crl指令,配合定时更新。 - 监控指标 — 最关键的指标是
ssl_client_verify != SUCCESS的计数。另外要监控证书过期时间,小于 30 天的上报告警。
举个我遇到的真实案例:某次夜间发布,Ops 不小心把新的 client.crt 签发日期设成了 pst 时区,网关用的是 UTC。新证书立刻被认定为“未来证书”,连接全部失败。好在那次有“熔断+回滚”机制,30 秒内切回了老证书。事后我给所有证书签发加上了统一时间戳 + 偏移测试。
4.5 一张图看懂 mTLS 认证决策流
4.6 避坑清单(都是真金白银的教训)
- 不要忽略 SAN 校验。 我曾经只验证了 CN,结果有人申请了同名 CN 的证书,私钥从另一个项目泄露,直接冒充成功。现在必须同时检查 SAN(Subject Alternative Name),比如
X-Client-Cert-SAN里包含 service-a.internal。 - CRL 更新延迟。 吊销证书后,如果网关每小时才拉一次 CRL,中间那 30 分钟就是窗口期。建议用 OCSP stapling 实时查询,或者将吊销列表推送到 etcd,网关通过 watch 动态 reload。
- 证书过期导致全链路瘫痪。 我见过一个极端案例:所有微服务证书在同一天过期。从那以后我用
cert-manager的自动轮转 + 提前 30 天告警,并且每次发布前先跑一次证书过期扫描。 - 双向认证的日志不要乱打。 证书 DN、S/N 都属于敏感信息。我曾看到有人把整个
ssl_client_certbase64 打印到 INFO 日志里,一下子泄露了所有内部服务身份。建议只记录 hash 或脱敏后的标识。