1. 从一次配置泄露事故说起为什么ACL不是可选项去年帮一个朋友排查线上问题他们的配置中心里数据库密码被明文拉取到了一个测试环境的日志里。排查下来原因很简单Zookeeper集群没有开ACL任何能连上2181端口的客户端都能读取/config下的所有节点。这件事之后我把手上所有用SpringCloud Zookeeper做配置中心的项目都过了一遍ACL配置。SpringCloud Zookeeper Config这套东西用起来确实舒服——spring-cloud-starter-zookeeper-config一引bootstrap.yml里写个connect-string配置就自动从Zookeeper的/config路径下加载进来了。但舒服的背后有个容易被忽略的事实Zookeeper默认是完全没有访问控制的。它不像MySQL有账号密码不像Redis好歹有个requirepassZookeeper的默认行为就是谁连上谁说了算。这就带来一个很现实的问题在微服务架构里配置中心往往存放着数据库连接串、第三方API密钥、加密盐值这类高敏感信息。如果Zookeeper集群暴露在内网但缺乏ACL那么任何一个能访问到该网段的进程——包括被入侵的容器、误配置的测试服务、甚至是开发同学本地跑的一个demo——都能把生产配置完整读走。ACL在Zookeeper里解决的就是这个问题它给每个znode挂上一组权限规则规定谁能对这个节点做什么操作。而SpringCloud Zookeeper Config要做的是把这套ACL机制和Spring的配置加载流程对接起来让配置的读取和写入都经过认证。这篇文章会从Zookeeper ACL的底层模型讲起然后一步步落到SpringCloud Zookeeper Config的实际配置上包括认证信息的注入、节点的权限规划、以及我在实际项目中踩过的几个坑。不管你是刚开始用Zookeeper做配置中心还是已经在生产环境跑了一段时间想补上安全这块应该都能找到有用的东西。2. Zookeeper ACL的权限模型Scheme、ID与Permission的三元组2.1 ACL的本质是一个三元组Zookeeper的ACL模型比很多人想象的要简单它的核心就是一个三元组scheme:ID:permission。理解了这个三元组后面所有的配置都是它的组合。Scheme是认证方式决定了用什么方式证明你是谁。Zookeeper内置了几种schemeworld最开放的只有一个ID叫anyone表示所有人。world:anyone:r就是任何人可读。auth不使用任何ID表示所有已通过认证的用户。这个scheme比较特殊它不指定具体ID而是把权限授予当前会话中所有已认证的身份。digest用用户名和密码做认证ID格式是username:base64(sha1(username:password))。这是最常用的方式。ip按IP地址做认证ID就是IP或者IP段比如192.168.1.0/24。sasl用SASL做认证适合已经有一套Kerberos体系的环境。ID是具体身份标识格式取决于scheme。比如digest scheme下ID就是user:hash的形式。Permission是权限位Zookeeper定义了五种权限简写含义CREATEc可以在该节点下创建子节点READr可以读取该节点的数据和子节点列表WRITEw可以修改该节点的数据DELETEd可以删除该节点的子节点ADMINa可以设置该节点的ACL这里有个容易搞混的点DELETE权限管的是删除子节点不是删除自己。一个节点自己的删除权限是由它父节点的DELETE权限决定的。这个设计逻辑是你对一个节点的子节点有删除权才能删掉它下面的东西。2.2 digest认证的密码是怎么算出来的digest scheme的ID格式是username:Base64(SHA1(username:password))。注意这里有个细节哈希的输入是username:password这个整体字符串不是只哈希password。举个例子用户名admin密码secret123那么SHA1(admin:secret123) 一串20字节的摘要 Base64(那串摘要) 最终的hash部分最终ID就是admin:xxxxxxxxxxxxxxxxxxxxxxxxxxx。这个计算过程在实际操作中经常需要手动做因为Zookeeper的命令行工具zkCli.sh在添加ACL时如果你用addauth digest admin:secret123它会自动帮你算hash。但如果你要在配置文件里直接写ACL就得自己算好。用命令行算的话echo -n admin:secret123 | openssl dgst -binary -sha1 | openssl base64或者用Pythonimport hashlib, base64 def digest_id(username, password): raw f{username}:{password}.encode() sha1 hashlib.sha1(raw).digest() return f{username}:{base64.b64encode(sha1).decode()}注意echo -n的-n不能省否则会把换行符也算进哈希输入算出来的hash和Zookeeper内部算的对不上。2.3 ACL的继承规则子节点不会自动继承这是Zookeeper ACL设计里最容易被误解的一点ACL不会从父节点继承到子节点。你在/config上设了ACL不代表/config/application会自动有同样的ACL。每个znode的ACL是独立设置的。当你创建一个子节点时如果没有显式指定ACLZookeeper会用默认ACL也就是world:anyone:cdrwa——完全开放。这意味着一个很危险的场景你在/config根节点上精心设置了digest认证但SpringCloud Zookeeper Config在启动时如果发现/config/application不存在它会自动创建这个节点而创建时用的是默认ACL。结果就是根节点有保护子节点全裸奔。正确的做法是要么预先手动创建好所有需要的节点并设置ACL要么在客户端创建节点时显式传入ACL。SpringCloud Zookeeper Config在这一点上提供了配置项后面会详细讲。3. SpringCloud Zookeeper Config的ACL接入点认证信息往哪塞3.1 CuratorFramework的ACL支持SpringCloud Zookeeper底层用的是Apache Curator来操作Zookeeper。Curator对ACL的支持主要通过两个地方一是CuratorFrameworkFactory构建时传入authorization信息这相当于在连接建立后自动执行addauth。二是创建节点时通过CreateBuilder的withACL()方法指定ACL列表。SpringCloud Zookeeper的自动配置类ZookeeperAutoConfiguration在构建CuratorFramework时会读取配置属性来决定是否注入认证信息。具体来说它看的是ZookeeperProperties里的authInfo字段。3.2 在bootstrap.yml里配置认证信息最直接的方式是在bootstrap.yml注意是bootstrap不是application因为配置中心的连接信息要在应用上下文启动早期就可用里加spring: cloud: zookeeper: connect-string: 10.0.0.1:2181,10.0.0.2:2181,10.0.0.3:2181 auth-info: admin:secret123这里auth-info的格式是username:password的明文形式。SpringCloud Zookeeper在构建Curator客户端时会把它转成digest认证信息注入进去。但这里有个问题明文密码写在配置文件里。如果配置文件本身被提交到了代码仓库或者被不具备权限的人看到那ACL就形同虚设了。所以生产环境一般不会这么干。3.3 用环境变量或启动参数注入更稳妥的做法是把认证信息从配置文件里抽出来通过环境变量传入spring: cloud: zookeeper: connect-string: ${ZK_CONNECT:10.0.0.1:2181} auth-info: ${ZK_AUTH:}然后在部署时通过环境变量ZK_AUTHadmin:secret123注入。这样配置文件里看不到密码代码仓库里也不会泄露。如果是用Kubernetes部署可以把密码放在Secret里然后通过envFrom或者valueFrom注入到容器环境变量。如果是用传统的虚拟机部署可以在启动脚本里从密钥管理服务拉取后export。提示auth-info这个属性在SpringCloud Zookeeper的不同版本里名字可能有差异。早期版本用的是spring.cloud.zookeeper.auth-info后来有些版本改成了spring.cloud.zookeeper.auth.info或者需要通过ZookeeperProperties的自定义扩展来实现。建议先确认自己用的版本再看对应的配置项名称。如果发现配置不生效可以打开org.apache.curator的DEBUG日志看连接建立后有没有执行addAuth操作。3.4 认证信息注入的时机问题这里有一个实际踩过的坑SpringCloud Zookeeper Config在应用启动时会先连接Zookeeper去拉取配置然后用拉到的配置来构建应用上下文。但认证信息的注入必须发生在第一次读取操作之前。如果auth-info配置在bootstrap.yml里那没问题因为bootstrap上下文比application上下文先启动。但如果有人把auth-info写在了application.yml里那就会出问题——因为application.yml本身可能就是从Zookeeper拉下来的而拉取的时候还没认证直接就被拒了。所以记住一个原则所有跟Zookeeper连接和认证相关的配置必须放在bootstrap.yml或者通过启动参数/环境变量传入不能放在application.yml里。4. 节点权限的规划从/config根节点到每个应用路径4.1 典型的节点结构SpringCloud Zookeeper Config默认使用的根路径是/config。在这个根下面它按应用名和profile来组织节点。典型的路径结构是这样的/config /application # 所有应用共享的配置 /default # 默认profile /dev # dev profile /my-service # 特定应用的配置 /default /prod每个叶子节点比如/config/my-service/prod里存放的是具体的配置数据通常是JSON或者properties格式的字符串。4.2 权限规划的实际方案在实际项目中我一般会把权限分成三类角色配置管理员对所有节点有全部权限cdrwa。一般是运维或者配置管理平台的服务账号。应用服务账号对/config/application和/config/{自己的应用名}有读权限r对/config/{自己的应用名}有写权限w——有些场景下应用需要动态更新自己的配置。但一般不给CREATE和DELETE权限防止应用误创建或误删节点。只读监控账号对所有节点只有读权限r用于监控系统拉取配置做审计。对应的ACL设置大概是# 配置管理员 digest:config-admin:HASH:cdrwa # 应用服务账号以my-service为例 digest:my-service:HASH:rw # 监控账号 digest:monitor:HASH:r4.3 用脚本批量设置ACL手动用zkCli.sh一个个节点设ACL太累了而且容易漏。我一般会写一个脚本遍历/config下的所有节点统一设置ACL。用Python的kazoo库写一个from kazoo.client import KazooClient from kazoo.security import make_digest_acl zk KazooClient(hosts10.0.0.1:2181) zk.start() zk.add_auth(digest, admin:secret123) def set_acl_recursive(path, acls): try: zk.set_acls(path, acls) except Exception as e: print(fset acl failed on {path}: {e}) for child in zk.get_children(path): set_acl_recursive(f{path}/{child}, acls) acls [ make_digest_acl(config-admin, admin-pass, allTrue), make_digest_acl(my-service, svc-pass, readTrue, writeTrue), make_digest_acl(monitor, mon-pass, readTrue), ] set_acl_recursive(/config, acls) zk.stop()这个脚本的逻辑是递归遍历对每个节点设置相同的ACL。实际使用时可以根据路径做差异化处理比如/config/application只给读权限/config/my-service给读写权限。注意set_acls操作需要当前会话对目标节点有ADMIN权限。如果你已经设了ACL但忘了给自己留ADMIN那就锁死了。所以第一次设置ACL时一定要确保管理员账号有a权限。4.4 创建节点时的ACL指定前面提到SpringCloud Zookeeper Config在节点不存在时会自动创建。如果不想让它用默认的开放ACL创建有两个办法一是在应用启动前用脚本预先创建好所有需要的节点并设好ACL。这样应用启动时发现节点已存在就不会触发创建逻辑。二是通过Curator的CreateBuilder自定义ACL。SpringCloud Zookeeper Config本身没有直接暴露这个配置项但可以通过自定义CuratorFramework的Bean来干预。具体做法是定义一个CuratorFramework的Bean在构建时设置默认的ACLBean public CuratorFramework curatorFramework(ZookeeperProperties properties) { CuratorFrameworkFactory.Builder builder CuratorFrameworkFactory.builder() .connectString(properties.getConnectString()) .retryPolicy(new RetryNTimes(3, 1000)); if (StringUtils.hasText(properties.getAuthInfo())) { builder.authorization(digest, properties.getAuthInfo().getBytes()); } CuratorFramework client builder.build(); client.start(); return client; }但这种方式下创建节点时的ACL还是需要额外处理。更彻底的做法是自定义一个ZookeeperConfigProperties或者覆盖ConfigDataLoader在创建节点时传入ACL列表。这部分涉及SpringCloud Zookeeper的内部实现改动量较大一般项目里用预创建节点的方式就够了。5. 踩坑实录ACL配置中那些让人抓狂的细节5.1 auth-info格式不对导致认证静默失败最开始配的时候我在bootstrap.yml里写的是spring: cloud: zookeeper: auth-info: admin:secret123结果应用启动后读配置一直报NoAuth异常。查了半天发现SpringCloud Zookeeper在解析auth-info时期望的格式其实是scheme:username:password也就是要带上scheme前缀。正确的写法是spring: cloud: zookeeper: auth-info: digest:admin:secret123但这里又有个版本差异有些版本的ZookeeperProperties里authInfo字段的解析逻辑是直接按username:password处理的scheme硬编码为digest。所以到底要不要带digest:前缀取决于你用的版本。我的建议是先看源码里ZookeeperProperties.getAuthInfo()被怎么用的或者直接试两种格式看哪种能通。5.2 节点已存在但ACL不对应用读不到配置有一次在测试环境应用突然读不到配置了报KeeperErrorCode NoAuth。排查发现是有人手动用zkCli.sh改了/config/my-service的ACL把应用的账号权限去掉了。这里的问题是Zookeeper的ACL修改是立即生效的而且没有回滚机制。一旦某个账号的权限被移除正在运行的会话如果之前已经通过了认证可能还能继续操作取决于会话状态但新建立的连接就会被拒。所以修改生产环境ACL时一定要走变更流程先确认新ACL里包含了所有需要的账号和权限再执行修改。修改后立即用各个角色的账号验证一遍读写权限。5.3 SpringCloud Zookeeper Config的watcher在ACL变更后失效SpringCloud Zookeeper Config依赖Curator的watcher来监听配置变化。当配置节点数据变化时watcher触发应用重新拉取配置。但如果ACL变更导致当前会话失去读权限watcher可能会收到NoAuth异常而且不会自动恢复。这意味着如果你在应用运行期间修改了ACL把应用的读权限去掉了应用不仅读不到新配置连配置变更的通知都收不到了。更麻烦的是这种失败是静默的——应用不会崩溃只是配置永远停留在旧版本。所以ACL变更后稳妥的做法是滚动重启所有相关应用让它们用新的认证信息重新建立连接和watcher。5.4 容器化部署时的IP scheme陷阱有些团队会用ipscheme来做ACL比如只允许特定网段的机器访问。这在虚拟机部署时没问题但在容器化环境里就是个坑容器的IP是动态分配的每次重启可能都不一样。用IP做ACL要么频繁改ACL要么就得把整个容器网段都放开失去了精细控制的意义。容器环境下还是建议用digest scheme每个应用一个独立的账号密码通过Secret注入。这样即使容器IP变了认证信息不变ACL也不用动。6. 验证与排查怎么确认ACL真的生效了6.1 用zkCli.sh验证最直接的验证方式是用zkCli.sh连上去分别用不同账号认证然后尝试读写# 连接 zkCli.sh -server 10.0.0.1:2181 # 不认证直接读应该被拒 get /config/my-service/prod # 用应用账号认证 addauth digest my-service:svc-pass get /config/my-service/prod # 应该成功 set /config/my-service/prod test # 如果有写权限应该成功 # 尝试访问其他应用的配置应该被拒 get /config/other-service/prod如果get返回null或者抛NoAuth说明ACL生效了。如果返回了数据说明ACL没配对。6.2 查看节点的ACL列表用getAcl命令可以查看某个节点当前的ACLgetAcl /config/my-service输出会列出所有的scheme:ID:permission条目。检查一下是否包含了你期望的账号和权限。6.3 应用侧的日志排查如果应用启动时报NoAuth打开Curator的DEBUG日志logging: level: org.apache.curator: DEBUG org.apache.zookeeper: DEBUG日志里会显示连接建立后是否执行了addAuth以及具体的认证结果。如果看到Authentication failed或者NoAuth说明认证信息不对。另一个排查点是确认auth-info配置真的被加载了。可以在启动时打印一下ZookeeperProperties的authInfo字段值注意脱敏看是不是空。6.4 一个实用的ACL自检脚本我写了一个简单的自检脚本在应用启动前跑一遍确认当前账号对所需路径有正确的权限from kazoo.client import KazooClient from kazoo.exceptions import NoAuthError def check_access(hosts, auth, paths): zk KazooClient(hostshosts) zk.start() zk.add_auth(digest, auth) for path in paths: try: zk.get(path) print(fREAD OK: {path}) except NoAuthError: print(fREAD DENIED: {path}) except Exception as e: print(fERROR on {path}: {e}) zk.stop() check_access( 10.0.0.1:2181, my-service:svc-pass, [/config/application/prod, /config/my-service/prod] )这个脚本可以集成到CI/CD流程里部署前自动检查权限配置是否正确避免上线后才发现读不到配置。7. 生产环境的ACL管理策略从手动到自动化7.1 账号体系的设计在生产环境里我建议按应用环境的维度来划分账号。比如svc-myapp-prodmyapp在生产环境的账号svc-myapp-devmyapp在开发环境的账号admin-config配置管理员的账号monitor-readonly监控系统的只读账号每个账号的密码不同通过密钥管理服务统一管理。应用部署时从密钥服务拉取对应的密码注入环境变量。这样做的好处是权限边界清晰一个应用的账号泄露不会影响其他应用审计时能追溯到具体是哪个应用的操作。7.2 ACL变更的流程化ACL变更不能随手改要有流程在配置管理平台提交ACL变更申请说明变更原因和影响范围。自动检查新ACL是否包含了所有必要的账号和权限可以用上面的自检脚本。在预发环境验证通过后再应用到生产。变更后滚动重启相关应用确保watcher重新建立。记录变更日志便于回溯。7.3 定期审计Zookeeper本身不提供ACL变更的审计日志但可以通过定期dump所有节点的ACL和基线做对比发现异常变更。用zkCli.sh的getAcl命令配合脚本可以定期导出ACL快照#!/bin/bash ZK_SERVER10.0.0.1:2181 OUTPUTacl_snapshot_$(date %Y%m%d).txt echo getAcl /config | zkCli.sh -server $ZK_SERVER $OUTPUT # 递归遍历所有子节点...把快照存起来和上一次的做diff如果有非预期的变更就告警。8. 一些个人体会ACL这个东西配起来不复杂但细节特别多。我踩过的坑里大部分不是因为不懂原理而是因为版本差异、配置项名称不一致、以及以为配了其实没生效。几个我觉得最值得记住的点第一认证信息必须放在bootstrap阶段能读到的地方不能放在application.yml里。这个坑我见过至少三个团队踩过。第二ACL不会继承每个节点都要单独设。自动创建的节点默认是开放的要么预创建要么自定义创建逻辑。第三改ACL后要重启应用因为watcher可能已经失效了而且这种失效是静默的不重启你根本不知道。第四容器环境别用IP scheme用digest每个应用独立账号。最后如果你现在还没配ACL建议至少先把/config根节点和所有应用节点的ACL加上哪怕先用一个统一的账号。有总比没有强后面再慢慢细化到按应用分账号。安全这件事从来都是先有防线再谈精细。
阅读完成 · 觉得有帮助?