最近在写一个MapReduce程序,需要从DB里面读取某些数据,但是公司内所有的DB都是Kerberos方式认证,在这种情况下如何传递kerberos credential,Hadoop如何利用Kerberos认证?因而有必要对其认证机制做一些初步的研究。Hadoop定义了两种基本认证方式,即Simple和Kerberos,后者被认为是isSecurityEnabled。由于以前没有接触过Security相关技术,首先从Simple认证出发,理解一下基本概念和工作流程。
JAAS
首先,Hadoop Security是基于JAAS(Java Authentication and Authorization Service)实现的,这是JDK标准的一个框架,可plug各种类型的LoginModule实现来执行不同的Authentication。JAAS中有两个基本概念:Principal和Subject。
Principal是一种身份的ID,在这种身份上可以唯一地标识一个user。例如,同一个person,他可能有不同的身份,公民,学生,读者等。那么他的公民ID,学生ID以及读者ID都分别是一种Principal。不同的认证机制可以定义不同的Principal来标识user。
Subject是一个container,包含一组关于某个user的安全相关信息。Subject最主要的内容是Principals和Credentials,Principals比如上面提到的公民ID,学生ID。Subject可以在不同的认证机制中传递,每个认证机制都可以将自己定义的Principal加入该Subject。而Credential是对Principal的补充,看如下解释:
With somewhat more controversy, the JAAS designers concluded that Principals may have some sort of proof of identity that they need to be able to provide at a moment’s notice, andthese proofs of identity may include sensitive information, so a set of public credentials and a set of private credentials were also added to Subject. Since the content of a credential may vary widely across authentication mechanisms, from a simple password to a fingerprint (to infinity and beyondl), the type of a credential was simply left as java.lang.Obiect. Relationships between Principals and credentials, if any, were left as an exercise for the implementer of the particular Principal class (or more likely, the particular LoginModule class). From a JAAS perspective, the only difference between private and public credentials is that a particular javax.security.auth.AuthPermission is required for access to the set of private credentials.
LoginModule
弄清楚了JAAS的基本概念,来看Hadoop如何执行Simple Auth。在JobClient初始化时,需要构建一个UserGroupInformation来代表当前提交Job的user。这个构建过程由factory method UserGroupInformation.getLoginUser()完成。期间,根据用户配置的AuthMethod(即SIMPLE和KERBEROS),来调用相应的LoginModule执行认证。假如用户配置Simple模式,将顺序调用两种LoginModule:
org.apache.hadoop.security.UserGroupInformation
private static final AppConfigurationEntry[] SIMPLE_CONF = new AppConfigurationEntry[]{OS_SPECIFIC_LOGIN, HADOOP_LOGIN};
其中,OS_SPECIFIC_LOGIN依赖于OS和JDK vendor:
private static String getOSLoginModuleName() { if (System.getProperty("java.vendor").contains("IBM")) { return windows ? "com.ibm.security.auth.module.NTLoginModule" : "com.ibm.security.auth.module.LinuxLoginModule"; } else { return windows ? "com.sun.security.auth.module.NTLoginModule" : "com.sun.security.auth.module.UnixLoginModule"; }
从框架的层面看,认证的过程就是将Subject对象顺序交给不同的LoginModule,由它们进行specific的认证并将Principals写入Subject。LoginModule接口关键的实现方法有两个login()和commit(),下面看两种LoginModule如何实现。
UnixLoginModule
查看JDK代码,UnixLoginModule有定义三种Principal:UnixPrincipal, UnixNumericUserPrincipal, UnixNumericGroupPrincipal。它的login()实现方法通过native Unix system call拿出当前Unix user的身份信息,并创建Principals:
com.sun.security.auth.module.UnixLoginModule
ss = new UnixSystem(); userPrincipal = new UnixPrincipal(ss.getUsername()); UIDPrincipal = new UnixNumericUserPrincipal(ss.getUid()); GIDPrincipal = new UnixNumericGroupPrincipal(ss.getGid(), true);
commit()方法将这些Principals加入Subject中:
if (!subject.getPrincipals().contains(userPrincipal)) subject.getPrincipals().add(userPrincipal); if (!subject.getPrincipals().contains(UIDPrincipal)) subject.getPrincipals().add(UIDPrincipal); if (!subject.getPrincipals().contains(GIDPrincipal)) subject.getPrincipals().add(GIDPrincipal);
HadoopLoginModule
UnixLoginModule认证后的Subject将传入HadoopLoginModule。进行Hadoop内部的认证。它的login()实际上不做任何事情:
org.apache.hadoop.security.UserGroupInformation.HadoopLoginModule
public boolean login() throws LoginException { if (LOG.isDebugEnabled()) { LOG.debug("hadoop login"); } return true; }
commit()在SIMPLE模式下,尝试从三个地方顺次取username,优先级由高到低分别是:环境变量HADOOP_USER_NAME,系统属性HADOOP_USER_NAME,UnixPrincipal:
if (!isSecurityEnabled() && (user == null)) { String envUser = System.getenv(HADOOP_USER_NAME); if (envUser == null) { envUser = System.getProperty(HADOOP_USER_NAME); } user = envUser == null ? null : new User(envUser); } // use the OS user if (user == null) { user = getCanonicalUser(OS_PRINCIPAL_CLASS); if (LOG.isDebugEnabled()) { LOG.debug("using local user:"+user); } }
最后,基于取出来的username创建一个Hadoop自定义的Principal——org.apache.hadoop.security.User,并且将其加入Subject中:
if (user != null) { subject.getPrincipals().add(new User(user.getName())); }
AuthInfo Propagation
最后来看Client如何将上述认证信息传送给Server。在之前分析RPC的文章中提到,在RPC元数据中有两种Auth相关的字段,AuthMethod和AuthInfo。
对于三种AuthMethod,Hadoop自定义编码如下。在RPC Header字段中,将写入byte编码,而不是string名称。
public static enum AuthMethod { SIMPLE((byte) 80, "", AuthenticationMethod.SIMPLE), KERBEROS((byte) 81, "GSSAPI", AuthenticationMethod.KERBEROS), DIGEST((byte) 82, "DIGEST-MD5", AuthenticationMethod.TOKEN); }
ConnectionHeader中包含AuthInfo字段,如果是Simple Auth,将写入如下信息:
out.writeBoolean(true); out.writeUTF(ugi.getUserName()); if (ugi.getRealUser() != null) { out.writeBoolean(true); out.writeUTF(ugi.getRealUser().getUserName()); } else { out.writeBoolean(false); }
Server端接收到AuthInfo,调用UserGroupInformation.createRemoteUser()重构Subject:
Subject subject = new Subject(); subject.getPrincipals().add(new User(user)); UserGroupInformation result = new UserGroupInformation(subject); result.setAuthenticationMethod(AuthenticationMethod.SIMPLE);
Conclusion
在这种Simple Auth机制下,Client端提取本地OS login username发送给Server,Server毫无保留地接受username,并以此身份运行Job。实际上,Hadoop本身没有做任何认证。这是不安全的,例如,恶意用户在自己的机器上伪造一个其他人的username(比如hdfs),然后向JobTracker发送Job,JobTracker将以hdfs身份运行Job,用户Job将拥有一切hdfs所有的权限。