Kerberos豆知识

虽然这是上个世纪的东西了,虽然很麻烦,但这是hadoop唯一官方支持的安全机制。。。
我不想太深入kerberos的原理(其实是不懂),只是整理下使用中碰到的一些问题。

基本原理

kerberos最主要的作用是身份认证。比如服务端和客户端要互相验证对方的身份。
它基于一个很朴素的思想:让一个可信的第三方(KDC)来负责身份认证。client对server说,我是XXX,这是我的密钥。server用这个密钥去KDC中查询,是否确实是XXX的。如果是,则证明这个client确实是XXX,是可信的。这是server端验证client的身份。同样client端也要验证server的身份。server对client说,我是YYY,这是密钥。client同样也要去KDC中查询并确认。当client和server互相确认身份后,才能开始通信。

以上只是一种极简的描述。中间有很多细节。比如如何防止窃听、如何加密等等。非常复杂。
但在hadoop中使用时,知道个大概就够了。

里面的很多术语,比如ticket/realm/principal/TGT等等,不一一论述了。感兴趣可以自己google下。

关于JCE

JCE = Java Cryptography Extension。可以理解成包含一堆加密/解密算法的库。由于美国出口的限制(法律上的问题不懂),这个库不能随JDK一起分发。必须到oracle的网站上单独下载。下载后解压并替换$JAVA_HOME/jre/lib/security中的同名文件。

kerberos使用了某种加密算法(似乎是AES?),必须要JCE相关lib。否则在kinit后在java中无法获取ticket。
以上说的都是OracleJDK,如果是OpenJDK则不用替换。

单点?

从之前的分析可以看出,KDC的角色非常重要,一旦KDC挂掉,整个hadoop集群都会挂。而且我们的KDC上不只有hadoop,还有hbase/hiveserver等等。
好在可以配置多个KDC,我们一般是配置3个。例子:

1
2
3
4
5
6
7
8
[realms]
HADOOP.HZ.NETEASE.COM = {
kdc = hadoop0.photo.163.org
kdc = app-109.photo.163.org
kdc = app-110.photo.163.org
admin_server = hadoop0.photo.163.org
kadmind_port = 779
}

kinit的时候,会轮询直到找到可用的KDC。
不太清楚KDC之间是怎么同步数据的。

但配置太多KDC也可能会有问题。由于历史原因我们迁移过KDC,结果一些老的机器上配置没改,开头有很多无效的KDC,kinit时就非常慢,因为要一个个试过去。

关于时间

如果客户端和KDC的时间不同步,差值超过10分钟,kinit是必定失败的。所以务必保证hadoop集群中所有机器的时间和KDC同步。

另外kinit获取的ticket是有时间限制的。在我们的KDC配置中是10小时。快要过期之前可以kinit -R刷新下,就会又有10小时的有效期。最长可以延长到7天。
但超过7天、或者快到10小时没能及时kinit -R,ticket会过期,就必须要重新获取了。
kinit -R是否有效似乎取决于服务端的配置,有些ticket是不能renew的,我不是很熟。
可以用klist命令查看当前ticket的剩余时间、renew的期限。

hadoop中如何认证

这里指的只是客户端。比如我们在提交job时,如何通过kerberos认证?

两种方式:

  1. 在命令行中用kinit命令。比如kinit -kt xxx.keytab yyy/zzz。kinit成功后,就可以像平常一样用hadoop jar提交job了。job的代码不用做任何改变。原理:kinit获取ticket后会缓存在一个临时文件中。java可以读取这个文件并获取kerberos认证相关信息。前提是替换过JCE相关jar。
  2. 用java代码获取ticket。hadoop提供了一个类UserGroupInformation,可以用以下代码获取ticket:
1
2
3
4
5
6
7
8
9
// 如果core-site.xml在classpath里,会自动加载,就不用手动设置属性了
Configuration conf = new Configuration();
// 只是举个例子,kerberos认证的时候只有下面两个属性是必须的
conf.setBoolean("hadoop.security.authorization", true);
conf.set("hadoop.security.authentication", "kerberos");
UserGroupInformation.setConfiguration(conf);
// 如果认证成功会有日志输出
UserGroupInformation.loginUserFromKeytab("yyy/zzz","E:/Documents/TEMP/xxx.keytab");
// 接下来就可以做其他操作了,比如操作FileSystem、提交job等

方法1不用改代码,但是如果一个linux用户要用不同的keytab进行认证,会互相冲突。原因在于默认情况下,对同一个linux用户而言,ticket文件的缓存和kerberos的配置文件都只能有一个。可以通过一些环境变量来设置。
方法2要改些代码,但比较灵活。loginUserFromKeytab一次之后,同一个JVM内所有线程都可以用。不同JVM之间不会互相影响。而且UserGroupInformation提供了其他一些方便的方法,比如ticket快要过期时自动更新、代理执行等等。具体去看javadoc吧。

我现在更喜欢用方法2。

方法2要注意代码执行的顺序。loginUserFromKeytab方法必须在其他代码(访问hdfs、提交job之类)之前执行。

Failed to find any Kerberos tgt

很多地方都可能出现这个错误。
首先klist命令看下输出,确定已经拿到ticket并且没过期。
然后检查hadoop-env.sh中的JAVA_HOME设置。如果是OracleJDK,需要替换jre/lib/security中的jce相关的jar包。一般就正常了。

另外在jdk6u26和之前的版本里,有个bug也可能导致这个错误。kinit后java无法获取ticket。必须kinit -R一次。

Server not found in Kerberos database

参考上面说的kerberos基本原理。client在验证server端身份时,要去KDC中验证对应的principal。如果验证失败,就可能有上面的错误。
具体的错误原因还要具体分析。我们碰到的情况是客户端DNS有问题。服务端返回的principal是hdfs/12.34.56.78,正常情况下客户端应该把IP换成域名,再去KDC中查询的。结果DNS有问题,无法反向查询这个IP,直接用IP去KDC中查,当然找不到。
在客户端本地加了hosts,问题解决。

Server has invalid Kerberos principal

跟上一个问题类似,也是验证server端身份时出错。
我们碰到的情况也是DNS问题。hdfs客户端取出配置文件中的namenode prinicpal(hdfs/hostname.photo.163.org),跟服务端返回的principal(hdfs/12.34.56.78)对比,二者不等则会抛出这个错误。
似乎服务端返回的principal中必定是IP,由客户端去反查域名。没看过代码,不确定。
加了hosts后解决。

JVM属性

在java中使用kerberos时,可以传入一些JVM参数覆盖配置文件中的值。常用的有:

参数 说明
-Djava.security.krb5.conf=${KRB5_CONFIG} 设置krb5.conf的位置。如果不设置,linux下默认是/etc/krb5.conf,windows下是c:/windows/krb5.ini。这两个文件虽然后缀不同,但格式完全一样的。
-Dsun.security.krb5.debug=true 开启debug日志。非常有用。
-Djava.security.krb5.realm=${KRB5_REALM} 设置默认的realm
-Djava.security.krb5.kdc=${kdc.hostname} 设置KDC地址

这里列出的可能不全。
JVM参数可以用-D设置,也可以System.setProperty("sun.security.krb5.debug", "true");

环境变量

在命令行中kinit时有效。

1
2
3
4
# kerberos ticket的缓存位置,默认是/tmp/krb5cc_${uid}
export KRB5CCNAME=/tmp/krbcache
# krb5.conf文件的位置,默认为/etc/krb5.conf
export KRB5_CONFIG=/home/hadoop/my-krb5.conf

只要设置为不同的缓存位置和配置文件,就可以从不同的KDC上获取ticket,进而向多个secured hadoop集群提交任务(我估计设置不同的JVM属性,也能向不同的secured hadoop提交任务,见上一条,但我没试过)。
另外,kerberos的这两个环境变量,在windows版本里也是有用的。只是windows设置环境变量要用set命令:set KRB5CCNAME=D:\krb5cache