ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

ZooKeeper SSL/TLS加密配置实战:从证书生成到客户端接入

ZooKeeper SSL/TLS加密配置实战:从证书生成到客户端接入

有一次帮内部中间件平台做上线前安全自查,评审的人指着监控面板问了一句:你们的协调服务,客户端和服务端之间是明文还是加密?我当时心里咯噔一下——因为不止是客户端到服务端,集群节点之间也一直在裸奔。回去翻了翻配置,那套跑了两年多的Zookeeper,所有配置下发、临时节点注册、会话管理,全部走默认的非加密端口。那次之后我花了两个周末,把 Zookeeper 的 SSL 通信从证书生成到客户端接入完整过了一遍,踩了一堆文档里没写的坑。这篇把整个流程和一些关键细节整理出来,给后面要做的同学当参考。

先交代清楚这篇攻略能解决什么问题:给 Zookeeper 3.5 以上版本开启 SSL 加密与证书认证,让 Java 客户端、命令行客户端都能走安全的secureClientPort连接集群,同时补充常见握手失败的排查思路、双向认证和证书轮换的生产实操方案。适合刚好在搞安全整改、或者准备给新集群提前上 TLS 的运维和开发同学,内容不区分具体框架,底层原理讲明白之后,无论你的客户端是原生 ZK 接口还是封装过的框架,都能直接套用。

1. 先弄明白一件事:ZK 这次的"认证"到底证的是谁

1.1 裸奔状态下,协调链路里跑着哪些敏感数据

很多人觉得 Zookeeper 跟消息队列不一样,它不存业务数据,所以裸奔无所谓。这个认知在安全评审面前基本站不住脚。ZK 里存的节点虽然小,但往往是分布式系统里最有价值的那部分元数据:服务实例的注册地址、分布式锁的持有者信息、动态配置项、集群主节点选出来之后的全局状态。这些数据在网络里以明文字节流传输,只要有人能在交换机上做镜像抓包,或者更简单一点,在内网一台机器上跑个监听工具,配置内容和服务拓扑几乎等于白送。

比偷看更麻烦的是中间人攻击。明文协议下,攻击者不需要破解任何东西,只要在网络路径上伪装成合法的 ZK 服务端,客户端就会傻乎乎地把会话交过去。之后攻击者既可以假冒客户端往 ZK 里写节点、抢锁、删配置,也可以把 ZK 返回的数据篡改了再转发给客户端。这比单纯窃听严重一个量级,因为问题不是"信息泄露",而是"协调逻辑本身被操纵了"。分布式系统里锁和配置一旦被操纵,表现出的故障会非常诡异,排查半天可能都想不到是链路层出了问题。

所以给 ZK 上 TLS,不是锦上添花,而是把"协调服务"真正当作基础设施来对待的基本动作。至少要和数据库链路、缓存链路一样,加密这件事应该默认开启,而不是等评审来问。

1.2 SSL 在这里提供的不是一把锁,而是三件事

光说"SSL 就是加密",其实低估了它在这个场景里的作用。客户端和服务端之间拉起一条 TLS 连接,实际上同时完成了三件事。

第一是机密性。握手之后双方协商出会话密钥,所有 ZK 协议报文都变成密文,监听者抓到的只有 TLS 记录头和数据包,看不到节点路径、看不到 watch 事件、看不到配置内容。

第二是服务端身份认证。客户端在握手阶段会校验服务端证书是否由自己信任的 CA 签发,这相当于确认"我连的确实是运维同学搭的那台 ZK,而不是中间某个冒牌货"。这一点对 ZK 尤其重要,因为 ZK 集群靠 IP 和端口定位,内网里 IP 是可以被劫持的。

第三是完整性保护。TLS 的 MAC/签名机制保证密文在传输过程中被篡改之后,接收方会直接丢弃并断开连接,攻击者想"改一个字段再转发"是做不到的。

另外要澄清一个常见的概念混淆:Zookeeper 里有另一套基于 SASL 的应用层认证(Digest-MD5 账号密码、Kerberos 票据),和 SSL 证书认证是两码事。SASL 解决的是"登录用户是谁、有没有权限",SSL 解决的是"传输通道安不安全、连接的双方是不是证书声称的那个人"。标题里说的"SSL 认证"指的是后者,基于证书的传输层身份认证,两条链路可以同时配,但不互相替代。

