`

ZooKeeper安全认证机制

阅读更多

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

    只有一个名为anyoneIdworld: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实现类
每一个AuthenticationProvider实现类所关联的schema如下所示:

 

AuthenticationProvider实现类 schema
DigestAuthenticationProvider digest
IPAuthenticationProvider ip
SASLAuthenticationProvider sasl
X509AuthenticationProvider x509

当znode acl schema为world时,是不需要经任何AuthenticationProvider进行认证的,因此不需要任何实现类。
当znode acl schema为auth时,代表着需要对请求上下文中的认证信息进行校验,在ServerCnxnauthInfo中保存了所有的已认证成功的Id以及认证服务所关联的的schema,由该schema再去ProviderRegistry中查找所关联的AuthenticationProvider实现类来对认证信息进行校验。
除了上述已有的实现者以外,用户还可以自定义实现AuthenticationProvider。自定义的实现类,需要设置到System Properties中,对应的Property Key需以"zookeeper.authProvider."开头。另外,自定义的AuthenticationProviderschema名称不应与现有的重名,否则会覆盖现有的实现。

Reference

  1. Client-Server Mutual Authentication
  2. 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 Map<String, AppConfigurationEntry[]> sections =
          new HashMap<String, AppConfigurationEntry[]>();
​
  public void addSection(String name, String loginModuleName, String... args) {
    Map<String, String> options = new HashMap<String, String>();
    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;
}

关于如上源码的更多备注信息:

  1. Login阶段,已经配置了LoginModule为org.apache.zookeeper.server.auth.DigestLoginModule
  2. DigestLoginModule中在初始化时已经将Client配置的usernamepassword信息加载到subject中:
    public void initialize(Subject subject, CallbackHandler callbackHandler, 
                           Map<String,?> sharedState, Map<String,?> 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;
    }
  3. 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) {
    Map<String,?> options = entry.getOptions();
    // 所有的用户名都被配置在以"user_"为前缀的属性名中
    for(Map.Entry<String, ?> 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的基本设计思想:

  1. Client向Server端索要”公钥
  2. Client对获取的”公钥“进行校验
  3. 双方协商生成“会话密钥
  4. 双方基于”会话密钥“进行信息交换

前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为x509X509AuthenticationProvider基于javax.net.ssl.X509KeyManagerjavax.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
  • 覆写X509AuthenticationProvidergetKeyManagergetTrustManager方法

这样,自定义的实现才会在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

  1. Client-Server Mutual Authentication
  2. ZOOKEEPER-938
  3. ZOOKEEPER-2125
  4. ZooKeeper SSL User Guide
  5. SSL/TLS协议运行机制的概述

http://www.nosqlnotes.com/technotes/zookeeper-acl/

http://www.nosqlnotes.com/technotes/zookeeper-digest-md5/

http://www.nosqlnotes.com/technotes/zookeeper-ssl/

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics