Hadoop, Hbase, Zookeeper安全实践

出自sebug security vulnerability(SSV) DB
跳转到: 导航, 搜索

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

说到安全,这里主要包括两个方面,一个是Authentication,一个是Authorization:


有了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]》为基础来进行实施的,这里不会再将配置项再列一遍,官方文档里写的已经比较清楚了,这里主要介绍一些我们在配置中遇到的问题,以及解决的方案。


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.


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


Q3

DataNode需要root启动

hadoop以及Cloudera的官方文档中,都推荐security的datanode要用低端口(<1024),而且用jsvc来启动。

这里我们遇到两个问题:

这两个问题导致datanode无法用我们的布署脚本来像其它程序一样正常的布署、启动。


其实,这个问题到现在也并没有真正的解决,只是用了datanode自己开的一个小后门,配置‘ignore.secure.ports.for.testing=true’,这样就可以不用一定要监听低端口,一定要用root jsvc启动,目前来说还没发现这个有什么别的副作用。


Q4

远程客户端访问不了HDFS

解决了上面提到的几个坑,HDFS with kerberos authencation就能正常run起来了。

接下来需要验证了,在布署hdfs的机器上用hadoop提供的shell进行了各种操作,都OK. 接下来在一台外部机器上,验证远程客户端,客户端却提示找不到合法的的credential,明明通过kinit初始化了,而且klist也看ticket cache中的一切都是正常的。

这是怎么回事呢?这里就不卖关子了,这个是个小坑,是因为jce。

用了AES-256加密的话,需要安装jce,在布署hdfs的机器上,我们在布署之前把这些环境都安装好了,所以一切都是正常的。而在远程客户端所在的机器上,刚开始没有意思到这个问题,安装了jce之后就OK了。


Q5

Yarn要求使用LinuxContainer, LinuxContainer要求提交MR任务的用户提前在Yarn机器上创建好 Hadoop官方和Cloudera的文档对于security的Yarn,都是推荐要用LinuxContainer, 而LinuxContainer有个要求,就是要求提交Job的用户帐户必须提前在每个nodemanager所在的机器上预创建好,这又是一个运维非常麻烦的事情。

而且我个人觉得,这个要求也有点不太合理,用户要使用服务,还需要在服务所在的物理机器上创建用户帐户,太不科学了!对于这个,我们只好还是采用DefaultContainer,目前还没发现有什么大的坑。


Q6

HDFS只有布署Namenode的principal可以执行管理员操作 当前HDFS的实现,布署Namenode所用的用户主是整个cluster的管理员,具有超级权限。打开了安全认证之后,就是布署namenode的那个principal具有超级用户权限。

这里的主要麻烦是,我想通过shell远程管理hdfs cluster, 就必须把namenode的principal的keytab file到处copy, 这样在一定程度上增加了安全隐患。

基于这个考虑,我们给hdfs又加了一个特性,可以通过配置文件指定一个超级用户,而这个超级用户在kerberos上是用密码验证的,每隔一段时间修改一下密码,基本上来说还是比较安全。


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单独修改。

Q8

HDFS打开ACL之后,检查UserGroup失败

这个只是一个异常,但程序可以正常run, 对于对异常有强迫症的朋友,也可以考虑把它Fix掉。HDFS缺省建议的配置是“hadoop.security.group.mapping=org.apache.hadoop.security.ShellBasedUnixGroupsMapping”,这里要求使用hdfs的用户也必须在hdfs所在的物理机器上属于某个存在的group,这个对于运维又是极大的不方便,理由中Yarn中LinuxContainer的一样。

这里可以自己实现一个简单的GroupsMapping的类,通过配置文件指定,就可以Fix这个。


Hbase

Q1

Hbase的实现也要求每个service的principal中必须含有FQDN

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


Q2

SecureRpcEngine找不到

打开kerberos安全认证的hbase,要配置“hbase.rpc.engine=org.apache.hadoop.hbase.ipc.SecureRpcEngine”, 按正常的编译、布署,发现启动的时候,报找不到SecureRpcEngine这个类。

发现开了security的hbase,需要在maven编译的时候,加上-Psecurity, 这个是跟hadoop有点不一样的。


Q3

管理员问题

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


Zookeeper

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


http://www.wuzesheng.com/?p=2345


如果显示不正常,请使用Mozilla Firefox或Chrome进行浏览


sebug_flat_0da.png

SEBUG安全漏洞信息库免费漏洞信息平台,免费提供最新的漏洞信息、漏洞修复、漏洞目录、安全文档、漏洞趋势分析、漏洞检测等
个人工具
名字空间
变换
导航
工具箱