mTLS 双向认证证书轮转

解读

在国内互联网、金融、政务云等高安全等级业务里,PHP 服务常以微服务或 API 网关形式对外提供接口。为了符合《网络安全法》《等保 2.0》以及 PCI-DSS、GDPR 等合规要求,越来越多的公司把“零信任”落到协议层:对外、对内全部启用 mTLS(Mutual TLS)。
面试问“证书轮转”并不是考你会不会 openssl x509 -req,而是看你是否理解“在线业务不停机、零信任架构不掉线”的完整闭环:证书生命周期管理、PHP 运行时的热加载、灰度发布、异常回滚、可观测性、合规审计,一个都不能少。
一句话:证书要到期、要泄露、要算法升级,PHP 进程如何在用户无感知的前提下完成“信任链切换”。

知识点

  1. 证书生命周期
    – 有效期:国内 CA 一般签发 1 年,Let's Encrypt 90 天;等保要求“到期前 30 天完成更换”。
    – 算法:RSA 2048/3072/4096 → ECDSA P-256/P-384;国密 SM2 在政务场景强制。
    – 吊销:CRL、OCSP Must-Staple;PHP 侧需开启 openssl.cafile 并定时更新 CRL。

  2. PHP 与 TLS 的耦合点
    – FPM / CLI 模式:证书私钥落在文件系统,进程启动时读入内存,不支持 inotify 自动重载。
    – Swoole / OpenSwoole / Swow 协程引擎:支持 SSL_CTX 热替换,可在 onWorkerStart 里轮询证书目录。
    – 反向代理层:Nginx/Envoy/Tengine 做 TLS 终端,PHP 只跑 HTTP,证书轮转在代理层完成,PHP 需感知“代理层版本号”以便灰度。

  3. 轮转策略
    – 并行双证:新证书与旧证书同时信任,逐步缩容旧证。
    – 证书版本号:在 SAN 里加入 version=2025Q2 自定义字段,PHP 代码按版本路由灰度。
    – 密钥隔离:私钥走 KMS(阿里云 KMS、腾讯云 KMS、Vault),PHP 通过 openssl_pkey_get_private("engine:kms") 调用,永不落盘。

  4. 热加载实现
    – FPM:USR2 信号平滑重启,但会清空 OPcache;需配合 opcache.preload 预热,重启耗时 200~500 ms。
    – Swoole:$server->set(['ssl_cert_file' => '/certs/live.crt', 'ssl_key_file' => '/certs/live.key']); 通过 reload() 只重启 Worker,零丢包。
    – 容器化:把证书做成 emptyDir + Reloader 边车,文件更新后触发 kill -USR2 1,Kubernetes 原生支持。

  5. 可观测与回滚
    – Prometheus:导出 openssl_x509_parse($cert)['validTo_time_t'] 作为指标,提前 30 天告警。
    – Trace:在 Guzzle/Swoole HTTP 客户端中间件注入 X-Cert-Version,链路追踪证书版本。
    – 回滚:保留旧证书 7 天,Nginx ssl_trusted_certificate 双证信任,PHP 配置通过 Consul/Kv 秒级回切。

  6. 合规审计
    – 国密:PHP 使用 gmssl 扩展或 Swoole-GMSSL 分支,支持 SM2 双证轮转。
    – 等保:证书更换需双人审批,Jenkins 流水线留痕,Webhook 自动写审计表 certificate_audit_log

答案

“我们在每日亿级调用的支付微服务里落地过三次 mTLS 证书轮转,核心思路是‘代理层双证 + 业务层无感 + 可观测 + 一键回滚’。
第一步,提前 35 天在阿里云 KMS 生成新 RSA-3072 证书,把完整链(leaf + intermediate + root)压到 Consul Kv,版本号 pay-v202506
第二步,Nginx-Ingress 同时挂载新旧两份证书,利用 map $ssl_server_name $cert 实现 1% 灰度;PHP-FPM 无需重启,因为 TLS 终端在 Nginx。
第三步,PHP 侧在 Guzzle 中间件里读取 X-Client-Cert-Version,如果版本低于预期就写日志并告警,确保旧证流量逐步归零。
第四步,Swoole 协程服务采用 SSL_CTX 热替换:

$server->on('WorkerStart', function ($serv, $workerId) {
    $certDir = '/certs/live';
    $certMtime = filemtime($certDir . '/server.crt');
    if ($certMtime > $serv->certMtime) {
        $serv->set([
            'ssl_cert_file' => $certDir . '/server.crt',
            'ssl_key_file'  => $certDir . '/server.key',
        ]);
        $serv->certMtime = $certMtime;
        $serv->reload();
    }
});

第五步,Prometheus 采集证书过期时间,Grafana 面板提前 30 天红色告警;同时把轮换记录写进审计表,满足等保三级‘双人复核 + 日志留存 6 个月’要求。
第六步,保留旧证 7 天,万一异常,通过 Consul 一键切回,Nginx 和 PHP 同时回滚,整个过程用户 0 报错。
这样做到‘业务无感知、合规可审计、回滚秒级’,三次轮转零故障。”

拓展思考

  1. 国密 SM2 双证:PHP 官方 OpenSSL 扩展不支持 SM2,如果政务云强制国密,你会选择
    A. 用 gmssl 扩展重新编译 PHP,
    B. 用 Swoole-GMSSL 分支,
    C. 把 TLS 终端下沉到国密硬件网关(如江南科友、格尔网关),PHP 只跑 HTTP?
    请给出编译参数、性能损耗评估和合规报告模板。

  2. 零信任环境下的“证书即身份”:当客户端证书里携带 UID、部门、角色,PHP 如何在不解析证书的情况下完成 RBAC?
    提示:Envoy 的 principal 透传头部 + JWT 聚合,PHP 只认 JWT,证书解析下沉到 Sidecar。

  3. 证书泄露应急响应:私钥走 KMS 不落盘,但如果 KMS 的 AK/SK 泄露,攻击者可任意签发证书,PHP 侧如何在 5 分钟内完成“信任根切换”?
    需要设计“KMS 密钥版本 + 证书吊销列表 + 紧急配置热更新”的联动方案,并给出 Ansible Playbook 示例。