4. mTLS 双向认证:原理、配置与运维实践

mTLS,说白了就是“你给我看你的证书,我也给你看我的证书”。

跟浏览器访问 HTTPS 网站那种单向认证不一样。单向认证只验证服务器身份,客户端是不需要掏证书的。但在企业内部 API 网关和微服务之间传数据,光验服务端不够。谁调用谁?得两头发誓。

我在前东家做内部核心交易网关时,就被攻破过一次——某个中间层服务伪造了调用方身份,往结算服务里塞了几笔假账。从那以后,我把所有服务间调用都强制切成了 mTLS。一步到位,省得后面补窟窿。

4.1 核心原理:一次握手的完整画面

mTLS 的握手仍然是 TLS 1.3 或 1.2 的流程,只多了一步:服务端也会请求客户端证书,并且验证它。

典型的握手步骤大概是这样的:

  1. Client Hello — 客户端发支持的密码套件、TLS 版本。
  2. Server Hello — 服务端选定参数,并带上自己的证书(Server Certificate)。
  3. Certificate Request — 服务端额外发一条,说“你的证书呢?拿来看看”。
  4. Client Certificate — 客户端把自己的证书发给服务端。
  5. 两端互相验证 — 各自校验证书链、有效期、吊销状态,并确认对方私钥正握在自己手里。
  6. 密钥协商 + 加密通信 — 正常走 ECDHE 或其它密钥交换,之后全加密。

这里容易踩坑的点,我记得最清楚的就是第 3 步。很多网关默认不启 ssl_client_certificate 或者 verify_client,结果双向认证变成“只验服务端、客户端随便传个空证书也放行”。

小窍门: 配置 mTLS 时务必设置 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"
    }
  }'
重要: Kong 的 mtls-auth 默认只验证证书链,不检查 CN/SAN 是否匹配调用方身份。你必须在 upstream 里再做一层业务级校验,或者用自定义插件提前拦截。我曾经吃过这个亏,一个伪造的客户端证书虽然签名合法,但 CN 是错的,网关直接放行了。

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 认证决策流

mTLS 双向认证决策流程图 客户端 API网关 上游服务 阶段1:TLS握手 & 双向证书交换 网关验证客户端证书链 (CA)、有效期、CRL/OCSP;客户端验证服务端证书 阶段2:校验结果决策 根据 ssl_client_verify 判断:SUCCESS → 放行;FAILED/NONE → 拒绝连接 允许转发请求 返回 401 / 拒绝 ⚠ 上游服务仍需校验 X-Client-Cert-* 头 网关只做证书链验证,业务身份由上游服务基于证书 DN/SAN 控制

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_cert base64 打印到 INFO 日志里,一下子泄露了所有内部服务身份。建议只记录 hash 或脱敏后的标识。
核心结论: mTLS 不是加个配置就完事的。它需要一套完整的生命周期管理——签发、分发、校验、吊销、监控。你投入的精力越多,网关的防护边界就越硬。