1.3 版本与组件的硬门槛

动手之前最该确认的是版本,因为 Zookeeper 的 SSL 支持是 3.5 才引入的。3.4 及以前的版本没有内置 TLS 能力,想加密只能在外面套一层隧道,要么自己维护端口映射,非常痛苦。如果集群还在 3.4,别想着改配置能解决,老老实实规划升级到 3.5 以上。

即便到了 3.5、3.6,还有两个组件层面的硬性要求:服务端连接工厂必须从默认的 NIO 实现切换到 Netty 实现,客户端连接 socket 同样要换 Netty 实现。这是因为 ZK 的 SSL 功能是在 Netty 通道上做的,老 NIO 通道根本不认识 TLS 相关配置。服务端对应serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory,客户端对应zookeeper.clientCnxnSocket=org.apache.zookeeper.ClientCnxnSocketNetty,少任何一个都会在连接阶段直接失败。后面配置章节会再展开。

JDK 版本也会影响最终能协商出的 TLS 协议版本,JDK 8 一般支持到 TLSv1.2,JDK 11 以上对 TLSv1.3 支持更好。ZK 本身不做太多干预,直接用 JVM 默认的 TLS 实现,所以如果你的服务端和客户端 JDK 大版本差太多,可能出现"服务端最高只认 1.2、客户端非 1.3 不可"的协商失败。硬件门槛不难满足,但版本和组件两个前提必须提前对齐。

2. 证书制备:用 keytool 搭一条自签名信任链

2.1 keystore 和 truststore,一个装自己,一个信别人

刚开始接触 Java 证书体系的人,最容易卡在 keystore 和 truststore 的区别上。我用一个生活化的类比来解释:keystore 像你的身份证,里面装的是"我是谁"的私钥和证书,出示给对手方看;truststore 像你手机通讯录里"可信任联系人"的名单,里面装的不是你的身份,而是你愿意相信的 CA 证书,用来核实对方递过来的证件是不是真的。

服务端需要 keystore,因为它是被验证的一方,要把自己的证书亮出来。客户端需要 truststore,因为它要核验服务端证书的签名链。如果做双向认证(mutual TLS),那么两边都要同时准备 keystore 和 truststore:服务端用 keystore 证明自己、用 truststore 验证客户端;客户端用 keystore 证明自己、用 truststore 验证服务端。

实操中最常见的错误就是把同一份 JKS 文件既当 keystore 又当 truststore,逻辑上能通,但会把私钥暴露给不需要私钥的一方,安全上非常不推荐。

2.2 自建 CA 并签发服务端证书

生产环境里大多数 ZK 集群规模都不大,没必要去公开 CA 机构买证书,自建一套内部 CA 是最省事的方案。核心思路:先创建一个 CA 密钥对,再用这个 CA 去签发各个 ZK 节点的服务端证书,最后把 CA 证书导入所有需要校验身份的一方。

第一步,生成 CA 密钥对。这一步可以在一台"签发机"上完成,这台机器只负责管 CA,不对外提供服务:

keytool -genkeypair -alias zk-ca -keyalg RSA -keysize 2048 -validity 3650 \ -dname "CN=Internal ZK CA, OU=Platform Infra, O=DevOps, C=CN" \ -keystore ca.jks -storetype JKS \ -storepass ca123456 -keypass ca123456

第二步,为每个 ZK 节点生成独立的密钥对。注意-dname里的CN字段非常关键,它要填客户端实际用来连接这个节点的主机名,也就是连接串里写的那个域名。如果是 IP 直连,就填 IP,或者在证书里补 SAN 扩展。每个节点单独生成一份,不要让所有节点共用同一个证书,后面做轮换和故障隔离时会省很多事:

keytool -genkeypair -alias zk-server-01 -keyalg RSA -keysize 2048 -validity 825 \ -dname "CN=zk-server-01, OU=Platform Infra, O=DevOps, C=CN" \ -keystore server-01.jks -storetype JKS \ -storepass server123 -keypass server123

第三步,从节点 keystore 里导出证书签名请求(CSR),交给 CA 签名:

keytool -certreq -alias zk-server-01 -keystore server-01.jks -file server-01.csr -storepass server123

