首页 > BigData > Hadoop, Hbase, Zookeeper安全实践

Hadoop, Hbase, Zookeeper安全实践

2012年12月31日 发表评论 阅读评论

过去的一个月,一直在折腾Hadoop, Hbase, Zookeeper的安全,中间碰到各种坑,在这里做一个简单的总结,希望能够抛砖引玉,与感兴趣的朋友交流一些实践经验。说到安全,这里主要包括两个方面,一个是Authentication,一个是Authorization:

  • Authentication要做的事情,是认证用户的身份,即你说你是A用户, Authentication要确保你真的是A,而不是B;
  • Authorization要做的是权限控制,就是对A用户只能操作它自己有权限的实体(比如HDFS的文件,Hbase的表),对于他没有权限的他不能操作。

有了Authentication和Authorization,总体上算是比较安全了,基本上不会出现,像A用户误删了B用户的数据的事情。在Hbase/Hadoop/Zookeeper中,Authentication是通过Kerberos是实现的,Authorization有各自的实现,相比而言,Authentication的实现相对复杂一些,里面的坑也比较多,因此本篇文章的大部分篇幅会以Authentication为主。对Kerberos之前没了解的同学,可以看一下这篇文章:[Hadoop Kerberos安全机制介绍][1],里面介绍Kerberos认证原理的部分讲得比较清楚。下面就我在实践过程中遇到的一些坑做一个总结。

在实践开始之前,先安装好Kerberos服务器,kerberos的安装比较简单,也不是本文要讨论的内容,直接在google搜索,相关的tutorial应该比较多,照着一步步做下来一般都不会有问题,需要注意的就是区分OS发行版,比如Ubuntu和CentOS,会有一些细微的差别。

另外,要说明一点,我们的安全实践遵循了这样一个原则,在能尽量保证安全的前提下,尽量简化运维,这个原则,贯穿了我们实践的自始至终,为了运维的方便,我们也挖了不少坑,一一解决了,这个会在下面的介绍中提及。

Hadoop

Hadoop的安全配置,我们是以Cloudera的官方文章《[Configuring Hadoop Security in CDH4][1]》为基础来进行实施的,这里不会再将配置项再列一遍,官方文档里写的已经比较清楚了,这里主要介绍一些我们在配置中遇到的问题,以及解决的方案。

  1. Q1: Hadoop的实现中,要求每个service的principal中必须含有FQDN(Fully Qualified Domain Name)
    Hadoop这么设计的初衷,我猜应该是为了让每个principal都只能在一个机器上用,即使别人拿了某个机器上principal的keytab file,不在本机上,也用不了,最大程度的提高安全性。但这样带来的后果是,运维的复杂度的提高。假如有个1000台机器的cluster, 上面布有hdfs和yarn, 至少要生成2000+个keytab file, 而且每新加机器,都要为新机器生成keytab file, 再加上这么多keytab file, 也会造成集群的布署比较麻烦。
    我们的原则是前面提到的“在尽量保证安全的前提下,尽量简化运维”,我们考虑让同一个cluster的同一个种service用同一个principal, 比如hdfs用一个,yarn用一个。因为我们觉得做为一个后端服务平台,做安全的主要目的是为了防止用户误用而导致的事故,比如误删数据,误操作等,在这个基础上,我们希望运维尽量方便。我们来看看Hadoop中对principal的检查(org.apache.hadoop.security.SecurityUtil.java):

    public static String getServerPrincipal(String principalConfig,
        InetAddress addr) throws IOException {
      String[] components = getComponents(principalConfig);
      if (components == null || components.length != 3
          || !components[1].equals(HOSTNAME_PATTERN)) {
        return principalConfig;
      } else {
        if (addr == null) {
          throw new IOException("Can't replace " + HOSTNAME_PATTERN
              + " pattern since client address is null");
        }
        return replacePattern(components, addr.getCanonicalHostName());
      }
    }
    

    它这里的主要逻辑是:先对principal用’/@’进行split, 如果split失败或者split后不是传统的‘hdfs/_HOST@realm’这种格式的三段,或者第二段不是‘_HOST’这个pattern的话,就直接返回在配置文件配的原始的pincipal, 否则就把’_HOST’pattern替换成FQDN,这也正是hadoop以及Cloudera官方配置中推荐的方式。我们要做的第一件事,就是把’_HOST’ pattern替换掉,用一个别的字符串,比如’hdfs/hadoop@realm’这种格式。 当然,仅仅这样做是不够的,还会有坑,这个具体在下面的Q2.

  2. Q2: Namenode通过HTTP请求JournalNode失败
    按Q1中的说法,我们将hadoop的所有principal都改成了‘hdfs/hadoop@realm’,’HTTP/hadoop@realm’, ‘yarn/hadoop@realm’这种形式,启动的时候发现了一个新的问题。namenode无法启动,具体原因是namenode在启动的时候,会去向journalnode请求editlog, 这里的请求是用Http协议来实现的,而这里的问题就是Http请求失败,通不过验证。这个坑是个大坑,当时花了很多时间,无所不用其及的用了各种方法,最终找到了答案,先看java里的一段代码(sun.net.www.protocol.http.NegotiatorImpl.java):

    private void init(final String hostname, String scheme) throws GSSException { 
      // here skip some unimportant code ...
      GSSManagerImpl manager = new GSSManagerImpl(
          GSSUtil.CALLER_HTTP_NEGOTIATE);
    
      String peerName = "HTTP/" + hostname;
    
      GSSName serverName = manager.createName(peerName, null);
      context = manager.createContext(serverName,
          oid,
          null,
          GSSContext.DEFAULT_LIFETIME);
    
      context.requestCredDeleg(true);
      oneToken = context.initSecContext(new byte[0], 0, 0); 
    }   
    

    NegotiatorImpl这个类是Java提供的SPNEGO实现的Negotiation的实现,这里最为关键的是’String peerName = “HTTP/” + hostname;’这一行,它这里明确指定了peer的principal是HTTP/FQDN@realm, 这样在negotiation的时候生成的token就是以这个principal为基础的。而我们在服务端(journalnode中),配置的principal是HTTP/hadoop@realm,因此,negotiation必然会失败。找到了原因,下面就来介绍我们所采用的解决方法。我们发现,在sun.net.www.protocol.http.Negotiator.java中有是通过反射的形式创建NegotiatorImpl的实例的:

    abstract class Negotiator {
      static Negotiator getSupported(String hostname, String scheme) 
        throws Exception {
    
          // These lines are equivalent to
          //     return new NegotiatorImpl(hostname, scheme);
          // The current implementation will make sure NegotiatorImpl is not  
          // directly referenced when compiling, thus smooth the way of building 
          // the J2SE platform where HttpURLConnection is a bootstrap class. 
    
          Class clazz = Class.forName("sun.net.www.protocol.http.NegotiatorImpl"); 
          java.lang.reflect.Constructor c = clazz.getConstructor(
              String.class, String.class);
          return (Negotiator) (c.newInstance(hostname, scheme));
        }
    
      abstract byte[] firstToken() throws Exception;
    
      abstract byte[] nextToken(byte[] in) throws Exception;
    }
    

    既然是通过反射注册进去的,那我们就可以通过设置classpath来,来让它构造是的我们修改过的NegotiatorImpl.java, 我们具体做法是包括下面两步:

    • 修改NegotiatorImpl.java:

      String kerberosInstanceName = System.getProperty("kerberos.instance"); 
      String peerName = null;
      if (kerberosInstanceName == null) {
        peerName = "HTTP/" + hostname;
      } else {
        peerName = "HTTP/" + kerberosInstanceName;
      }                  
      
    • 设置boot classpath: -Xbootclasspath/p:$path_to_modified_negotiator_jar

    • 启动时传入参数: -Dkerberos.instance=hadoop

  3. Q3: DataNode需要root启动
    hadoop以及Cloudera的官方文档中,都推荐security的datanode要用低端口(<1024),而且用jsvc来启动。这里我们遇到两个问题:一是在linux上,低端口的程序是需要用root来启动; 二是我们弄了自己一套发布脚本来布置hadoop,用的普通帐户来布署、启动的,目前并不能很好的和jsvc结合。这两个问题导致datanode无法用我们的布署脚本来像其它程序一样正常的布署、启动。其实,这个问题到现在也并没有真正的解决,只是用了datanode自己开的一个小后门,配置‘ignore.secure.ports.for.testing=true’,这样就可以不用一定要监听低端口,一定要用root jsvc启动,目前来说还没发现这个有什么别的副作用。

  4. Q4: 远程客户端访问不了HDFS
    解决了上面提到的几个坑,HDFS with kerberos authencation就能正常run起来了。接下来需要验证了,在布署hdfs的机器上用hadoop提供的shell进行了各种操作,都OK. 接下来在一台外部机器上,验证远程客户端,客户端却提示找不到合法的的credential,明明通过kinit初始化了,而且klist也看ticket cache中的一切都是正常的。这是怎么回事呢?这里就不卖关子了,这个是个小坑,是因为jce。用了AES-256加密的话,需要安装jce,在布署hdfs的机器上,我们在布署之前把这些环境都安装好了,所以一切都是正常的。而在远程客户端所在的机器上,刚开始没有意思到这个问题,安装了jce之后就OK了。

  5. Q5: Yarn要求使用LinuxContainer, LinuxContainer要求提交MR任务的用户提前在Yarn机器上创建好
    Hadoop官方和Cloudera的文档对于security的Yarn,都是推荐要用LinuxContainer, 而LinuxContainer有个要求,就是要求提交Job的用户帐户必须提前在每个nodemanager所在的机器上预创建好,这又是一个运维非常麻烦的事情。而且我个人觉得,这个要求也有点不太合理,用户要使用服务,还需要在服务所在的物理机器上创建用户帐户,太不科学了!对于这个,我们只好还是采用DefaultContainer,目前还没发现有什么大的坑。

  6. Q6: HDFS只有布署Namenode的principal可以执行管理员操作
    当前HDFS的实现,布署Namenode所用的用户主是整个cluster的管理员,具有超级权限。打开了安全认证之后,就是布署namenode的那个principal具有超级用户权限。这里的主要麻烦是,我想通过shell远程管理hdfs cluster, 就必须把namenode的principal的keytab file到处copy, 这样在一定程度上增加了安全隐患。基于这个考虑,我们给hdfs又加了一个特性,可以通过配置文件指定一个超级用户,而这个超级用户在kerberos上是用密码验证的,每隔一段时间修改一下密码,基本上来说还是比较安全。

  7. Q7: 客户端从ticket cache中取principal的credential, 如何保证不过期
    对于加了Kerberos认证的hdfs,通常我们是这样操作的:

    kinit principal_name # 按提示输入密码
    ./bin/hdfs dfs -ls /
    

    kinit是对principal进行初始化,初始化后,就可以通过klist看到ticket cache的情况。打开了安全认证的hadoop客户端运行时就是从ticket cache是里去读credential的。这里有个问题,ticket会过期,如何保证长时间运行的任务不出问题呢?我们的解决方法是用一个cron job, 定时去renew ticket,通过定时招待kinit -R来完成。但在具体实施的过程中,发现kinit -R报错:’kinit: Ticket expired while renewing credentials’, 这个坑的主要原因是kerberos服务器没有配置renew time, 配置好就可以了。对于之前已经生成好的principal, 需要通过modprinc单独修改。

  8. Q8: HDFS打开ACL之后,检查UserGroup失败
    这个只是一个异常,但程序可以正常run, 对于对异常有强迫症的朋友,也可以考虑把它Fix掉。HDFS缺省建议的配置是“hadoop.security.group.mapping=org.apache.hadoop.security.ShellBasedUnixGroupsMapping”,这里要求使用hdfs的用户也必须在hdfs所在的物理机器上属于某个存在的group,这个对于运维又是极大的不方便,理由中Yarn中LinuxContainer的一样。这里可以自己实现一个简单的GroupsMapping的类,通过配置文件指定,就可以Fix这个。

Hbase

  1. Q1: Hbase的实现也要求每个service的principal中必须含有FQDN
    Hbase的对于安全的实现,基本上跟hadoop中是一样的。也是要求principal中含有FQDN, 不过它的代码中没有其它额外的check, 直接在配置文件中修改成’hbase/hadoop@realm’这样的principal可以正常run.

  2. Q2: SecureRpcEngine找不到
    打开kerberos安全认证的hbase,要配置“hbase.rpc.engine=org.apache.hadoop.hbase.ipc.SecureRpcEngine”, 按正常的编译、布署,发现启动的时候,报找不到SecureRpcEngine这个类。发现开了security的hbase,需要在maven编译的时候,加上-Psecurity, 这个是跟hadoop有点不一样的。

  3. Q3: 管理员问题
    Hbase缺省就提供了一个‘hbase.superuser’的配置项,可以指定超级用户,就不需要再额外修改代码了。

Zookeeper

  1. Q1: Zookeeper的实现也要求每个service的principal中必须含有FQDN
    zookeeper的实现中,server的principal, 在server端是通过jaas.conf来配置的,而在客户端是hardcode的zookeeper/serverHost, 下面是代码中的实现(org.apache.zookeeper.ClientCnxn.java):

    private void startConnect() throws IOException {
      state = States.CONNECTING;
    
      InetSocketAddress addr;
      if (rwServerAddress != null) {
        addr = rwServerAddress;
        rwServerAddress = null;
      } else {
        addr = hostProvider.next(1000);
      }   
    
      LOG.info("Opening socket connection to server " + addr);
    
      setName(getName().replaceAll("\\(.*\\)",
            "(" + addr.getHostName() + ":" + addr.getPort() + ")"));
      try {
        zooKeeperSaslClient = new ZooKeeperSaslClient("zookeeper/"+addr.getHostName()); 
      } catch (LoginException e) {
        LOG.warn("SASL authentication failed: " + e
            + " Will continue connection to Zookeeper server without " 
            + "SASL authentication, if Zookeeper server allows it.");
        eventThread.queueEvent(new WatchedEvent(
              Watcher.Event.EventType.None,
              Watcher.Event.KeeperState.AuthFailed, null));
      }   
      clientCnxnSocket.connect(addr);
    }
    

    这里要修改的就是‘zooKeeperSaslClient = new ZooKeeperSaslClient(“zookeeper/”+addr.getHostName());’,可以简单把这里改成‘zooKeeperSaslClient = new ZooKeeperSaslClient(“zookeeper/hadoop”);’, 更好的一点的做法是,改成可配置的,这个比较简单,这里不再赘言。

