ZooKeeper安全认证机制:ZNode ACL
ZooKeeper的Client-Server互认证机制是从3.4.0版本开始引入的,本文主要介绍znodes的ACL的定义,任务服务接口定义与几种已有的认证服务实现,以及ACL与多种认证服务是如何建立联系的。本文内容基于ZooKeeper 3.5.1版本。
ACL
ZooKeeper的ACL可针对znodes
设置相应的权限信息。ACL数据的表示格式为:schema:id:permissions
- schema 支持的几种schema为:
- world
只有一个名为
anyone
的Id
,world:anyone
代表任何人,也就是说,对应节点任何人可访问- auth
代表任何通过认证的用户,该schema不需要配置
Id
信息- digest
基于
username:password
生成的MD5 Hash值作为Id
信息,认证基于username:password
明文认证,但在acl中存储的是username:base64(password)
- ip
基于IP地址作为
Id
,支持IP地址或IP地址段 - id 代表用户
- permissions 权限定义为(READ, WRITE, CREATE, DELETE, ADMIN, ALL)
由ACL的定义信息,可以看出来,ZooKeeper可以针对不同的znodes
来提供不同的认证机制。
AuthenticationProvider
每一种认证服务均需要实现AuthenticationProvider
接口来支持一种新的schema,所有的AuthenticationProvider
实现类都被注册在ProviderRegistry
中。ZooKeeper中已经提供的AuthenticationProvider`的实现类:
每一个AuthenticationProvider
实现类所关联的schema
如下所示:
DigestAuthenticationProvider | digest |
IPAuthenticationProvider | ip |
SASLAuthenticationProvider | sasl |
X509AuthenticationProvider | x509 |
当znode acl schema为world
时,是不需要经任何AuthenticationProvider
进行认证的,因此不需要任何实现类。
当znode acl schema为auth
时,代表着需要对请求上下文中的认证信息进行校验,在ServerCnxn
的authInfo
中保存了所有的已认证成功的Id
以及认证服务所关联的的schema
,由该schema
再去ProviderRegistry
中查找所关联的AuthenticationProvider
实现类来对认证信息进行校验。
除了上述已有的实现者以外,用户还可以自定义实现AuthenticationProvider
。自定义的实现类,需要设置到System Properties中,对应的Property Key
需以"zookeeper.authProvider."
开头。另外,自定义的AuthenticationProvider
的schema
名称不应与现有的重名,否则会覆盖现有的实现。
Reference
- Client-Server Mutual Authentication
- ZOOKEEPER-938
ZooKeeper安全认证机制:用户名密码认证
ZooKeeper提供了简单的基于用户名和密码的认证机制,即DIGEST-MD5认证机制。本文首先介绍使用该认证机制所涉及的一些配置细节,接下来介绍ZooKeeper内部关于DIGEST-MD5认证机制的一些实现细节。
如何使用
Client
系统属性配置:
// "zookeeper.sasl.clientconfig"如果不设置,默认值为"Client" System.setProperty("zookeeper.sasl.clientconfig", "Client"); System.setProperty("zookeeper.sasl.client", "true");
自定义一个JaasConf对象,继承自javax.security.auth.login.Configuration,目的是为了便于Configuration所需参数的配置:
public class JaasConf extends Configuration { private Mapsections = new HashMap (); public void addSection(String name, String loginModuleName, String... args) { Map options = new HashMap (); for (int i = 0; i < args.length; i += 2) { options.put(args[i], args[i + 1]); } AppConfigurationEntry[] entries = new AppConfigurationEntry[]{ new AppConfigurationEntry(loginModuleName, AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options)}; this.sections.put(name, entries); } @Override public AppConfigurationEntry[] getAppConfigurationEntry(String name) { return this.sections.get(name); } }
实例化JaasConf,设置LoginModuleName以及对应的username/password等信息:
JaasConf conf = new JaasConf(); // Section Name: "Client", 这里的名称与系统属性"zookeeper.sasl.clientconfig"保持一致 // LoginModule Name: "org.apache.zookeeper.server.auth.DigestLoginModule" // Options: // "username": "nosql" // "password": "nosql123" conf.addSection("Client", "org.apache.zookeeper.server.auth.DigestLoginModule", "username", "nosql", "password", "nosql123"); Configuration.setConfiguration(conf);
Server
系统属性配置:
System.setProperty("zookeeper.sasl.serverconfig", "Server"); System.setProperty("zookeeper.authProvider.sasl", "org.apache.zookeeper.server.auth.SASLAuthenticationProvider");
实例化JaasConf,并在Server端配置所有允许访问的username/password信息:
JaasConf conf = new JaasConf(); // LoginModuleName: "org.apache.zookeeper.server.auth.DigestLoginModule" // Options: // "user_nosql: nosql123" conf.addSection("Server", "org.apache.zookeeper.server.auth.DigestLoginModule", "user_nosql", "nosql123"); Configuration.setConfiguration(conf);
可以看到,Client端与Server端配置username/password的参数名称是不同的:
- Client 用户名通过静态参数”username“指定,密码通过静态参数”password“指定
- Server 用户名直接配置在一个以”user_“开头的动态参数名中,参数值直接为对应的password
Client通过这种模式只能配置一个username/password,而Server端的动态参数则允许配置多个Client的username/password。原因在于,Client只需要配置一个username/password即可,而Server端则允许配置多个Client的username/password。
实现原理
整体思路
- Server端在初始化ServerCnxnFactory时,加载预先配置的允许访问的一个或多个username/password列表,并执行Login操作
- Client基于配置的username/password以及DigestLoginModule,执行Login操作
- Client请求与Server端建立Sasl连接,建立连接过程中,通过com.sun.security.sasl.digest.FactoryImpl提供的认证机制,完成对username/password的合法校验
Client初始化
ZooKeeperSaslClient初始化时:
if (login == null) { if (LOG.isDebugEnabled()) { LOG.debug("JAAS loginContext is: " + loginContext); } // 初始化Login对象,Login对象是static类型的,也就说,该对象在进程级别内 // 是共享的. Login对象利用Java JAAS机制执行login操作,具体的Login机制由 // 配置的LoginContext来实现. login = new Login(loginContext, new ClientCallbackHandler(null)); login.startThreadIfNeeded(); } Subject subject = login.getSubject(); SaslClient saslClient; // ZooKeeper支持的认证主要是GSSAPI(Kerberos)以及DIGEST-MD5. 如果基于GSSAPI, // 认证成功后会在Subject中添加对应的Principal信息. 如果Subject中的Principal // 信息为空,则认为要使用DIGEST-MD5认证(注: 这种设计并不太好) if (subject.getPrincipals().isEmpty()) { // no principals: must not be GSSAPI: use DIGEST-MD5 mechanism instead. LOG.info("Client will use DIGEST-MD5 as SASL mechanism."); String[] mechs = {"DIGEST-MD5"}; // 从subject中获取username与password信息 String username = (String)(subject.getPublicCredentials().toArray()[0]); String password = (String)(subject.getPrivateCredentials().toArray()[0]); // 初始化SaslClient时,将username传入,password在ClientCallbackHandler中. // "zk-sasl-md5" is a hard-wired 'domain' parameter shared with // zookeeper server code (see ServerCnxnFactory.java) saslClient = Sasl.createSaslClient(mechs, username, "zookeeper", "zk-sasl-md5", null, new ClientCallbackHandler(password)); return saslClient; }
关于如上源码的更多备注信息:
- Login阶段,已经配置了LoginModule为
org.apache.zookeeper.server.auth.DigestLoginModule
-
DigestLoginModule
中在初始化时已经将Client配置的username和password信息加载到subject中:public void initialize(Subject subject, CallbackHandler callbackHandler, Map
sharedState, Map options) { if (options.containsKey("username")) { // Zookeeper client: get username and password from JAAS conf // (only used if using DIGEST-MD5). this.subject = subject; String username = (String)options.get("username"); this.subject.getPublicCredentials().add((Object)username); String password = (String)options.get("password"); this.subject.getPrivateCredentials().add((Object)password); } return; } - Sasl.createSaslClient的流程:
String mechFilter = "SaslClientFactory." + mechName; Provider[] provs = Security.getProviders(mechFilter); for (int j = 0; provs != null && j < provs.length; j++) { className = provs[j].getProperty(mechFilter); if (className == null) { // Case is ignored continue; } fac = (SaslClientFactory) loadFactory(provs[j], className); if (fac != null) { mech = fac.createSaslClient( new String[]{mechanisms[i]}, authorizationId, protocol, serverName, props, cbh); if (mech != null) { return mech; } } }
“SaslClientFactory.DEGIEST-MD5″所关联的SaslClientFactory实现为:
com.sun.security.sasl.digest.FactoryImpl
所有的SaslClientFactory的实现信息都被注册在java.security.Security中。
Security与ProviderRegistry:
java.security.Security: Java Security框架中的定义,用来注册SaslClientFactory. 每一个SaslClientFactory都关联着一个Name.
org.apache.zookeeper.server.auth.ProviderRegistry: ZooKeeper中自定义的用来注册所有的AuthenticationProvider的类,每一个AuthenticationProvider关联一个schema
Server端初始化
ServerCnxnFactory#configureSaslLogin中的一些关键源码:
String serverSection = System.getProperty("zookeeper.sasl.serverconfig", "Server"); // Note that 'Configuration' here refers to javax.security.auth.login.Configuration. AppConfigurationEntry entries[] = null; SecurityException securityException = null; try { entries = Configuration.getConfiguration().getAppConfigurationEntry(serverSection); } catch (SecurityException e) { // handle below: might be harmless if the user doesn't intend to use JAAS authentication. securityException = e; } // ...中间略去一下非关键源码.... try { // 初始化SaslServerCallbackHandler saslServerCallbackHandler = new SaslServerCallbackHandler(Configuration.getConfiguration()); // 初始化Login对象,利用配置的LoginModule执行login操作. login = new Login(serverSection, saslServerCallbackHandler); login.startThreadIfNeeded(); } catch (LoginException e) { // .... }
SaslServerCallbackHandler
初始化过程中,加载配置的一个或多个username/password信息:
public SaslServerCallbackHandler(Configuration configuration) throws IOException { String serverSection = System.getProperty("zookeeper.sasl.serverconfig", "Server"); AppConfigurationEntry configurationEntries[] = configuration.getAppConfigurationEntry(serverSection); if (configurationEntries == null) { String errorMessage = "Could not find a 'Server' entry in" + " this configuration: Server cannot start."; LOG.error(errorMessage); throw new IOException(errorMessage); } credentials.clear(); for(AppConfigurationEntry entry: configurationEntries) { Mapoptions = entry.getOptions(); // 所有的用户名都被配置在以"user_"为前缀的属性名中 for(Map.Entry pair : options.entrySet()) { String key = pair.getKey(); if (key.startsWith(USER_PREFIX)) { String userName = key.substring(USER_PREFIX.length()); credentials.put(userName,(String)pair.getValue()); } } } }
总结
该机制虽然实现了基于用户名和密码的简单认证机制,但所有的用户名和密码信息都是静态配置的,无法支持用户的动态增加,这是该方案的最大软肋。
ZooKeeper安全认证机制:SSL
本文探讨ZooKeeper的SSL安全机制。默认情形下,ZooKeeper的网络通信是没有加密的,但ZooKeeper提供了SSL特性,目前仅应用在Client与Server端之间的交互(Server与Server之间的交互尚不支持),且RPC通信协议基于Netty时(ZooKeeper内置的NIO实现中不支持)。
SSL简介
SSL全称为Secure Socket Layer
,它是一种介于传输层和应用层的协议,它通过”握手协议”和“传输协议”来解决信息传输的安全问题,它可以被建立在任何可靠的传输层协议之上(例如TCP,但不能是UDP)。SSL协议主要提供如下三方面的能力:
- 信息的加密传播
- 校验机制,数据一旦被篡改,通信双方均会立刻发现
- 身份证书,防止身份被冒充
SSL的基本设计思想:
- Client向Server端索要”公钥“
- Client对获取的”公钥“进行校验
- 双方协商生成“会话密钥”
- 双方基于”会话密钥“进行信息交换
前3步称之为”握手阶段”,”握手阶段”采用”非对称加密“算法。
第4步称之为”传输阶段”,基于”对称加密“算法,”对称加密”算法的性能是远高于”非对称加密”算法的,因此,更适用于大数据量的传输加密。
如何使用
Client端配置
ZooKeeper Client通过配置如下系统属性来启用基于Netty的RPC通信层:
zookeeper.clientCnxnSocket=”org.apache.zookeeper.ClientCnxnSocketNetty”
Client需要设置如下参数来启用安全通信:
zookeeper.client.secure=true
设置了zookeeper.client.secure
属性为true
以后,意味着Client与Server之间只能通过"secureClientPort"
所指定的端口进行交互。
最后,需要配置KeyStore与TrustStore的相关系统属性:
zookeeper.ssl.keyStore.location=”/path/to/your/keystore”
zookeeper.ssl.keyStore.password=”keystore_password”
zookeeper.ssl.trustStore.location=”/path/to/your/truststore”
zookeeper.ssl.trustStore.password=”truststore_password”
Server端配置
ZooKeeper Server通过配置如下系统属性来启用Netty:
zookeeper.serverCnxnFactory=”org.apache.zookeeper.server.NettyServerCnxnFactory”
在”zoo.cfg”中配置”secureClientPort”端口值,该端口值与原来的”clientPort”端口值应该区别开:
secureClientPort=2281
最后也需要设置KeyStore与TrustStore的配置,与Client端配置类似。
配置示例
“bin/zkServer.sh”的配置示例如下:
export SERVER_JVMFLAGS=”
-Dzookeeper.serverCnxnFactory=org.apache.zookeeper.server.NettyServerCnxnFactory
-Dzookeeper.ssl.keyStore.location=/root/zookeeper/ssl/testKeyStore.jks
-Dzookeeper.ssl.keyStore.password=testpass
-Dzookeeper.ssl.trustStore.location=/root/zookeeper/ssl/testTrustStore.jks
-Dzookeeper.ssl.trustStore.password=testpass”
在 “zoo.cfg”中增加:
secureClientPort=2281
“bin/zkCli.sh”的配置为:
export CLIENT_JVMFLAGS=”
-Dzookeeper.clientCnxnSocket=org.apache.zookeeper.ClientCnxnSocketNetty
-Dzookeeper.client.secure=true
-Dzookeeper.ssl.keyStore.location=/root/zookeeper/ssl/testKeyStore.jks
-Dzookeeper.ssl.keyStore.password=testpass
-Dzookeeper.ssl.trustStore.location=/root/zookeeper/ssl/testTrustStore.jks
-Dzookeeper.ssl.trustStore.password=testpass”
X509AuthenticationProvider
默认情况下,SSL认证是由X509AuthenticationProvider
提供的,对应的schema为x509
。X509AuthenticationProvider
基于javax.net.ssl.X509KeyManager
与javax.net.ssl.X509TrustManager
提供Host
的证书认证机制。X509AuthenticationProvider仅仅当zookeeper.serverCnxnFactory
配置为NettyServerCnxnFactory
时才可使用,ZooKeeper内置的NIO实现类NIOServerCnxnFactory
并不支持SSL。
关键的配置项如下所示:
zookeeper.ssl.keyStore.location | KeyStore的路径 |
zookeeper.ssl.trustStore.location | TrustStore的路径 |
zookeeper.ssl.keyStore.password | KeyStore的访问密码 |
zookeeper.ssl.trustStore.password | TrustStore的访问密码 |
在KeyStore JKS文件中保存了Server的证书以及私钥信息,该证书需要由Client端信任,因此,该证书或CA(证书认证机构信息)也会被存储在Client端的TrustStore JKS文件中。同时,Server端的TrustStore JFS文件中存储了所信任的Client的证书/CA信息。
Client认证成功之后,会创建一个ZooKeeper Session,Client可以设置ACLs的schema为”x509″. “x509″使用Client认证成功后的X500 Principal作为ACL ID。 ACL信息中包含Client认证后的确切的X500 Principal名称。
关于X509与 X500:
X509: 一套数字证书体系标准
X500: 定义了一种区别命名规则,以命名树来确保用户名称的唯一性
与digest
认证类似,Server端可以配置一个X509的superUser
,对应的Property Key为:
zookeeper.X509AuthenticationProvider.superUser
superUser
可以绕过ACL配置从而拥有所有znodes的所有权限。
定制X509AuthenticationProvider
除了默认的X509AuthenticationProvider
以外,ZooKeeper允许自定义扩展实现X509的安全信任机制,尤其是Certificate Key Infrastructures不使用JKS时。
自定义实现X509AuthenticationProvider
应该遵循:
- 继承自
X509AuthenticationProvider
- KeyManager需要继承自
javax.net.ssl.X509ExtendedKeyManager
- TrustManager需要继承自
javax.net.ssl.X509ExtendedTrustManager
- 覆写
X509AuthenticationProvider
的getKeyManager
与getTrustManager
方法
这样,自定义的实现才会在SSLEngine中发挥作用。
自定义的AuthenticationProvider需要配置一个对应的schema
名称,并且通过系统属性"zookeeper.authProvider.[schema_name]"
来配置新定义的AuthenticationProvider实现类,这样在ProviderRegistry初始化时会自动加载。接下来,还需要设置系统属性"zookeeper.ssl.authProvider=[schema_name]"
,这样,新定义的AuthenticationProvider才可以被应用在安全认证中。
实现细节
NettyServerCnxnFactory
构造函数中初始化ChannelPipeline时调用初始化SSL的方法:NettyServerCnxnFactory#initSSL
方法的实现如下:CnxnChannelHandler#channelConnected
方法的定义如下:
当SslHandler中的handshake Future中的监听者被触发以后,由CertificateVerifier
来对证书的合法性进行校验,而CertificateVerifier
对证书进行校验的操作是由X509AuthenticationProvider
或者自定义的扩展实现类来完成:
Reference
- Client-Server Mutual Authentication
- ZOOKEEPER-938
- ZOOKEEPER-2125
- ZooKeeper SSL User Guide
- SSL/TLS协议运行机制的概述
http://www.nosqlnotes.com/technotes/zookeeper-acl/
http://www.nosqlnotes.com/technotes/zookeeper-digest-md5/
http://www.nosqlnotes.com/technotes/zookeeper-ssl/