第四步,CA 对 CSR 签名。如果 JDK 版本支持,顺手加上 SAN 扩展,这样一台机器用域名和备用域名都能通过主机名校验:

keytool -gencert -alias zk-ca -keystore ca.jks \ -infile server-01.csr -outfile server-01-signed.pem \ -validity 825 -storepass ca123456 -rfc \ -ext "SAN=DNS:zk-server-01,DNS:zk1.dev.internal"

第五步,把 CA 证书和已签名的服务端证书导回节点 keystore。顺序有讲究,先导入 CA 再导入自己的签名证书,这样 keystore 里就构成了一条完整的信任链:

keytool -importcert -alias zk-ca -file ca.pem -keystore server-01.jks -storepass server123 -noprompt keytool -importcert -alias zk-server-01 -file server-01-signed.pem -keystore server-01.jks -storepass server123 -noprompt

2.3 把 CA 送到客户端手里

客户端要校验服务端证书,但它不需要私钥,只需要把签署服务端证书的那张 CA 证书放进自己的 truststore。这一步在任何一台机器上执行即可:

keytool -exportcert -alias zk-ca -keystore ca.jks -rfc -file ca.pem -storepass ca123456 keytool -importcert -alias zk-ca -file ca.pem -keystore client-trust.jks -storepass client123 -noprompt

注意这里导入的是 CA 证书而不是各节点的服务端证书。truststore 里只要有 CA,那么所有由该 CA 签发的节点证书都能通过校验。这样做的好处是客户端配置只需要维护一个 truststore 文件,集群加节点、换节点证书时,客户端完全不用动。如果反过来把单个节点证书导入 truststore,那集群扩容一次就得给所有客户端重新推一遍。

2.4 证书文件的管理与权限细节

证书文件生成完,接下来是文件管理。几个我踩过、也看同事踩过的细节:

一是密码泄露问题。keytool 的私钥密码和 storepass 会明文出现在 zoo.cfg 和 JVM 参数里,所以证书文件本身必须锁权限,建议chmod 600,配置文件同样收权限。至少不要让普通应用账号能读到 JKS 文件,否则私钥等于公开。

二是 JKS 和 PKCS12 的选择。老版本默认 JKS,JDK 9 之后官方推荐 PKCS12。ZK 对两种格式都支持,我建议统一用 PKCS12,-storetype PKCS12,兼容性更好,也不容易被历史遗留工具链卡住。注意 PKCS12 里 keypass 必须和 storepass 相同,生成的时候别把两个密码设置成不一样的,否则导入时各种报错。

三是有效期。很多团队自签证书时直接-validity 3650,十年有效期图省事,但安全评审通常会对此有异议。合理做法是 CA 给五年,节点证书给两到三年,并在日历上设好轮换提醒。证书过期不像端口不通那么明显,它往往表现为"某个节点突然连不上",而且错误信息有一定迷惑性,非常容易误判成网络故障。

3. 服务端开启 SSL:zoo.cfg 逐行拆解与重启验证

3.1 关键参数与完整配置样例

服务端配置集中在zoo.cfg。一套最小可用的 SSL 配置长这样:

tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zk clientPort=2181 secureClientPort=2281 serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory admin.enableServer=true ssl.keystore.location=/etc/zookeeper/ssl/server-01.jks ssl.keystore.password=server123 ssl.truststore.location=/etc/zookeeper/ssl/client-trust.jks ssl.truststore.password=client123 ssl.hostnameVerification=true

我把几个关键参数的作用和容易出错的地方列个表:

参数作用注意事项
secureClientPort开启 SSL 客户端端口不要跟clientPort重复,建议单独网段/单独端口管理
serverCnxnFactory指定 Netty 连接工厂不设置的话secureClientPort直接不可用
ssl.keystore.location服务端证书库路径放节点自己的 JKS,含私钥
ssl.keystore.password证书库密码明文写在配置里,注意文件权限
ssl.truststore.location服务端信任库路径单向认证时至少放自己的 CA,双向认证时放客户端 CA
ssl.hostnameVerification是否校验客户端连接时的主机名生产环境保持true

提示:配置改完一定要逐项核对,特别是ssl.truststore.location。在单向认证场景下,服务端严格来说不必须验证客户端证书,但 ZK 的 SSLContext 初始化依然会读取这个 truststore,路径写错或密码不对,服务端启动阶段就会抛异常,根本起不来。所以先给服务端也准备一个 truststore 是最稳妥的。

3.2 为什么 serverCnxnFactory 必须是 Netty

这个坑我一开始没注意,结果服务端启动完,secureClientPort怎么连都连不上,日志还静悄悄的。后来翻了源码才知道,ZK 处理客户端连接默认走的是NIOServerCnxnFactory,这个实现压根没有 TLS 能力;只有 NettyServerCnxnFactory 的 pipeline 里挂了 SSL 处理器。你配置了secureClientPort却不改工厂类,ZK 启动时既不会报错,也不会真的监听这个 SSL 端口,表现就是"端口看起来开了、但连接完全不通"。

另外一个容易忽略的点是,clientPort和secureClientPort可以同时存在。过渡期里这是有意为之:老客户端继续走 2181,新客户端走 2281,等全部迁移完再关掉明文端口。这种双端口模式在配置迁移、灰度切换的时候非常有用,后面第 5 节会展开讲具体怎么迁。

3.3 双端口过渡方案与重启验证

修改zoo.cfg之后,对节点逐个执行滚动重启。ZK 集群是过半可用就能继续服务,所以一次重启一个节点,等它重新加入集群、follower 状态恢复正常之后再动下一个。重启动作本身对客户端的影响主要是会话重连,短暂的网络抖动会让客户端触发重连逻辑,只要 session 超时配置合理,一般无感知。

重启完先看本地端口监听:

netstat -lntp | grep 2281 ss -lntp | grep 2281

如果 2281 在监听,说明 Netty 工厂和 SSL 配置已经初始化成功。再翻一下 ZK 日志,正常情况能看到 Netty 连接工厂启动的记录,以及类似下面这样一行 SSL 上下文初始化的日志(不同版本略有差异):

INFO NettyServerCnxnFactory - Using Netty to service client connections. INFO NettyServerCnxnFactory - SSL context created for server.

没有这行信息或者直接搜不到secureClientPort相关日志,多半就是serverCnxnFactory没生效,回去检查配置文件是不是被覆盖了。

3.4 用 openssl 和日志确认握手成功

服务端配好了之后,在客户端机器上可以用openssl s_client快速验证 TLS 握手,不用先写任何 Java 代码:

openssl s_client -connect zk-server-01:2281 \ -CAfile ca.pem -servername zk-server-01

如果证书链正常,输出里会有一段SSL-Session信息,显示协商出的协议版本和 cipher suite。如果出现证书链错误或者主机名不匹配,banner 里直接就能看到原因。这个命令是排查"服务端层面"问题最快的手段,因为它绕过了 ZK 应用层,直接验证传输层 TLS 是否可用。握手通过之后再接 ZK 客户端,分层定位就非常清晰:TLS 都握不上是证书/网络问题,TLS 能握上但 ZK 会话建不起来才是应用层协议问题。

4. 客户端连接:Java 应用、zkCli 和常见框架适配

4.1 Java 客户端的三行系统属性

服务端开好了,客户端这边才是真正容易乱的地方。原生 ZooKeeper Java 客户端在创建ZooKeeper实例的时候会读取一系列系统属性,要连 SSL 端口,核心是三个:

系统属性值作用
zookeeper.client.securetrue通知客户端走 SSL 连接
zookeeper.clientCnxnSocketorg.apache.zookeeper.ClientCnxnSocketNetty使用 Netty socket 实现
zookeeper.ssl.truststore.location客户端 truststore 路径校验服务端证书
zookeeper.ssl.truststore.password对应密码打开 truststore

代码里最简单的写法是在创建连接之前设置:

System.setProperty("zookeeper.client.secure", "true"); System.setProperty("zookeeper.clientCnxnSocket", "org.apache.zookeeper.ClientCnxnSocketNetty"); System.setProperty("zookeeper.ssl.truststore.location", "/etc/zk-client/client-trust.jks"); System.setProperty("zookeeper.ssl.truststore.password", "client123"); ZooKeeper zk = new ZooKeeper("zk-server-01:2281,zk-server-02:2281", 10000, watcher);