Finally, hadoop/hbase/zookeeper with kerberos authentication and with ACL are running!

[1]: http://dongxicheng.org/mapreduce/hadoop-kerberos-introduction/ Hadoop Kerberos安全机制介绍
[2]: https://ccp.cloudera.com/display/CDH4DOC/Configuring+Hadoop+Security+in+CDH4 Configuring Hadoop Security in CDH4
[3]: https://ccp.cloudera.com/display/CDH4DOC/HBase+Security+Configuration HBase Security Configuration
[4]: https://ccp.cloudera.com/display/CDH4DOC/ZooKeeper+Security+Configuration ZooKeeper Security Configuration

  1. 李李
    2013年9月5日14:08 | #1

    小武哥,我是研究hadoop安全的,我想问一下,您文章中所说的“接下来在一台外部机器上,验证远程客户端,客户端却提示找不到合法的的credential,明明通过kinit初始化了,”的客户端是自己编写的客户端还是什么?还有就是“org.apache.zookeeper.ClientCnxn.java”这段程序代码是hadoop的代码的。我怎么在hadoop中没有找到。

  2. 2013年10月8日22:00 | #2

    1. 这个客户端是指hdfs的客户端,我用的hadoop自带的hdfs那个工具;2.这个是zookeeper代码里面的@李李

  3. zdandljb
    2014年4月1日14:12 | #3

    您好,请教一个问题,期盼赐教。
    我现在配置了hadoop、hbase和zookeeper启用kerberos认证,hbase有一个应用opentsdb,目前的opentsdb是不支持安全认证的,请问你们有遇到这个问题吗?如果与偶的话是怎么解决的?
    tks!

  4. 2014年4月5日08:13 | #4

    opentsdb目前确实不支持安全,可以用opentsdb+专用非安全hbase+公共安全hdfs的方式@zdandljb

  5. 或或或
    2014年7月24日19:42 | #5

    服务端加了keyberos 认证,我的java程序要连接zookeeper创建节点,删除节点,客户端代码应该怎么写呢?

  6. 2014年7月25日09:21 | #6

    客户端代码原来怎么写,现在还怎么写;只不过要增加安全相关的配置文件jaas.conf, 运行时加上:-Djava.security.auth.login.config=${path_to_your_jaas.conf} @或或或

  7. 或或或
    2014年7月25日11:22 | #7

    这个程序里要连接zookeeper,还要操作hbase ,那是不是要加两段这个代码了? @小武哥

  8. 2014年7月25日11:48 | #8

    配置一个就够了@或或或

  9. 九剑问天
    2014年10月10日14:17 | #9

    你好,我给HBase配置了Kerberos之后,我怎么样在我的集群外的机子上用我的Java程序访问HBase呢?谢谢

  10. 九剑问天
    2014年10月10日15:14 | #10

    你好,现在还有一个情况,就是我给HBase配置的kerberos,但是我现在依旧可以在另一台机子上通过java代码访问,这是不是表明我的kerberos没有起作用?

  11. honux
    2014年10月29日18:03 | #12

    你好,最近也在搞hadoop集群安全这块,采用hdp集成的Kerberos,本地采用的keytab形式,那么对于非集群内的机器读写hdfs时需要Kerberos的认证:用户和用户密码。这块应该怎么处理啊?非常感谢~~~

  12. 2014年10月29日18:05 | #13

    @honux
    在命令行直接kinit ${your_principal}, 然后按提示输入密码

  13. honux
    2014年10月29日18:10 | #14

    程序里面采用private static KerberosWebHDFSConnection conn = new KerberosWebHDFSConnection(webHdfsUrl, webHdfsUser, webHdfsPwd);这里的webHdfsUser和webHdfsPwd如何填写?@小武哥

  14. 2014年10月29日20:09 | #15

    @honux 你是用webhdfs吗?这个没玩过,参考一下官方文档吧,感觉这里传的应该不是kerberos的principal和password

  15. 2015年2月6日10:09 | #16

    你好,我现在的hbase已经配置了kerberos,并且可以成功对数据表进行赋权,现在的问题是:我想使用java远程访问hbase的数据表,可是访问不到啊~没有添加认证时是可以访问到的~请问您做过这方面吗
    static {
    conf = HBaseConfiguration.create();
    //这里是不是需要加些东西
    conf.set(“hbase.zookeeper.quorum”, “yoyo81”);
    conf.set(“hbase.zookeeper.property.clientPort”, “2181”);
    }

  16. 王尚
    2015年2月7日16:57 | #17

    你好在吗?我想请教几个问题,今天看了你的那篇关于java远程访问hbase的文章。我现在已经实现访问hbase的数据了,但是我要实现hbase和kerberos的结合,请问配置有kerberos的hbase怎么使用java远程访问
    需要zookeeper也要配置kerberos吗

    • 2015年2月8日09:03 | #18

      在你程序要运行的机器上 kinit your_principal, 按提示输入密码
      另外,配置文件中要确认开启了安全
      其它跟不开安全时一样

  17. 老猫
    2016年10月24日16:59 | #19

    您好,请问这种配置如何实现HBASE的跨域CopyTable或Replication,测试时发现认证失败

  1. 本文目前尚无任何 trackbacks 和 pingbacks.
您必须在 登录 后才能发布评论.