本文尝试记录HDFS各服务配置使用kerberos的过程,配置的东西比较多,一定会有疏漏

我的环境:

  • 三台服务器,分别命名为zelda1、zelda2、zelda3
  • ubuntu 14.04
  • hadoop 2.7.2
  • spark 2.0/1.6.1

原理

默认Hadoop各个组件间无任何认证,因此可以恶意伪装某一组件(比如NameNode)接入到集群中搞破坏。而通过kerberos,可以将密钥事先放到可靠的节点上并只允许有限制的访问,该节点的服务启动时读取密钥,并与kerberos交互以做认证,从而接入到hadoop集群中。注意,我们这里主要是针对服务与服务之间的安全认证,没有涉及user。

有很多文章讲解Kerberos的原理,或是过于简单或是过于复杂,下面盗用网上翻译的官方示例,可以很清晰的理解其过程:

用户要去游乐场,首先要在门口检查用户的身份(即 CHECK 用户的 ID 和 PASS), 如果用户通过验证,游乐场的门卫 (AS) 即提供给用户一张门卡 (TGT)。

这张卡片的用处就是告诉游乐场的各个场所,用户是通过正门进来,而不是后门偷爬进来的,并且也是获取进入场所一把钥匙。

现在用户有张卡,但是这对用户来不重要,因为用户来游乐场不是为了拿这张卡的而是为了游览游乐项目,这时用户摩天楼,并想游玩。

这时摩天轮的服务员 (client) 拦下用户,向用户要求摩天轮的 (ST) 票据,用户说用户只有一个门卡 (TGT), 那用户只要把 TGT 放在一旁的票据授权机 (TGS) 上刷一下。 票据授权机 (TGS) 就根据用户现在所在的摩天轮,给用户一张摩天轮的票据 (ST), 这样用户有了摩天轮的票据,现在用户可以畅通无阻的进入摩天轮里游玩了。

当然如果用户玩完摩天轮后,想去游乐园的咖啡厅休息下,那用户一样只要带着那张门卡 (TGT). 到相应的咖啡厅的票据授权机 (TGS) 刷一下,得到咖啡厅的票据 (ST) 就可以进入咖啡厅。

当用户离开游乐场后,想用这张 TGT 去刷打的回家的费用,对不起,用户的 TGT 已经过期了,在用户离开游乐场那刻开始,用户的 TGT 就已经销毁了。

下面还有张图来说明这个过程。

原理图

Kerberos有如下几个概念:

Term Description
Key Distribution Center, or KDC 可信任的认证来源,密钥分发中心
Kerberos KDC Server KDS服务提供者
Kerberos Client 集群中准备去做kerberos认证的机器
Principal The unique name of a user or service that authenticates against the KDC
Keytab A file that includes one or more principals and their keys.当不方便使用密码的时候
Realm 我简单理解为域
KDC Admin Account 管理员账户,用来添加其他principal

Principal可以理解为用户或服务的名字,全集群唯一,由三部分组成:username(or servicename)/instance@realm,例如:nn/zelda1@ZELDA.COM,zelda1为集群中的一台机器;或admin/admin@ZELDA.COM,管理员账户。

  • username or servicename:在本文里为服务,HDFS的2个服务分别取名为nn和dn,即namenode和datanode
  • instance:在本文里为具体的FQDN机器名,用来保证全局唯一(比如多个datanode节点,各节点需要各自独立认证)
  • realm:域,我这里为ZELDA.COM(全大写哟)

对于HDFS的各个服务来说,keytab更合适:生成一个keytab文件,其包含一个或多个principal+key对,例如在HDFS配置文件里为nn指定的keytab文件:

<property>
  <name>dfs.namenode.keytab.file</name>
  <value>/etc/security/nn.service.keytab</value>
</property>
<property>

配置

配置DNS服务

如上所述,Principal中的instance为各主机的FQDN名,所以集群中需要有DNS,这里我们用的是dnsmasq,相对比较简单。 简单说明下DNS:DNS是一个递归服务,以查询zh.wikipedia.org为例:

客户端发送查询报文”query zh.wikipedia.org”至DNS服务器,DNS服务器首先检查自身缓存,如果存在记录则直接返回结果。 如果记录老化或不存在,则

  1. DNS服务器向根域名服务器发送查询报文”query zh.wikipedia.org”,根域名服务器返回.org域的权威域名服务器地址,这一级首先会返回的是顶级域名的权威域名服务器。
  2. DNS服务器向.org域的权威域名服务器发送查询报文”query zh.wikipedia.org”,得到.wikipedia.org域的权威域名服务器地址。
  3. DNS服务器向.wikipedia.org域的权威域名服务器发送查询报文”query zh.wikipedia.org”,得到主机zh的A记录,存入自身缓存并返回给客户端。

我们平时上网配置的杭州电信DNS地址(202.101.172.35),对于PC来说只要有DNS解析的需求,都会丢给这个地址的服务器(即为hzdns1)上,那么全球这么多国家这么多地址,显然不可能都存在杭州电信的hzdns1上,所以它会继续向上一直到根服务器请求解析。

所以,在我们这里其实可以自己架设了一个dns缓存服务器,集群内所有的DNS请求都先走这个服务器,如果查询失败,再由这台DNS缓存服务器向上查询;同时,我们也可以将集群内各个主机的主机名加到DNS缓存服务器里,这样集群内的FQDN解析就可以通过这个DNS缓存服务器来完成了(当然,需要将各个机器的DNS nameserver配置为集群内的DNS缓存服务器)。多说一句,对于企业内已有自建DNS的情况,加个接入级的DNS服务器对已有拓扑无任何影响。

我的操作系统是ubuntu 14.04,安装比较简单,sudo apt-get install dnsmasq即可。不过我人品比较好,遇到了网易163的ubuntu源错乱的情况,怎么也get不了,总是提示xx包被依赖但不会被安装,换用aliyun的源正常了。如果你在杭州,建议更换,服务器在杭州电信,速度特别好:

deb http://mirrors.aliyun.com/ubuntu/ trusty main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ trusty-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ trusty-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ trusty-backports main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ trusty-proposed main restricted universe multiverse

dnsmasq安装后需要做配置,可以参考我的:

no-resolv
no-poll
server=202.101.172.35
domain=zelda.com

值得注意的是,配置upstream DNS有两个办法:

  1. 从resolv.conf(or compatible)文件中获取。但我在配置resolv-file=的时候遇到了些障碍,如果用这种方式,发现dnsmasq总是会请求一个61.x.x.x的DNS地址(温州电信的DNS)作为其upstream DNS,但其实这个DNS已经故障不能访问了,即使不用resolv.conf新增一个文件也不能解决。
  2. 指定no-resolv,直接在dnsmasq的配置文件中指定upstream server,对我来说更方便。

检查DNS服务:各机器的/etc/resolv.conf修改为dnsmasq服务所在机器的地址(zelda2),ping集群内各机器的FQDN,如ping zelda1、ping zelda1.zelda.com、ping www.googlebaidu.com。

配置NTP

Kerberos集群对时间同步敏感度较高,默认时间相差超过5分钟就会出现问题,所以最好是在集群中增加NTP服务器。不过我的集群机器比较少,先跳过NTP吧。

创建MIT KDC

Kerberos有不同的实现,如MIT KDC、Microsoft Active Directory等。我这里采用的是MIT KDC。安装过程可以参考ubuntu官网

这段是根据history写的回忆录,可能有错误,下次安装的时候记得修正

(一)krb5 MDC机器上:

1、安装KDC服务和管理员服务

apt-get install krb5-kdc krb5-admin-server

kdc是服务提供者,admin-server是admin登录上去做配置的服务提供者。

跟centos、Suse不一样,UBUNTU的Kr5安装时会提示要求用户输入KDC的realm(如ZELDA.COM)、提供服务的机器名(如zelda2),直接生成krb5的配置文件和realm(/etc/krb5.conf和/var/lib/krb5/principal等),如下:

[libdefaults]
        default_realm = ZELDA.COM
..
[realms]
        ZELDA.COM = {
                kdc = zelda2
                admin_server = zelda2
        }
[domain_realm]
..
        .zelda.com = ZELDA.COM
        zelda.com = ZELDA.COM

注意domain_realm这里,默认生成的文件里没有这两行,你可以试试不加是否也没问题。

2、创建新的realm

!!在ubuntu版本里,不需要做这步,因为上面包安装的时候已经做了。如果装包的时候跳过了config阶段,才需要再来一把。

krb5_newrealm

3、创建一个管理员账户

建议使用一个跟平时用的账户不同的名字。在KDC的机器上:

$ kadmin.local
kadmin.local: addprinc steve/admin

记住自己的密码。kadmin.local是在KDC所在的机器上使用的,而kadmin是任意机器都可以,方便远程管理不过我没用功。

4、修改ACL给新的管理员账户权限

修改/etc/krb5kdc/kadm5.acl:

steve/admin@ZELDA.COM *

当然也可以把steve改为*,这样任意admin这个instance里的用户都有管理员权限了。由于后面配置Hadoop各组件的时候我参考了Hortonwork的文档,所以我还增加了一个admin/admin的用户:

kadmin.local -q "addprinc $username/admin

5、重启服务

/etc/init.d/krb5-kdc restart
/etc/init.d/krb5-admin-server restart

6、使用kinit测试

$ kinit steve/admin
passwd:
$ klist
Ticket cache: FILE:/tmp/krb5cc_0
Default principal: steve/admin@ZELDA.COM

Valid starting       Expires              Service principal
06/06/2016 17:29:07  06/07/2016 03:29:07  krbtgt/ZELDA.COM@ZELDA.COM
	renew until 06/07/2016 17:29:03

(二)在各Krb Client机器上

所有机器上安装krb5的client:

apt-get install krb5-user krb5-config

为HDFS等组件生成keytab

具体参考hortonworks官网

使用admin账户进入kadmin控制台,对于每个服务来说,命令有2条:

addprinc -randkey nn/zelda1@ZELDA.COM
xst -k nn.service.keytab nn/zelda1

第一条是向KDC增加一个principal,之后使用lisprincs可以看到,注意名字按照HDP的要求来; 第二条是导出keytab文件,生成的文件在/var/lib/krb5kdc中。由于拿到了keytab实际也就拿到了Kerberos认证,所以需要将keytab文件放到一个比较隐秘的目录(例如/etc/security/),并且小心其读写权限,只要给最终用户授权就好了。

HDFS的datanode一般有多个,我的做法是为每台机器单独生成keytab,各个机器的keytab只包含自己的principal。也有人为所有datanode生成一个keytab文件,然后扔到各个机器上混用,更方便一点,不过我没用过。

可以使用klist检查keytab是不是自己想要的:

klist –k -t /etc/security/nn.service.keytab

配置HDFS

Hadoop的配置分为2步:

  1. 创建Principal和*unix用户的映射关系。

例如我们上面datanode服务的Principal是dn/zelda1@ZELDA.COM,默认的hadoop.security.auth_to_local规则是将instance和realm去掉,只保留dn,但实际上hdfs的用户并不是dn(我这里操作hdfs的用户是dtdream,如果你用包安装的话可能是叫hdfs,具体可以看hdfs上默认文件的用户是谁),dn这个用户在linux机器里也不存在,那么dn连接nn的时候,会提示错误找不到这个用户的组:

2016-06-07 19:06:58,329 INFO SecurityLogger.org.apache.hadoop.ipc.Server: Auth successful for dn/zelda2@ZELDA.COM (auth:KERBEROS)
2016-06-07 19:06:58,393 WARN org.apache.hadoop.security.UserGroupInformation: No groups available for user dn
2016-06-07 19:06:58,393 INFO org.apache.hadoop.ipc.Server: IPC Server handler 2 on 8020, call org.apache.hadoop.hdfs.server.protocol.DatanodeProtocol.versionRequest from 192.168.103.224:55035 Call#0 Retry#0: org.apache.hadoop.security.AccessControlException: Access denied for user dn. Superuser privilege is required

map配置方法:修改core-site.xml,增加这段:

<property>
  <name>hadoop.security.auth_to_local</name>
  <value>
RULE:[2:$1@$0]([nd]n@ZELDA.COM)s/.*/dtdream/
DEFAULT
</value>
</property>

RULE的写法可以参考这儿,跟Kerberos的规则一样。具体到hadoop,这篇文章更直接一点。

上面的RULE表示会将nn和dn都替换为dtdream。

2、配置HDFS

2.1 总的配置

详细各参数的说明请见hortonworks官网,下面直接贴出来我的配置,略长。

<property>
  <name>dfs.block.access.token.enable</name>
  <value>true</value>
</property>

<!-- NameNode security config -->
<property>
  <name>dfs.namenode.kerberos.principal</name>
  <value>nn/_HOST@ZELDA.COM</value>
</property>
<property>
  <name>dfs.namenode.keytab.file</name>
  <value>/etc/security/nn.service.keytab</value> <!-- path to the HDFS keytab -->
</property>
<property>
  <name>dfs.https.port</name>
  <value>50470</value>
</property>
<property>
  <name>dfs.https.address</name>
  <value>zelda1.zelda.com:50470</value>
</property>

<!-- DataNode security config -->
<property>
  <name>dfs.datanode.kerberos.principal</name>
  <value>dn/_HOST@ZELDA.COM</value>
</property>
<property>
  <name>dfs.datanode.keytab.file</name>
  <value>/etc/security/dn.service.keytab</value> <!-- path to the HDFS keytab -->
</property>
<property>
  <name>dfs.datanode.data.dir.perm</name>
  <value>700</value>
</property>

<!-- datanode SASL配置 -->
<property>
  <name>dfs.datanode.address</name>
  <value>0.0.0.0:61004</value>
</property>
<property>
  <name>dfs.datanode.http.address</name>
  <value>0.0.0.0:61006</value>
</property>
<property>
  <name>dfs.http.policy</name>
  <value>HTTPS_ONLY</value>
</property>
<property>
  <name>dfs.data.transfer.protection</name>
  <value>integrity</value>
</property>

<property>
     <name>dfs.permissions.supergroup</name>
     <value>supergroup</value>
     <description>The name of the group of
     super-users.</description>
</property>

<property>
     <name>dfs.web.authentication.kerberos.principal</name>
     <value>HTTP/_HOST@ZELDA.COM</value>
</property>
<property>
     <name>dfs.web.authentication.kerberos.keytab</name>
     <value>/etc/security/spnego.service.keytab</value>
</property>

简要说明:

  • _HOST是变量,表示所在机器的FQDN名,这样所有节点可以共用一份hdfs-site.xml
  • namenode的webUI改为了https,端口号这里为50470
  • 需要保证各个keytab文件对当前的dtdream可以访问

2.2 特别说明下datanode的SASL配置。

如果不加中间SASL这部分配置,启动时datanode会报错并退出:

2016-06-07 14:02:03,509 FATAL org.apache.hadoop.hdfs.server.datanode.DataNode: Exception in secureMain
java.lang.RuntimeException: Cannot start secure DataNode without configuring either privileged resources or SASL RPC data transfer protection and SSL for HTTP.  Using privileged resources in combination with SASL RPC data transfer protection is not supported.
        at org.apache.hadoop.hdfs.server.datanode.DataNode.checkSecureConfig(DataNode.java:1173)
        at org.apache.hadoop.hdfs.server.datanode.DataNode.startDataNode(DataNode.java:1073)
        at org.apache.hadoop.hdfs.server.datanode.DataNode.<init>(DataNode.java:428)
        at org.apache.hadoop.hdfs.server.datanode.DataNode.makeInstance(DataNode.java:2370)
        at org.apache.hadoop.hdfs.server.datanode.DataNode.instantiateDataNode(DataNode.java:2257)
        at org.apache.hadoop.hdfs.server.datanode.DataNode.createDataNode(DataNode.java:2304)
        at org.apache.hadoop.hdfs.server.datanode.DataNode.secureMain(DataNode.java:2481)
        at org.apache.hadoop.hdfs.server.datanode.DataNode.main(DataNode.java:2505)
2016-06-07 14:02:03,513 INFO org.apache.hadoop.util.ExitUtil: Exiting with status 1
2016-06-07 14:02:03,516 INFO org.apache.hadoop.hdfs.server.datanode.DataNode: SHUTDOWN_MSG:

Kerberos要求datanode以secure mode启动,在2.6之前的版本,hadoop只能使用jsvc,先以root用户来启动datanode,然后再切到普通用户;由于我们只对普通用户做了免密码打通,以root用户启动start-dfs.sh脚本会报错,而且jsvc看上去比较复杂。下面说的很清楚:

As of version 2.6.0, SASL can be used to authenticate the data transfer protocol. In this configuration, it is no longer required for secured clusters to start the DataNode as root using jsvc and bind to privileged ports. To enable SASL on data transfer protocol, set dfs.data.transfer.protection in hdfs-site.xml, set a non-privileged port for dfs.datanode.address, set dfs.http.policy to HTTPS_ONLY and make sure the HADOOP_SECURE_DN_USER environment variable is not defined. Note that it is not possible to use SASL on data transfer protocol if dfs.datanode.address is set to a privileged port. This is required for backwards-compatibility reasons.

配置上有这么几点:

  • 设置dfs.data.transfer.protection为integrity
  • 设置dfs.datanode.address为非特权端口,即大于1024的端口,我这用的是61004
  • 设置dfs.http.policy为HTTPS_ONLY
  • 确保HADOOP_SECURE_DN_USER变量为空

不过故事到这里还没结束,按上面的配置同步各节点后启动hdfs,会发现namenode报错后退出:

2016-06-07 14:12:37,273 INFO org.apache.hadoop.http.HttpServer2: HttpServer.start() threw a non Bind IOException
java.io.FileNotFoundException: /home/dtdream/.keystore (No such file or directory)
        at java.io.FileInputStream.open(Native Method)
        at java.io.FileInputStream.<init>(FileInputStream.java:146)
        at org.mortbay.resource.FileResource.getInputStream(FileResource.java:275)
        at org.mortbay.jetty.security.SslSocketConnector.createFactory(SslSocketConnector.java:242)
        at org.mortbay.jetty.security.SslSocketConnector.newServerSocket(SslSocketConnector.java:476)
        at org.apache.hadoop.security.ssl.SslSocketConnectorSecure.newServerSocket(SslSocketConnectorSecure.java:46)
        at org.mortbay.jetty.bio.SocketConnector.open(SocketConnector.java:73)
        at org.apache.hadoop.http.HttpServer2.openListeners(HttpServer2.java:914)
        at org.apache.hadoop.http.HttpServer2.start(HttpServer2.java:856)
        at org.apache.hadoop.hdfs.server.namenode.NameNodeHttpServer.start(NameNodeHttpServer.java:142)
        at org.apache.hadoop.hdfs.server.namenode.NameNode.startHttpServer(NameNode.java:752)
        at org.apache.hadoop.hdfs.server.namenode.NameNode.initialize(NameNode.java:638)
        at org.apache.hadoop.hdfs.server.namenode.NameNode.<init>(NameNode.java:811)
        at org.apache.hadoop.hdfs.server.namenode.NameNode.<init>(NameNode.java:795)
        at org.apache.hadoop.hdfs.server.namenode.NameNode.createNameNode(NameNode.java:1488)
        at org.apache.hadoop.hdfs.server.namenode.NameNode.main(NameNode.java:1554)
2016-06-07 14:12:37,275 INFO org.apache.hadoop.metrics2.impl.MetricsSystemImpl: Stopping NameNode metrics system...

2.3 没有keystore文件呀

其实到这跟hadoop,Kerberos就没什么关系了,纯粹是https的配置了。简单说明下原理:https要求集群中有一个CA,它会生成ca_key和ca_cert,想要加入这个集群的节点,需要拿到这2个文件,然后经过一连串的动作生成keystore,并在hadoop的ssl-server.xml和ssl-client.xml中指定这个keystore的路径和密码,这样各个节点间就可以使用https进行通信了(可能大概是这么回事吧)。更详细的可以参考SASL配置

下面直接贴命令。

CA机器上:

openssl req -new -x509 -keyout test_ca_key -out test_ca_cert -days 9999 -subj '/C=CN/ST=zhejiang/L=hangzhou/O=dtdream/OU=security/CN=zelda.com'

将上面生成的test_ca_key和test_ca_cert丢到所有机器上,在各个机器上继续:

keytool -keystore keystore -alias localhost -validity 9999 -genkey -keyalg RSA -keysize 2048 -dname "CN=zelda.com, OU=test, O=test, L=hangzhou, ST=zhejiang, C=cn"
keytool -keystore truststore -alias CARoot -import -file test_ca_cert
keytool -certreq -alias localhost -keystore keystore -file cert
openssl x509 -req -CA test_ca_cert -CAkey test_ca_key -in cert -out cert_signed -days 9999 -CAcreateserial -passin pass:changeit
keytool -keystore keystore -alias CARoot -import -file test_ca_cert
keytool -keystore keystore -alias localhost -import -file cert_signed

最终在当前目录下会生成keystore、trustkeystore文件。

2.4 配置ssl-server.xml和ssl-client.xml

从{target}.xml.example文件拷贝一份出来,并制定keystore、trustkeystore两个文件的路径、password,然后同步到所有节点。

2.5 启动HDFS

从namenode的日志可以看到服务已经正常启动,DN都连接上来了,使用https://{namenode_ip}:50470上可以看到各个datanode。但是使用hadoop命令行 fs -ls时会报错:

./hadoop fs -ls /
16/06/08 13:24:35 WARN ipc.Client: Exception encountered while connecting to the server : javax.security.sasl.SaslException: GSS initiate failed [Caused by GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt)]
ls: Failed on local exception: java.io.IOException: javax.security.sasl.SaslException: GSS initiate failed [Caused by GSSException: No valid credentials provided (Mechanism level: Failed to find any Kerberos tgt)]; Host Details : local host is: "zelda1/192.168.103.237"; destination host is: "zelda1":8020;

提示没有credentials,也就是说没有凭据。一方面说明Kerberos的确是保护了hdfs,另一方面也说明我们还需要给本地加一个Principal来访问hdfs。

2.6 增加用户Principal

KDC上:

kadmin.local:  addprinc dtdream@ZELDA.COM
WARNING: no policy specified for dtdream@ZELDA.COM; defaulting to no policy
Enter password for principal "dtdream@ZELDA.COM":
Re-enter password for principal "dtdream@ZELDA.COM":
Principal "dtdream@ZELDA.COM" created.

再在hdfs客户端的机器上使用kinit初始化credentials,再去访问hdfs就正常了。

$ kinit dtdream@ZELDA.COM
Password for dtdream@ZELDA.COM: 
$ klist
Ticket cache: FILE:/tmp/krb5cc_1000
Default principal: dtdream@ZELDA.COM

Valid starting       Expires              Service principal
2016-06-08T13:37:21  2016-06-08T23:37:21  krbtgt/ZELDA.COM@ZELDA.COM
	renew until 2016-06-09T13:37:19
$ ./hadoop fs -ls /
Found 3 items
drwxr-xr-x   - dtdream supergroup          0 2016-05-27 09:45 /home
drwxrwxr-x   - dtdream supergroup          0 2016-05-25 16:01 /tmp
drwxr-xr-x   - dtdream supergroup          0 2016-05-25 16:11 /user

2.7 配置chrome以访问被Kerberos保护的hdfs webUI

配置FIREFOX、chrome、IE支持Kerberos HTTP SPNEGO,参考这里

以firefox为例,需要将zelda.com作为可信uri,设置到network.negotiate-auth.trusted-uris里去。

but,还是有问题,我不知道怎么把认证传过去,先放一下。

http://docs.hortonworks.com/HDPDocuments/HDP2/HDP-2.4.0/bk_Security_Guide/content/_optional_install_a_new_mit_kdc.html

Kerberos安装参考

主要参考hortonworks官网


传送门:

Kerberos从入门到放弃(一):HDFS使用kerberos

Kerberos从入门到放弃(二):YARN、Spark、Hive使用kerberos

Kerberos从入门到放弃(三):kerberos+LDAP各司其职