Zookeeper权限机制深度解析:ACL、digest认证与安全实践

Zookeeper权限机制深度解析:ACL、digest认证与安全实践 上个月给团队做 Zookeeper 内部培训我问了三个问题默认 ACL 是什么digest 密文到底怎么生成父节点 ACL 改了子节点要不要跟着改现场三十多个人能把三个问题都答明白的不超过三个。不是大家不努力而是 Zookeeper 权限机制平时很少有人系统梳理网上资料又比较零散基本都是遇到 NoAuth 报错才临时查查完又忘。其实 Zookeeper 权限机制没那么玄乎核心就三点认证决定你是谁ACL 决定你能干什么以及 ACL 在节点树上的生效范围。把这三块打通常见权限问题基本都能自己排查。这篇就按这个思路从原理讲到实操把我在生产环境里踩过的坑一并分享出来。1. 先把权限模型捋清楚为什么大家都答错1.1 默认 ACL 到底意味着什么很多人刚接触 Zookeeper 时装完环境就create /test xxx然后get /test一切正常于是以为 Zookeeper 没有权限控制。实际上它有一个默认的开放 ACL根节点初始是world:anyone拥有所有权限也就是“任何人不需要认证就能对节点做任何操作”。 新建的普通节点如果不显式指定 ACL会从父节点拷贝一份 ACL 作为自己的初始 ACL所以一路创建下去所有节点默认都是全开放的。这个设计在上线初期很省事但一旦上了生产尤其是多个团队共享集群时风险就大得吓人。没有 ACL 的情况下任何能连到 2181 端口的人都能把你的配置数据读走、改掉或者把分布式锁节点删掉。所以理解权限机制的第一步就是要意识到默认不是“没有权限”而是“权限全部开放给所有人”。1.2 权限机制的三段式认证、授权、生效范围Zookeeper 的权限体系由三个相对独立的部分组成认证Authentication客户端向服务器证明自己的身份Zookeeper 支持 digest、ip、world、sasl 等认证方式。授权Authorization服务器根据节点上的 ACL 规则决定某个身份是否可以执行某个操作。生效范围ACL 是配置在单个 znode 上的子节点创建时会从父节点“拷贝”一份 ACL但之后父节点的 ACL 变化不会自动同步给子节点。这三个部分经常被混在一起讨论导致很多人搞不清问题到底出在哪一步。比如客户端连不上受保护节点可能不是因为认证没通过而是因为客户端根本没有添加认证信息也可能是认证信息加对了但节点的 ACL 里没给这个身份授权。分开排查就清晰多了。1.3 一个例子说明“没有权限”和“没有认证”是两回事假设我在 /app/config 上设置了只允许digest:admin访问这时我用客户端直接执行get /app/config大概率会看到KeeperErrorCode NoAuth for /app/config。这个报错其实分两种情况我没有任何认证信息服务器根本不知道我是谁权限检查直接失败。我通过addauth digest admin:password添加了认证信息但密码不对或者 ACL 里配置的身份不是 admin同样会 NoAuth。第一种情况是“未认证”第二种是“已认证但未授权”报错都一样但排查方向完全不同。这也是为什么我建议所有排查都从getAcl开始先看目标节点要什么身份再看当前会话有没有这个身份最后看操作需要的权限位是否匹配。2. 第一点认证方式决定“你是谁”2.1 四种 scheme 怎么选world、auth、digest、ipZookeeper 的 ACL 条目由三部分组成scheme:id:permissions。scheme 决定了 id 的语义以及认证方式最常用的有四种schemeid 示例说明适用场景worldanyone所有客户端不需要认证默认开放、某些公共只读节点auth空使用当前会话的认证身份创建 ACL 时由服务器自动替换把权限绑定给当前登录用户比较省事digestusername:密文用户名加密文密文由 Zookeeper 工具生成日常生产最常用ip192.168.1.100按客户端 IP 限制内网信任环境或做基础网段隔离另外还有 sasl、x509 这类用于 Kerberos 和证书认证的一般企业规模不大用不上可以先忽略。日常用 digest 基本能解决绝大多数权限隔离需求。2.2 digest 认证实操addauth 与密文生成完整流程digest 认证的逻辑是服务器节点 ACL 里保存用户名和一段密文客户端连接后先通过addauth digest user:password把明文用户名密码交给服务器服务器用同样的算法校验这段信息是否匹配 ACL 里的密文。命令行里最常用的做法是先在当前会话添加认证身份再创建节点时用authscheme这样不用手工复制密文。示例# 连接 zkCli bin/zkCli.sh -server 127.0.0.1:2181 # 添加 digest 认证身份 addauth digest admin:admin123 # 创建一个节点并把 ACL 设置为“当前会话的认证身份” create /app init setAcl /app auth::cdrwa执行完getAcl /app会看到类似这样的输出digest,admin:8/2yPdc/pO0ZQm3v7qgM4pC1G6Q:0NThhZjEwMDA : cdrwa这个admin:哈希:盐值就是 digest 身份在 ACL 里的真实存储格式。下次别的客户端要访问 /app必须先用addauth digest admin:admin123添加同一个账号的认证信息否则操作会被拒绝。如果是给别人授权不方便在会话里先登录对方账号就需要主动生成密文并写入 ACL# 用 Zookeeper 自带的工具生成 digest 密文 java -cp zookeeper-3.7.1.jar:lib/* org.apache.zookeeper.server.auth.DigestAuthenticationProvider admin:admin123工具输出的整段内容就是可以放进 ACL 的 id 部分例如admin:8/2yPdc/pO0ZQm3v7qgM4pC1G6Q:0NThhZjEwMDA然后执行setAcl /app digest:admin:8/2yPdc/pO0ZQm3v7qgM4pC1G6Q:0NThhZjEwMDA:cdrwa注意必须把整段哈希:盐值完整拷进去不能只拷哈希部分因为服务器校验时需要用到盐值。2.3 digest 密文为什么不能手工拼salt 的作用很多人第一次看到 digest 密文时会下意识想这玩意儿是不是就是把密码做一次 SHA-1 然后 base64如果你也这么想恭喜你走进了最常见的误区。Zookeeper 的 digest 密文不是简单的SHA1(password)而是带随机盐值的。具体过程是生成一个随机的 salt把username:password和 salt 拼接后做 SHA-1再把摘要和 salt 一起编码。因为每次生成都会产生随机 salt所以同一个密码每次执行DigestAuthenticationProvider生成的密文都不同。这个设计的好处是防止彩虹表和密文碰撞。服务器为什么能校验因为盐值就存在 ACL 条目的后面一段服务器会把客户端提交的明文密码取出配合 ACL 里保存的盐值重新计算摘要然后对比前面的哈希值。理解了这一点你就不会再去尝试用echo admin:admin123 | sha1sum之类的方式伪造密文了。2.4 ip 认证的正确姿势与容易翻车的场景ip 认证理解起来最简单ip:192.168.1.100就是只允许这个 IP 访问ip:192.168.1.0/24可以限定网段。配置方式setAcl /app ip:192.168.1.100:cdrwa需要注意的是这里判断的 IP 是 Zookeeper 服务器实际看到的 TCP 连接来源 IP。一旦客户端经过 NAT、负载均衡、容器网络或者服务端在 Kubernetes 里通过 ClusterIP 访问来源 IP 可能与预期完全不同。我遇到过最典型的情况是本地开发用ip:127.0.0.1配得好好的部署到服务器后通过内部负载均衡访问Zookeeper 看到的 IP 变成负载均衡地址ACL 直接失效。所以在生产环境里ip 认证更适合用来做粗粒度的网络隔离比如只允许某个网段的机器访问某个配置中心根路径真正要区分不同使用者、不同应用还是建议用 digest。3. 第二点ACL 授权模型控制“你能做什么”3.1 五个权限位逐一说清CREATE/READ/WRITE/DELETE/ADMINZookeeper 的权限位有五个在 zkCli 里通常用缩写表示权限缩写全称作用cCREATE允许在当前节点下创建子节点rREAD允许读取节点数据、查看子节点列表wWRITE允许修改节点数据dDELETE允许删除当前节点的子节点aADMIN允许修改节点的 ACL以及获取 ACL 信息这里有一个非常关键的误区CREATE 和 DELETE 权限不是对“当前节点自身”生效而是对“当前节点的子节点”生效。也就是说你想在/app下创建/app/config需要检查的是/app的 CREATE 权限你想删除/app/config需要检查/app的 DELETE 权限。至于/app/config这个节点本身的 ACL 是否正确通常不决定它能否被父节点删除。这就好比 Unix 文件系统里创建和删除文件依赖的是所在目录的写权限而不是文件自己的权限。用这个类比去记基本不会错。3.2 用 setAcl/getAcl 把权限配置落到节点上在命令行里查看节点 ACL 用getAcl修改用setAcl。举一组完整例子# 查看 /app 的 ACL getAcl /app # 配置成只有 admin 用户能读和管理其他人不开放 setAcl /app digest:admin:8/2yPdc/pO0ZQm3v7qgM4pC1G6Q:0NThhZjEwMDA:rcdra权限位写在一起时顺序不限常见写法是cdrwa表示全部权限。想给只读用户就只给rsetAcl /app/public digest:readonly:xxxxxx:r需要注意的是setAcl本身可能破坏原有 ACL。比如原本节点是world:anyone:cdrwa你把它改成只允许 admin那么所有其他客户端立刻无法访问。这是个不可逆操作建议先在测试节点上验证。3.3 ADMIN 权限的特殊性谁掌握了 ADMIN 谁掌握全局五个权限位里最容易忽略的是 ADMIN。ADMIN 权限允许修改节点的 ACL也就是说谁拥有某个节点的 ADMIN 权限谁就能把节点 ACL 改掉给自己增加权限。举个例子/app 的 ACL 只允许admin写入但某个普通用户拥有 /app 的 ADMIN 权限。这个用户可以先setAcl /app world:anyone:cdrwa把权限改成全开放再随意读改数据。所以规划权限时ADMIN 权限一定要比 READ/WRITE 更谨慎尽量只分配给运维或管理员账号。另外不同版本对getAcl的权限要求不完全一致有些版本要求 READ有些版本要求 ADMIN。实际排查时如果getAcl报 NoAuth可以往 ADMIN 权限方向补一下这是比较省事的处理方式。3.4 super 超级用户留一条可控后门权限配置再严格也怕“所有人都没权限了”这种极端情况。Zookeeper 提供了 super 超级用户机制专门用于绕过节点 ACL 做管理操作。启用 super 需要为服务端配置系统属性JVMFLAGS-Dzookeeper.DigestAuthenticationProvider.superDigestadmin:8/2yPdc/pO0ZQm3v7qgM4pC1G6Q:0NThhZjEwMDA bin/zkServer.sh start密文同样用DigestAuthenticationProvider生成只是生成时的用户名建议用一个独立的管理账号。配置完成后客户端连接时执行addauth digest admin:admin123此时当前会话就拥有了超级用户身份可以对任意节点执行任何操作包括修改别人节点的 ACL。这个机制相当于留了一把后门钥匙生产环境要严格控制持有者并且尽量在权限事故时再使用。4. 第三点ACL 的继承与作用边界4.1 子节点的 ACL 是“创建时拷贝”不是“动态继承”这是我认为 Zookeeper 权限机制里最反直觉、也是最容易出问题的一点。很多人在设计 ACL 时以为“给 /app 配好权限/app 下面所有子节点自动受保护”实际上完全不是这样。ACL 的传递规则是创建子节点时服务器会把父节点当前的 ACL 完整复制给子节点作为子节点的初始 ACL。之后父节点的 ACL 再变化和已经存在的子节点没有任何关系。也就是说这个传递是一次性的、创建时发生的。举例# 父节点初始是开放权限 create /open v1 # 在 /open 下创建子节点子节点继承开放 ACL create /open/data d1 # 收紧父节点权限 setAcl /open digest:admin:xxxx:cdrwa # 此时 /open 需要认证但 /open/data 依然是 world:anyone:cdrwa等于裸奔 getAcl /open/data这个特性在生产环境会造成一个真实的“权限漏洞”你以为收紧了目录权限但历史子节点仍然对外开放数据照样可以被任何人读取。4.2 改父节点 ACL 不影响已有子节点的实际案例我经历过一次线上事故运维把某个配置中心的根路径 /config 从开放权限改成了只允许服务账号访问改完之后用监控脚本一测发现 /config 下的子路径仍然可以被匿名客户端读取。原因就是那些子路径是之前创建的继承的是旧的开放 ACL。排查思路很简单先看根节点再看子节点逐层getAcl。结果果然父节点是digest:...子节点还是world:anyone:cdrwa。后面我们用脚本递归把所有子节点的 ACL 统一刷新成和父节点一致才把漏洞补上。所以在设计权限体系时不要把“父节点控制子节点”当成理所当然应该在建目录结构之前就规划好 ACL或者在初始化脚本里对所有需要保护的路径递归设置 ACL。4.3 CREATE/DELETE 权限的检查点和常见误区结合刚才说的权限位实际操作中有几个容易踩坑的点在 /app 下创建子节点检查的是 /app 的 CREATE 权限而不是新节点的 ACL。新节点的 ACL 会被初始化为 /app 的 ACL。删除 /app/data 这个节点理论上需要 /app 有 DELETE 权限因为删除操作会修改 /app 的子节点列表同时被删节点自身有对应权限会更稳妥不同版本和不同客户端行为上会有差异。实际操作中建议父节点和子节点的 DELETE 权限都放开避免踩边界情况。使用deleteall递归删除时沿途涉及的所有节点的 DELETE 权限都会参与检查任何一个不满足都会导致整个删除失败并且可能只删了一部分。这些细节看起来琐碎但在做批量清理任务时经常遇到建议提前在测试环境把“只给父节点 DELETE、不给子节点 DELETE”这类组合验证一遍搞清你所用版本的实际行为。4.4 临时节点与权限一个容易被忽略的细节临时节点EPHEMERAL和普通持久节点的 ACL 机制本身没有区别但有个天然限制会让 CREATE 权限变得没有意义临时节点不能创建子节点。也就是说某个临时节点即使配置了create权限它也不可能有子节点这个权限位实际上不会产生任何作用。如果为了保险给临时节点全量权限也没问题但心里要有数CREATE 在临时节点上是冗余的。另一个和临时节点相关的坑是临时节点由客户端会话生命周期管理如果你用认证后创建的临时节点做分布式锁客户端断线后临时节点会被删除但 ACL 配置不会因为节点删除而自动清理。如果锁路径本身是通过父节点 ACL 控制的只要父节点还在后续新会话都要按父节点 ACL 重新认证这点在实现分布式锁时要提前设计好。5. 实战安装、命令行到 Hadoop 集成的落地5.1 本地安装完第一步就该做的权限检查不管你是 Linux 还是 Windows 环境Zookeeper 装好后的权限验证路径都是一样的。Linux 直接用bin/zkCli.shWindows 用bin/zkCli.cmd。装完先别急着建业务节点先跑一组基础检查# 查看根节点 ACL getAcl / # 建一个测试节点观察默认 ACL create /demo hello getAcl /demo如果根节点是world:anyone:cdrwa说明集群还处于完全开放状态。本地开发无所谓但如果你是照着教程准备在生产部署这一步就该停下来考虑权限规划了。5.2 zkCli 里完整操作一遍 digest 权限配置下面是一组可以直接照抄的命令序列完整演示从开放节点到受保护节点的转换# 1. 连接 bin/zkCli.sh -server 127.0.0.1:2181 # 2. 创建一个开放节点 create /app app root # 3. 添加认证身份 addauth digest admin:admin123 # 4. 修改 /app 的 ACL只允许当前认证用户 setAcl /app auth::cdrwa # 5. 验证当前会话可以正常访问 get /app # 6. 退出当前会话重新连接不执行 addauth quit bin/zkCli.sh -server 127.0.0.1:2181 # 7. 直接访问 /app应该报 NoAuth get /app看到 NoAuth 不要慌往回走一步执行addauth digest admin:admin123再get /app就通了。这套流程能让你直观感受到认证和 ACL 的配合关系。5.3 Java API 里设置 ACL 的代码与关键参数生产环境的客户端大多用 Java用 ZooKeeper 原生的 API 设置 ACL 也很直接。关键点在于先添加认证信息再创建节点// 创建 ZK 客户端 ZooKeeper zk new ZooKeeper(127.0.0.1:2181, 10000, null); // 添加 digest 认证信息必须在使用受保护节点前执行 zk.addAuthInfo(digest, admin:admin123.getBytes()); // 方式一使用 auth scheme由服务器替换为当前认证身份 ListACL aclAuth Collections.singletonList( new ACL(ZooDefs.Perms.ALL, new Id(auth, )) ); // 方式二直接指定 digest id ListACL aclDigest Collections.singletonList( new ACL(ZooDefs.Perms.ALL, new Id(digest, admin:8/2yPdc/pO0ZQm3v7qgM4pC1G6Q:0NThhZjEwMDA)) ); String path zk.create(/app/java, data.getBytes(), aclAuth, CreateMode.PERSISTENT);需要特别提醒的是addAuthInfo添加的是当前连接会话的认证信息不会持久化保存。客户端每次重连都要重新调用一次addAuthInfo。如果你用的是 Curator 这类高级客户端可以在构建客户端时统一配置认证信息避免遗漏。5.4 Hadoop 集成场景给 HA 状态节点补权限Hadoop 和 Zookeeper 整合实战中最典型的是 NameNode HA 用 Zookeeper 做 ActiveStandbyElector会在 /hadoop-ha 等路径下创建临时节点。默认情况下Hadoop 进程使用 Zookeeper 客户端时并不会主动做 digest 认证如果你的 Zookeeper 集群已经启用了 ACLHadoop 就会因为 NoAuth 无法创建临时节点导致 HA 状态选举失败。解决方法有两种给 Hadoop 使用的根路径保留world:anyone的读写权限简单但不够安全。为 Hadoop 服务创建专用 digest 账号在 Hadoop 的 core-site.xml 的 ZooKeeper 相关配置中把认证信息传给 Zookeeper 客户端。实际配置时很多发行版已经支持在配置里追加 Zookeeper 认证信息。重点是把专用账号的密码配在受控的配置文件中不要硬编码到代码仓库或命令行。给 Hadoop 的节点授权时一般给到cdrwa完整权限避免因为权限位缺失导致状态节点无法创建或删除。6. 常见问题排查与避坑清单6.1 一张表理清“权限不足”类报错我在群里见过最多的求助就是KeeperErrorCode NoAuth其实报错背后的原因可以归纳成几类整理成速查表报错/现象常见原因排查方向NoAuth for /xxx当前会话没有添加认证信息或认证身份与 ACL 不匹配先 getAcl 看要求再 addauth 验证InvalidACL for /xxxACL 格式错误尤其是 digest 密文格式不对检查是否完整拷贝了哈希:盐值可以 get 但 setData 失败缺少 WRITE 权限检查 ACL 是否包含w可以 getData 但 getChildren 失败缺少 READ 权限注意 getChildren 也算读操作给对应身份补rsetAcl 失败缺少 ADMIN 权限检查是否有a权限位删除节点失败或部分删除DELETE 权限检查不过逐层确认父节点和子节点的 DELETE 权限6.2 排查权限问题的三条命令组合遇到权限问题我的排查顺序永远是固定的三条命令# 1. 看节点要求什么身份 getAcl /目标节点 # 2. 看自己能不能读数据 get /目标节点 # 3. 添加认证后重试 addauth digest 用户名:密码 get /目标节点先getAcl的目的是确认目标 ACL 里有哪些身份、哪些权限再判断当前会话缺的是认证还是授权。如果addauth之后get成功说明问题出在认证如果addauth之后仍然 NoAuth可能是密码不对也可能是 ACL 里的用户名和当前添加的用户名不一致。用这套顺序90% 的权限问题十分钟内能定位。6.3 我踩过的一些坑认证信息不持久、递归 ACL 不同步第一个坑是客户端认证信息不持久。很多同学在命令行里执行了addauth验证通过后以为万事大吉结果写 Java 客户端时忘了addAuthInfo一上线就 NoAuth。记住Zookeeper 服务端保存的是会话级别的认证信息会话断了就没了客户端每次连接都要重新认证。第二个坑是递归 ACL 不同步。前面反复强调父节点改了 ACL历史子节点不会自动跟着变。做批量授权时千万别只改根路径要写递归脚本把整棵子树都扫一遍。如果用的是 Curator可以遍历子节点后逐个setACL如果不会写程序也可以用 Zookeeper 自带的命令行工具配合循环脚本处理。第三个坑是生产环境临时放开权限后忘记收回来。遇到紧急故障很多人图省事直接setAcl /节点 world:anyone:cdrwa恢复后又不改回去等于长期留了个大口子。建议每次临时改 ACL 都记在运维变更记录里处理后立刻恢复授权。6.4 给后来者的权限设计建议根据我个人的实际经验Zookeeper 权限机制真正让人头疼的往往不是命令不会敲而是初期没有做规划。比较推荐的做法是根路径下按业务域划分目录例如 /app/serviceA、/app/serviceB一开始就确定归属。每个业务域使用独立的 digest 账号不要所有服务共用一个账号否则排查问题时很难定位是谁在操作。根路径可以保持world:anyone:r只读让业务方能够发现目录结构具体业务数据路径再收紧到对应账号的完整权限。把 ACL 初始化写进部署脚本每次新建环境自动执行避免手工敲命令漏配。最后分享一个小技巧在 zkCli 里创建节点时如果你想快速把权限绑定给当前登录账号就用auth::cdrwa这种写法让服务器自动替换认证身份比手写 digest 密文省事而且不容易错。但如果是给别的账号授权还是老老实实生成密文填进去不要图省事把密码明文写进 ACL 里。