这里最容易被忽略的是第二行。很多同学只在 JVM 参数里加了-Dzookeeper.client.secure=true,连zookeeper.clientCnxnSocket没设,结果客户端还是走 NIO socket,NIO 不支持 TLS,连接直接失败或者表现成"一直超时"。这两个属性必须同时出现,少一个都不行。

如果做双向认证,客户端还需要额外配置自己的 keystore:

System.setProperty("zookeeper.ssl.keystore.location", "/etc/zk-client/client.jks"); System.setProperty("zookeeper.ssl.keystore.password", "client-keypass");

4.2 zkCli.sh 命令行客户端怎么带参数

命令行客户端zkCli.sh本质是个 Java 程序,所有的系统属性都可以通过JVMFLAGS环境变量传进去。这个变量很多人不知道,它专门用来给 ZK 启动脚本附加 JVM 参数:

export JVMFLAGS="-Dzookeeper.client.secure=true \ -Dzookeeper.clientCnxnSocket=org.apache.zookeeper.ClientCnxnSocketNetty \ -Dzookeeper.ssl.truststore.location=/etc/zk-client/client-trust.jks \ -Dzookeeper.ssl.truststore.password=client123" bin/zkCli.sh -server zk-server-01:2281

连接串里的端口必须和secureClientPort一致,别写成 2181。命令行客户端适合做手工验证,比如在排查某个节点证书问题时快速确认"用这个 truststore 到底能不能连上"。

4.3 应用接入时的几个常见姿势

生产环境里,应用一般不会直接new ZooKeeper(),而是通过封装框架接进来。好在大多数客户端框架底层都是复用原生 ZK 客户端,系统属性设置方式完全通用。

比如使用 Curator(Apache 旗下的 ZK 客户端封装库)的时候,不需要改任何 Curator 专属配置,只要保证应用 JVM 里带着前面那几个系统属性,Curator 创建的底层 ZooKeeper 实例会自动走 SSL。启动脚本里用JAVA_OPTS加参数就行:

JAVA_OPTS="$JAVA_OPTS \ -Dzookeeper.client.secure=true \ -Dzookeeper.clientCnxnSocket=org.apache.zookeeper.ClientCnxnSocketNetty \ -Dzookeeper.ssl.truststore.location=/opt/app/conf/client-trust.jks \ -Dzookeeper.ssl.truststore.password=client123"

容器环境更简单,把 truststore 挂载成只读卷,环境变量注入密码,启动命令统一拼JAVA_OPTS。但要注意,容器里如果同时有多个 Java 应用共享同一个 truststore 文件,权限上至少保证应用用户有读权限;千万别把私钥文件(keystore)随随便便挂给所有应用,私钥应该只属于真正需要它的那一个进程。

4.4 验证客户端真的走了加密通道

配置完别急着说"连上了就完事",验证一下流量是不是真的加密了。最简单的方法:在客户端机器上对服务端 2281 端口做一次连接,然后在服务端用tcpdump抓包看内容:

tcpdump -i eth0 -nn port 2281 -A

明文协议下,-A能看到 ZK 协议里的节点路径、请求字段;TLS 开启后,抓到的只有一堆 TLS 记录头,正文全是密文,-A基本看不到可读字符。看到密文就说明链路已经加密了,这个验证手段非常直观,安全评审时也很有说服力。另一个佐证方式是看服务端连接建立情况,secureClientPort上有来自客户端 IP 的 ESTABLISHED 连接,且协议栈是 TLS。

5. 实操中的坑:握手失败排查与性能观察

5.1 报错与根因对照表

SSL 配置出错,报错样式千奇百怪,但归根到底就那么几类。我把实操中遇到最多的几组现象和对应根因整理成表:

典型现象根因处理方向
PKIX path building failed: unable to find valid certification path客户端 truststore 里没有签发服务端证书的 CA把 CA 证书导入客户端 truststore
连接一直超时或ConnectionLossException客户端没切 Netty socket,或者连接串端口写错检查zookeeper.clientCnxnSocket和端口号
SSLHandshakeException: Certificate for <host> doesn't match证书 CN/SAN 和连接串里的主机名不一致统一主机名,或补 SAN
Received fatal alert: handshake_failureTLS 版本或 cipher suite 协商失败对齐 JDK 版本,检查 TLS 协议配置
服务端启动报错:No such file或密码错误ssl.keystore/ssl.truststore路径或密码错误逐项核对配置
secureClientPort端口没监听serverCnxnFactory未配置 Netty补配置后重启

