开放问答
Java 8 + Netty 自研 mTLS 网关,证书原地替换后长连接仍握旧证书,如何做到零中断轮换?
该问题的正文、回答与评论。
提问者 温望舒-239 · 状态 open
mTLS证书轮换NettyJava 8安全加固
【背景】 我维护一套内部 API 网关:Java 8 + Netty 4.1.x,3 副本跑在 3 节点自建 K8s 1.26,没有 Istio / sidecar,mTLS 由网关自己用 SslContextBuilder 做。上游客户端是 Java 8(OkHttp 3.x)和 Go 1.21 混合,存在大量长连接(gRPC 双向流 + WebSocket)。安全基线要求客户端证书 90 天强制轮换,CA 自签、每客户端一证书(CN=clientId)。
【现象】
运维脚本目前的做法是:CA 签发新 p12 → mv new.p12 old.p12 && cp new.p12 old.p12 原地替换 → SIGUSR1 通知网关 reload。
问题:已建立的长连接在 reload 后仍然握旧证书,原因是 Netty 的 SslContext 在 ChannelPipeline 安装 SslHandler 时就绑定了旧 SslContext,而 SslHandler 是 stateful 的,replace 会丢 pending IO。新握手虽然能读到新 truststore,但客户端侧如果已经按基线把旧 CA 从 truststore 里摘掉,就会出现 SSLHandshakeException: bad certificate,直接 502。
【已尝试的排查与结果】
- 直接重启 JVM —— 能跑但长连接全断,业务方不接受;
- reload 时
SslContextBuilder.forClient().trustManager(new File(new.p12))重建 SslContext —— 新连接生效,老连接无效(如上); pipeline.replace("ssl", new SslHandler(newCtx))—— 单元测试里 pending 的 gRPC 流会丢,线上不敢用;- 客户端侧 CA 双活(新旧 CA 并存 N 天)—— 能跑,但 CA 侧要改签发逻辑,且新证书 CN 不变时旧客户端根本感知不到;
- 给 SslHandler 加一个"软失效"钩子,握手完成后比对证书 NotAfter,发现即将到期就主动
channel.close()让客户端重连 —— 思路可行但没人在生产验证过,担心 gRPC 长连接重连风暴。
【期望得到的答案形态】
- 一个可复现的 reload 时序:旧 SslContext / 新 SslContext 如何共存、长连接如何自然淘汰、新连接何时切到新 truststore;
- 是否有 Netty 官方或开源方案(比如 netty-tcnative、Conscrypt 的 SslContext 热加载 API);
- 客户端侧(Java 8 / Go 1.21)在 90 天轮换窗口内的最佳实践,避免"新 server 拒绝旧 client"或"新 client 拒绝旧 server"的握手失败;
- 如果必须客户端配合,请给出双 CA 并存的具体签发与校验规则(CN 不变、SAN 变化、NotAfter 双写等),以及灰度切换的推荐窗口。
评论
暂无评论。