有一个排障原则值得强调:出问题先分清楚是传输层还是应用层。用openssl s_client探一下端口,如果握手都完成不了,那就别急着检查 ZK 客户端代码,问题一定在证书、主机名或网络层。反过来,openssl s_client能握手,但 ZK 客户端连不上,那才需要回头查应用属性配置。

5.2 主机名校验这关怎么过

主机名校验是自签名证书场景里最容易翻车的一环。ssl.hostnameVerification=true时,客户端会拿连接串里的主机名去匹配服务端证书里的CN或者SAN。这是一道很严格的检查,目的就是防止有人拿着一张有效证书用在别的机器上。

举个例子,证书CN=zk-server-01,但你在客户端连接串里写10.0.0.5:2281,握手时就会报证书不匹配。解决办法有三种,按推荐顺序排:

  1. 客户端连接串统一用证书里的主机名,别用 IP;
  2. 重新签证书,在 SAN 里加上 IP 或备用域名;
  3. 关掉ssl.hostnameVerification。

我强烈不建议选第三种。主机名校验是 TLS 身份认证的核心环节,关了它,前面辛苦建 CA、签证书的努力就废了一半。生产环境里真要临时排查问题,可以客户端单独带-Dzookeeper.ssl.hostnameVerification=false试一下,确认问题确实是主机名引起之后再回头正儿八经把证书改对,而不是直接把关闭校验写进长期配置。

5.3 性能实测:TLS 的代价到底多大

加密不是没有成本。TLS 握手阶段要多做密钥协商和证书校验,新建连接比明文连接慢几十毫秒到一百多毫秒(取决于网络 RTT 和 CPU)。但注意,这个开销只在"新建连接"时发生一次。ZK 客户端一般会复用连接、长期维持会话,不是每条读写请求都重新握手,所以对常规业务影响其实很小。

连接建立之后的读写阶段,TLS 的开销主要在两块:对称加密的计算成本,以及每个包多出来的 TLS 记录头和 MAC 校验。ZK 的请求响应都是小型节点操作,数据量小,AES 加密在现代 CPU 上有硬件指令加速,这点开销基本可以忽略。我这边在小集群上做过一轮对比测试,简单 get/set 操作在 TLS 下的时延增长大概在个位数到十几个百分点之间,对于协调服务这种"低吞吐、重时效"的典型负载,这个代价完全可接受。

真正的性能隐患反而在运维层面:如果应用频繁创建销毁 ZK 连接,比如代码里每次操作都new ZooKeeper(),那 TLS 握手的成本会被放大。这种时候应该先审视应用连接管理方式,上连接池或复用长连接,而不是反过来怪 TLS 慢。

5.4 平滑迁移策略

已经在跑的明文集群,不建议直接一把梭切 SSL。最稳的迁移路径是双端口并行:zoo.cfg里同时保留clientPort=2181和secureClientPort=2281,让所有客户端先切到 2281 验证功能正常,观察一段时间。

迁移过程中要盯三件事:一是服务端连接数,2281 的连接数应该稳步上涨,2181 的逐步下降;二是客户端日志,没有新的握手报错;三是应用的协调行为,比如锁请求、配置变更是否正常。等确认全部客户端都切过去了,再下线clientPort,彻底关闭明文通道。

回滚方案也要提前想好。最坏情况下客户端 TLS 配置有问题导致大面积断连,需要的操作只是把应用 JVM 参数里的zookeeper.client.secure改回false、把连接串端口改回 2181,重启应用即可。所以明文端口不要急着删,保留一个发布周期的冗余,能极大降低上线风险。

6. 进阶:双向认证与证书轮换

6.1 从单向认证到双向认证

前面配的是单向认证:客户端校验服务端身份,服务端不管你客户端是谁。这对单个团队自己用通常够了,但如果不只一个业务方共享同一个 ZK 集群,任何人都能带着一张"被信任的 CA 签发的客户端证书"连进来的话,你其实没法精确控制谁有资格接入。双向认证(mTLS)解决的就是这个问题:服务端在握手阶段也要求客户端出示证书,并校验它是否由服务端 truststore 里的 CA 签发。

配置上要动两处。服务端zoo.cfg里新增客户端证书的信任链:

ssl.truststore.location=/etc/zookeeper/ssl/server-trust.jks ssl.truststore.password=servertrust123

server-trust.jks里放客户端 CA 的证书。客户端这边则必须准备自己的 keystore:

keytool -genkeypair -alias zk-client -keyalg RSA -keysize 2048 \ -dname "CN=app-client-01, OU=App, O=DevOps, C=CN" \ -keystore client.jks -storetype PKCS12 \ -storepass clientkey123 -keypass clientkey123 keytool -certreq -alias zk-client -keystore client.jks -file client.csr -storepass clientkey123 # 用同一个 CA 签名 keytool -gencert -alias zk-ca -keystore ca.jks \ -infile client.csr -outfile client-signed.pem \ -validity 825 -storepass ca123456 -rfc keytool -importcert -alias zk-ca -file ca.pem -keystore client.jks -storepass clientkey123 -noprompt keytool -importcert -alias zk-client -file client-signed.pem -keystore client.jks -storepass clientkey123 -noprompt

然后把客户端 keystore 的路径和密码也配进客户端的系统属性里。部署完可以故意用一个没经过 CA 签发的证书去连,服务端直接握手失败,这就说明双向认证生效了。

要不要上双向认证,取决于你的威胁模型。如果 ZK 集群在同一内网、只有固定几个可信应用才拿得到连接信息,单向通常够了;如果是多团队混合、基础设施共享的大集群,强烈建议 mTLS,认证粒度从"谁能连"细化到"哪个客户端身份在连",配合 ZK 的 ACL 能形成两层防线。

6.2 证书轮换的完整流程

证书不是配一次就完了,有效期到了还是一场硬仗。轮换的核心原则是"让 truststore 短暂地同时信任新旧两代证书,减少停机窗口"。我把它拆成六步:

  1. 生成新的 CA 和证书,或者为旧 CA 签一批新节点证书。先把新的 CA 证书追加进所有客户端的 truststore,因为 truststore 可以同时装多个 CA,新旧并存不会互相冲突。
  2. 依次更新服务端的 truststore,同样新旧 CA 并存,不影响现有连接。
  3. 逐节点替换服务端 keystore,替换完滚动重启这个节点。
  4. 观察新节点是否正常加入集群、SSL 端口连接是否正常。
  5. 全部节点轮换完成后,从所有 truststore 里移除旧 CA。
  6. 记得清理签发机上过期的 CSR 和旧证书文件,别留一堆带私钥的历史文件。

轮换过程最怕的是"所有节点同一时刻重启"。ZK 的可用性依赖过半节点在线,如果三节点集群一次重启两个,选举都可能受影响。严格一次一个,确认每个节点回归之后再动下一个。

6.3 生产环境小清单

最后把生产环境该做不该做的整理成一份小清单,照着核对就好。

检查项建议
证书文件权限keystore 和 truststore 一律chmod 600,配置文件不放开读权限
密码管理配置里明文密码至少保证文件权限到位,更严格的话用配置中心下发系统属性
有效期日历证书到期前 30 天设提醒,别等到告警才发现过期
灰度通道保留明文端口直到所有客户端完成迁移
监控维度盯 2281 端口的连接数、握手失败数、TLS 握手时延
备份CA 的 keystore 一定要离线备份,CA 丢了,所有信任链都得重建
日志开启 SSL 相关日志级别,出问题时有迹可循

做完整套配置之后再回头看,其实 ZooKeeper 的 SSL 并没有多高深,无非是证书、两个配置项、几个系统属性。真正花时间的反而是那些不写在文档里的细节:Netty 工厂不能忘、主机名要对齐、迁移要留后路。我个人在实操里最大的体会是,给分布式协调链路上 TLS 这件事,越早做越省心——一旦跑起来再想加,就得面对"新旧客户端并存"的兼容期,比从第一天就配上要麻烦得多。另外一个实用小技巧:每次配完证书,用keytool -list -keystore <file> -storepass <pass>看一眼里面的证书指纹和有效期,把指纹记录下来,以后对比异常连接或排查篡改时会很有用。先按这套流程把集群跑通,等证书轮换、双向认证这些进阶点遇到时再回头翻这部分,你会有更深的体感。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进