Hadoop安全之Kerberos

简介

安全无小事,我们常常要为了预防安全问题而付出大量的代价。虽然小区楼道里面的灭火器、消防栓常年没人用,但是我们还是要准备着。我们之所以愿意为了这些小概率事件而付出巨大的成本,是因为安全问题一旦发生,很多时候我们将无法承担它带来的后果。

在软件行业,安全问题尤其突出,因为无法预料的事情实在太多了。软件的复杂性让我们几乎无法完全扫清安全问题,模块 A 独立运行可能没问题,但是一旦和模块 B 一起工作也许就产生了安全问题。

不可否认为了让软件更安全,我们引入了很多复杂的机制。不少人开发者也抱怨为了进行安全处理而做了太多额外的事情。在一个复杂的分布式软件 Hadoop 中,我们为此付出的成本将更大。比如,我们可能可以比较轻松的搭建一个无安全机制的集群,但是一旦需要支持安全机制的时候,我们可能会付出额外几倍的时间来进行各种复杂的配置和调试。

Hadoop 在开始的几个版本中其实并没有安全机制的支持,后来 Yahoo 在大规模应用 Hadoop 之后,安全问题也就日益明显起来。大家都在一个平台上面进行操作是很容易引起安全问题的,比如一个人把另一个人的数据删除了,一个人把另一个人正在运行的任务给停掉了,等等。在当今的企业应用里面,一旦我们的数据开始上规模之后,安全机制的引入几乎是必然的选择。所以作为大数据领域的开发者,理解 Hadoop 的安全机制就显得非常重要。

Hadoop 的安全机制现在已经比较成熟,网上关于它的介绍也很多,但相对较零散,下面我将尝试更系统的,并结合实例代码,给大家分享一下最近一段时间关于 Hadoop 安全机制的学习所得,抛个砖。

预计将包括这样几个方面:

  1. Kerberos 协议介绍及实践
  2. Kerberos 协议发展及 Hadoop 相关源码分析
  3. Hadoop 安全集群搭建及测试
  4. 周边工具的安全支持

安全认证协议

Kerberos

做 Web 开发的同学们可能比较熟悉的认证机制是 JWT,近两年 JWT 的流行几乎让其成为了实现单点登录的一个标准。JWT 将认证服务器认证后得到的 token 及一定的用户信息经过 base64 编码之后放到 HTTP 头中发送给服务器端,得益于 token 的加密机制(一般是非对称加密),服务器端可以在不连接认证服务器就进行 token 验证(第一次验证时会向认证服务器请求公钥),从而实现高性能的鉴权。这里的 token 虽然看起来不可读,实际上我们经过简单的解码就能得到 token 的内容。所以 JWT 一般是要结合 HTTPS 一起应用才能带来不错的安全性。

JWT 看起来还不错呀,安全模型比较简单,能不能直接用在 Hadoop 上面呢?可能可以。但是由于 Hadoop 的出现早于 JWT 太多,所以当时的设计者们是不可能考虑使用 JWT 的。实际上 JWT 主要是针对 web 的场景设计的,对于分布式场景中,很多问题它是没有给出答案的。一些典型的场景比如服务间的认证该如何实现,如何支持其他的协议,等等。Hadoop 的安全认证使用的是 Kerberos 机制。相比 JWTKerberos 是一个更为完整的认证协议,然而也正是因为其设计可以支持众多的功能,也给其理解和使用带来了困难。

这里之所以提到 JWT,是因为 JWT 实际上可以看成是 Kerberos 协议的一个极简版本。JWT 实现了一部分 Kerberos 的功能。如果我们能对于 JWT 的认证机制比较熟悉,那么对于 Kerberos 机制的理解应当是有较大帮助的。

Kerberos 协议诞生于 MIT 大学,早在上世纪 80 年代就被设计出来了,然后经过了多次版本演进才到了现在我们用的 V5 版本。作为一个久经考验的安全协议,Kerberos 的使用其实是非常广泛的,比如 Windows 操作系统的认证就是基于 Kerberos 的,而 Mac Red Hat Enterprise Linux 也都对于 Kerberos 有完善的支持。各种编程语言也都有内置的实现。对于这样一个重要的安全协议,就算我们不从事大数据相关的开发,也值得好好学习一下。

Kerberos 设计的有几个大的原则:

  1. 利用公开的加密算法实现
  2. 密码尽量不在网络上传输
  3. 高安全性和性能
  4. 支持广泛的安全场景,如防止窃听、防止重放攻击、保护数据完整性等

那么这个协议是如何工作的呢?与 JWT 类似,Kerberos 同样定义了一个中心化的认证服务器,不过对于这个认证服务器,Kerberos 按照功能进一步将其拆分为了三个组件:认证服务器(Authentication Server,AS)、密钥分发中心(Key Distribution Center,KDC)、票据授权服务器(Ticket Granting Server,TGS)。在整个工作流程中,还有两个参与者:客户端 (Client) 和服务提供端 (Service Server,SS)。

Kerberos 大体上的认证过程与 JWT 一致:第一步是客户端从认证服务器拿到 token(这里的术语是 Ticket,下文将不区分这两个词,请根据上下文理解);第二步是将这个 token 发往服务提供端去请求相应的服务。

下图是整个认证过程中各个组件按顺序相互传递的消息内容,在阅读整个流程之前,有几点提需要注意:

  1. 各个组件都有自己独立的秘钥:Client 的秘钥由用户提供,AS、TGS、SS 需要提前生成自己独立的秘钥
  2. AS、TGS 由于属于认证服务器的一部分,它们可以查询 KDC 得到用户或其他服务器的秘钥,比如 AS 可以认为拥有用户的、TGS 的以及 SS 的秘钥

看了这个复杂的流程,大家心里应该有很多疑惑。整个通信过程传递了很多的消息,消息被来来回回加密了很多次,真的是有必要的吗?背后的原因是什么呢?事实上,我们结合上面提到的几个设计原则来看,问题就会相对清晰一些。

虽然整个通信过程涉及到的消息很多,但是我们仔细思考就可以发现这几条规律:

  1. 整个认证过程中,避免了任何地方有明文的密码传输
  2. 与 JWT 一样,通信过程生成有效时间比较短的会话秘钥用于通信
  3. 与 JWT 一样,认证服务器无需存储会话秘钥,各个参与方(Client/SS)可以独立进行消息验证,从而实现高性能。这也是虽然消息 B 和 E 不能被 Client 解密,但是还是会发往 Client,然后再由 Client 回发的原因
  4. Kerberos 并没有对 Client 和 SS 之间的通信协议进行限制,虽然和认证服务器进行通信需要基于 TCP/UDP,但 Client 和 SS 通信可以用任意协议进行

理解了上述通信流程之后,可以看到,相比 JWTKerberos 还进行了下面的额外验证:

  1. 认证过程将验证服务提供端的 ID,一般会基于 hostname 进行
  2. 认证过程将验证各个组件的时间,相互不能相差太多,这也是 Kerberos 要求各个组件进行时间同步的原因

除了上面这些安全验证,其实 Kerberos 还支持免密码输入的登录,我们可以将用户的秘钥(并非真正的密码,由真正的密码 hash 生成)生成到一个 keytab 格式的文件中,这样在第一步中,就可以由用户提供 ID (principal) 及 keytab 文件来完成了。

虽然 Kerberos 可以支持多种场景的认证,但是由于其协议设计比较复杂,在使用上会给我们带来不少的困难。比如我们需要提前为各个组件生成独立的秘钥,一般要求每个服务器都不一样,与不同的主机绑定,这就给我们部署服务带来了挑战,特别是在当前微服务、云原生应用、容器、k8s 比较流行的时候。

通信过程演示

为了更清晰的看到整个通信的过程,我们可以动手实践一下看看

然后安装配置 kdc 并生成相关的秘钥:

# 将kdc kdc.hadoop.com加入hosts,以便后续进行基于hosts文件的主机名解析
yum install net-tools -y
ip_addr=$(ifconfig ens33 | grep inet | awk '{print $2}')
#这里主要的作用就是写一个本地的host的ip地址映射,如上图
echo "$ip_addr kdc-server kdc-server.hadoop.com" >> /etc/hosts

# 安装相关软件并进行配置
yum install krb5-server krb5-libs krb5-workstation -y
# 创建krb5配置文件,详细配置解释请参考:https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html
cat > /etc/krb5.conf <<EOF
#Configuration snippets may be placed in this directory as well
includedir /etc/krb5.conf.d/

[logging]
  default = FILE:/var/log/krb5.log
  kdc = FILE:/var/log/krb5kdc.log
  admin_server = FILE:/var/log/kadmind.log

[libdefaults]
  forcetcp = true
  default_realm = HADOOP.COM
  dns_lookup_realm = false
  dns_lookup_kdc = false
  ticket_lifetime = 24h
  renew_lifetime = 7d
  forwardable = true
  udp_preference_limit = 1
  default_tkt_enctypes = des-cbc-md5 des-cbc-crc des3-cbc-sha1
  default_tgs_enctypes = des-cbc-md5 des-cbc-crc des3-cbc-sha1
  permitted_enctypes = des-cbc-md5 des-cbc-crc des3-cbc-sha1

[realms]
  HADOOP.COM = {
    kdc = kdc-server.hadoop.com:2802
    admin_server = kdc-server.hadoop.com:2801
    default_domain = hadoop.com
  }

[domain_realm]
  .hadoop.com = HADOOP.COM
  hadoop.com = HADOOP.COM
EOF
# 创建kdc配置文件,详细配置解释请参考:https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/kdc_conf.html
cat > /var/kerberos/krb5kdc/kdc.conf <<EOF
default_realm = HADOOP.COM

[kdcdefaults]
 kdc_ports = 0
 v4_mode = nopreauth

[realms]
 HADOOP.COM = {
    kdc_ports = 2800
    kdc_tcp_ports = 2802
    admin_keytab = /etc/kadm5.keytab
    database_name = /var/kerberos/krb5kdc/principal
    acl_file = /var/kerberos/krb5kdc/kadm5.acl
    key_stash_file = /var/kerberos/krb5kdc/stash
    max_life = 10h 0m 0s
    max_renewable_life = 7d 0h 0m 0s
    master_key_type = des3-hmac-sha1
    supported_enctypes = arcfour-hmac:normal des3-hmac-sha1:normal des-cbc-crc:normal des:normal des:v4 des:norealm des:onlyrealm des:afs3
    default_principal_flags = +preauth
}
EOF

echo -e '123456\n123456' | kdb5_util create -r HADOOP.COM -s  # 创建一个名为HADOOP.COM的域
/usr/sbin/krb5kdc && /usr/sbin/kadmind                        # 启动kdc及kadmind服务

echo -e '123456\n123456' | kadmin.local addprinc gml    # 创建gml账号
kadmin.local xst -k gml.keytab gml@HADOOP.COM           # 生成gml账号的keytab文件

kadmin.local addprinc -randkey root/localhost@HADOOP.COM       # 创建名为root并和kdc主机进行绑定的服务账号
kadmin.local xst -k server.keytab root/localhost@HADOOP.COM    # 创建用于服务器的keytab文件

操作完以后查看下端口

还有对应生成的文件

 将生成的 keytab 文件下载到本地,然后就可以进行测试了。编写测试的客户端和服务端代码如下:

准备对应的文件

sz /etc/krb5.conf
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.Oid;

import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;


public class Test {

    public static class TestClient {
        private String srvPrincal;
        private String srvIP;
        private int srvPort;
        private Socket socket;
        private DataInputStream inStream;
        private DataOutputStream outStream;

        public TestClient(String srvPrincal, String srvIp, int srvPort) throws Exception {
            this.srvPrincal = srvPrincal;
            this.srvIP = srvIp;
            this.srvPort = srvPort;
            this.initSocket();
            this.initKerberos();
        }

        private void initSocket() throws IOException {
            this.socket = new Socket(srvIP, srvPort);
            this.inStream = new DataInputStream(socket.getInputStream());
            this.outStream = new DataOutputStream(socket.getOutputStream());
            System.out.println("Connected to server: " + this.socket.getInetAddress());
        }

        private void initKerberos() throws Exception {
            System.setProperty("java.security.krb5.conf", "src/main/krb5.conf");
            System.setProperty("java.security.auth.login.config", "src/main/gml.keytab");
            System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
            System.setProperty("sun.security.krb5.debug", "true");

            System.out.println("init kerberos: set up objects as configured");
            GSSManager manager = GSSManager.getInstance();
            Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
            GSSContext context = manager.createContext(
                    manager.createName(srvPrincal, null),
                    krb5Oid, null, GSSContext.DEFAULT_LIFETIME);
            context.requestMutualAuth(true);
            context.requestConf(true);
            context.requestInteg(true);

            System.out.println("init kerberos: Do the context establishment loop");

            byte[] token = new byte[0];

            while (!context.isEstablished()) {
                // token is ignored on the first call
                token = context.initSecContext(token, 0, token.length);

                // Send a token to the server if one was generated by initSecContext
                if (token != null) {
                    System.out.println("Will send token of size " + token.length + " from initSecContext.");
                    outStream.writeInt(token.length);
                    outStream.write(token);
                    outStream.flush();
                }

                // If the client is done with context establishment then there will be no more tokens to read in this loop
                if (!context.isEstablished()) {
                    token = new byte[inStream.readInt()];
                    System.out.println(
                            "Will read input token of size " + token.length + " for processing by initSecContext");
                    inStream.readFully(token);
                }
            }

            System.out.println("Context Established! ");
            System.out.println("Client is " + context.getSrcName());
            System.out.println("Server is " + context.getTargName());

        }

        public void sendMessage() throws Exception {
            // Obtain the command-line arguments and parse the port number

            String msg = "Hello Server ";
            byte[] messageBytes = msg.getBytes();
            outStream.writeInt(messageBytes.length);
            outStream.write(messageBytes);
            outStream.flush();

            byte[] token = new byte[inStream.readInt()];
            System.out.println("Will read token of size " + token.length);
            inStream.readFully(token);

            String s = new String(token);
            System.out.println(s);

            System.out.println("Exiting... ");
        }

        public static void main(String[] args) throws Exception {
            TestClient client = new TestClient("root/localhost@HADOOP.COM", "localhost", 9112);
            client.sendMessage();
        }
    }

    public static class TestServer {
        private int localPort;
        private ServerSocket ss;
        private Socket socket = null;

        public TestServer(int port) {
            this.localPort = port;
        }

        public void receive() throws IOException, GSSException {
            this.ss = new ServerSocket(localPort);
            socket = ss.accept();
            DataInputStream in = new DataInputStream(socket.getInputStream());
            DataOutputStream out = new DataOutputStream(socket.getOutputStream());
            this.initKerberos(in, out);

            int length = in.readInt();
            byte[] token = new byte[length];
            System.out.println("Will read token of size " + token.length);
            in.readFully(token);
            String s = new String(token);
            System.out.println("Receive Client token: " + s);

            byte[] token1 = "Receive Client Message".getBytes();
            out.writeInt(token1.length);
            out.write(token1);
            out.flush();
        }

        private void initKerberos(DataInputStream in, DataOutputStream out) throws GSSException, IOException {
            GSSManager manager = GSSManager.getInstance();
            GSSContext context = manager.createContext((GSSCredential) null);
            byte[] token;

            while (!context.isEstablished()) {
                token = new byte[in.readInt()];
                System.out.println("Will read input token of size " + token.length + " for processing by acceptSecContext");
                in.readFully(token);

                token = context.acceptSecContext(token, 0, token.length);

                // Send a token to the peer if one was generated by acceptSecContext
                if (token != null) {
                    System.out.println("Will send token of size " + token.length + " from acceptSecContext.");
                    out.writeInt(token.length);
                    out.write(token);
                    out.flush();
                }
            }

            System.out.println("Context Established! ");
            System.out.println("Client is " + context.getSrcName());
            System.out.println("Server is " + context.getTargName());
        }

        public static void main(String[] args) throws IOException, GSSException {
            System.setProperty("java.security.krb5.conf", "src/main/krb5.conf");
            System.setProperty("java.security.auth.login.config", "src/main/server.conf");
            System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");
            System.setProperty("sun.security.krb5.debug", "true");

            TestServer server = new TestServer(9112);
            server.receive();
        }
    }
}

先运行 Server 程序,再运行 Client 程序,我们将能从输出内容中看到整个通信的过程。

当前 web 应用成为主流的时候,Kerberos 如何在 HTTP/HTTPS 协议场景下使用呢?我们又要如何配置,才能运行一套支持认证的 Hadoop 集群呢?

我们分析了 Kerberos 协议的设计和通信过程。可以了解到,Kerberos 主要实现了不在网络传输密码的同时又能在本地进行高性能鉴权。

Kerberos 协议回顾

假设有三个组件 A B C,A 想和 C 进行安全通信,而 B 作为一个认证中心保存了认证信息。那么以以下的方式进行通信就可以做到安全:

  1. A 向 B 请求说要访问 C,将此消息用 A 的秘钥加密之后,发给 B
  2. B 验证 A 的权限之后,用 A 自己的秘钥加密一个会话密码,然后传给 A
  3. 同时 B 还向 A 发送一个 A 自己不能解密,只能由 C 解密的消息
  4. A 在解密会话密码之后,将需要和 C 通信的消息(业务消息)用这个会话密码加密然后发给 C,同时 A 需要将 B 发给 A 而 A 又不能解密的消息发给 C
  5. C 在拿到消息之后,可以将第三步中的消息解密,得到会话秘钥,从而可以解密 A 发过来的业务消息了

整个过程,A 无需知道 C 的密码,C 也无需知道 A 的密码就可以完成安全通信。

这里的安全性我们可以从以下几个方面来看:

  1. 如果消息被截获
    当第一步中的消息被截获:这里的消息用 A 的秘钥加密了,截获也无法解密
    当第二步中的消息被截获:这里的消息用 A 的秘钥加密了,截获也无法解密
    当第三步中的消息被截获:这里的消息用 C 的秘钥加密了,截获也无法解密
    当第四步中的消息被截获:这里的消息分别用会话秘钥、C 的秘钥加密了,截获也无法解密
    当第五步中的消息被截获:这里的消息用会话秘钥,截获也无法解密

  2. 如果 A 是一个攻击方(某一个有权限的用户想要提权)
    他只能拿到自己的秘钥,而无法获取 B 或 C 的秘钥,他不能随意生成一个加密消息发给 C 请求服务(冒充其他用户),因为他无法伪造有会话密码而又用 C 的秘钥加密的消息

  3. 如果 C 是一个攻击方(欺骗某个有权限的用户)
    他无法解密第三步中的消息,所以无法解密 A 的消息,从而也就无从提供服务

  4. 如果 B 是一个攻击方
    他无法解密 A 的消息,从而无法提供服务

  5. 如果传输的消息被破解(任何加密都是可以被破解的,只是时间的问题)
    由于整个通信过程由会话秘钥来加密,会话秘钥的有效期通常比较短,当消息被破解之后,攻击者也不能利用破解得到的秘钥去破解后续的消息

从这几个方面来看,这个协议都是比较安全的。

以上的安全通信步骤是 kerberos 安全的核心机制,A 对应文章中的 Client,B 对应文章中的 TGS,C 对应文章中的 SS

但 kerberos 还引入了一个 AS 的组件,这主要为了提高性能和扩展性。

有了 AS 之后,我们可以将整个通信看成两个上述 ABC 通信模式的重复。第一个通信模式 A 对应文章中的 Client,B 对应文章中的 AS,C 对应文章中的 TGS,为了实现 Client 和 TGS 的安全通信。第二个通信模式 A 对应文章中的 Client,B 对应文章中的 TGS,C 对应文章中的 SS,为了实现 Client 和 SS 的安全通信。

为什么有了两次通信模式之后,就能提高性能和扩展性呢?实际上一般我们可以将 Client/TGS 的会话秘钥有效期配置得更长一些,而将 Client/SS 的会话秘钥有效期配置得比较短。由于一旦我们有一个有效的 TGT 及 Client/TGS 会话秘钥,在这个秘钥的有效期内,我们无需再访问 AS 去生成新的会话秘钥。当 Client/TGS 会话秘钥有效期较长的时候,我们就可以较少的访问 AS,从而将 AS 这一第一入口服务的负载降低。而 TGS 由于需要经常参与秘钥生成,它的负载会相对较高,这里我们就可以将 TGS 扩展到多台服务器来支撑大的负载。AS 可以给 Client 提供一个有效的 TGS 地址,从而实现 TGS 的分布式扩展。

Kerberos 协议发展

GSS API

Kerberos 协议本身只是提供了一种安全认证和通信的手段,要应用这个协议,我们需要一套 API 接口。在具体实现的时候,每个人都会写出不一样的代码,从而产生不同的 API。这可不是好事,对于应用方而言,不仅仅学习成本高,而且系统迁移能力差,比如换一个 Kerberos 服务器可能就会出现兼容性问题。就像 windows 上面的换行用 \r\n,而 unix 类操作系统用 \n,这给每一个开发者都带来了麻烦。

所以,在具体的工程应用时,一种通用的 API 就变得非常重要。这就是 GSS API,其全称是 The Generic Security Services Application Program Interface,即通用安全服务应用程序接口。这套 API 在设计的时候其实不仅仅考虑了对于 Kerberos 的支持,还考虑了支持其他的协议,所以称为通用接口。由于我们总是会发展出其他的安全协议的,抽象一套可以长期保持不变的通用的 API 接口,就可以避免应用层进行修改。这一套 API 接口就是在上一篇文章中我们用到的接口了。

从 GSS API 接口来看,我们的认证过程可以抽象为这样几个简单的步骤:

  • 客户端:创建一个 Context 上下文用来保存数据 -> 通过 initSecContext 获取一个 token -> 将 token 发送给服务器 -> 等待服务器回发的用于通信的 token
  • 服务器:创建一个 Context 上下文用来保存数据 -> 读取客户端发来的 token -> 验证 token,并(可能)生成一个新的用于通信的 token -> 将 token 发给客户端

这里的认证过程简单到甚至没有出现认证服务器,基于这样的一套通用 API 去实现其他应用就相对轻松多了。Kerberos 内部的通信细节,多次传输的各种密文全部都隐藏在这样的 API 实现中。具体的 GSS API 使用代码示例。

SPNEGO

由于 GSS API 设计可以支持多种安全协议,另一个想法会自然的冒出来。我们可以让服务器支持多种认证协议,然后具体用哪种,由客户端和服务器端协商决定。这就使得我们在开发应用时可以给最终的用户提供选择,便于使用他或她所偏好使用的认证方式,从而带来更好的用户体验。同时,服务器和客户端在各自实现时,也可以相互独立的增量式的添加或去掉对于某一具体协议的支持,而不用完全同步的进行修改。这对于同一个服务器要支持多个版本的客户端而言会很有用。

这就是 SPNEGO 了,其全称是 Simple and Protected GSSAPI Negotiation Mechanism,即基于 GSS API 实现的一套简单的协议协商机制。这一协议由微软最早提出并应用在 windows 操作系统中,与我们最贴近的应用,当属于浏览器的系统集成认证了。大家回忆一下我们使用 IE 浏览器的体验,可以发现,很多网站可以直接使用系统的域账户登录。这就是用 SPNEGO 协议实现的浏览器系统集成认证。在企业中,如果我们为所有员工配置了 windows 域账户,而当我们有一些基于 web 的企业应用需要认证时,就可以利用这一机制实现无感知的认证。其实不只是 IE 浏览器,Firefox Chrome 等主流浏览器基本上都实现了这样的系统集成登录机制。

这个协议的通信过程大致为:

  1. Client 向 Server 请求服务
  2. Server 检查 Client 是否有提供有效的认证信息:如果没有,返回消息(包括服务器支持的认证方式)给 Client,以便 Client 可以完成认证;如果有,就提供服务
  3. Client 完成认证之后,向 Server 请求服务,并带上认证信息
  4. 回到第二步中进行认证检查,直到通过或认证次数达到阈值为止

Hadoop 认证机制

介绍了这么多,其实都是为了我们分析 Hadoop 的认证机制实现。到这里,相信大家应该也猜到了,在 Hadoop 的认证中,各个节点的通信实际上使用的就是 GSS API 去实现的基于 Kerberos 协议的单点认证。而 Hadoop 对外提供的很多基于 web 的应用,比如 Web HDFS、统计信息页面、Yarn Application 管理等等,其认证都是基于 SPNEGO 协议的。这两个协议的配置其实在我们后续配置 Hadoop 认证时也是最主要的配置了。

相关源代码分析

(下面的内容请大家结合源代码一起分析,仅仅读文字可能有很多内容会难以理解)

GSS API 中的 Kerberos 实现

我们打开 OpenJDK 的源代码库,浏览到下面这里的代码:

这里的代码量还是挺大的,细节很多,我们一起看一下主要的设计。GSS API 在 Java 语言中通过 jgss 模块来实现。jgss 首先定义了一些底层认证机制需要实现的接口,即 sun.security.jgss.spi 包中的基本接口 GSSContextSpi GSSNameSpi GSSCredentialSpi 和工厂接口 MechanismFactory。底层的协议只需要实现这几个接口就行了,关于 Kerberos 的实现在包 sun.security.jgss.krb5 中,其实这个包里面的代码只是对接了真正的 Kerberos 通信协议实现和 GSS API 接口。这里的设计,按照 DDD 的思想,我们可以理解为一套防腐层,GSS API 和 Kerberos 可以看成两个独立的领域,通过引入防腐层,它们就可以相互独立的各自演进。当接口有改变的时候,我们只需要修改防腐层的代码就行了。

真正的 Kerberos 协议实现在包 sun.security.krb5 下面,这里的实现通过 javax.security.auth.kerberos 包下面的类对应用层暴露接口(应用层在使用 GSS API 时,有时还是需要关心底层认证机制的相关信息的)。作为应用层,如果有必要获取底层认证机制相关的信息,我们将只使用 javax.security.auth.kerberos 中定义的接口,而无需关心 sun 包下面的实现。这里的实现的核心代码在 Credentials 类中,我们看到其定义了 acquireTGTFromCache acquireDefaultCreds acquireServiceCreds 等接口用于交换秘钥。更细节的实现代码,大家如有兴趣,可以结合上一篇文章中的通信流程自行研究。我们这里只简要分析一下主要的设计思想。

Hadoop 中使用 GSS API 进行认证

Hadoop 中和认证相关的模块主要有两个:一个是直接使用 GSS API 进行认证,用于 tcp 通信的 org.apache.hadoop.security.UserGroupInformation 类;另一个是基于 SPNEGO 协议进行认证,用于 HTTP 通信的 org.apache.hadoop.security.authentication.server.KerberosAuthenticationHandler

UserGroupInformation 主要用于 Hadoop 各个内部模块间的通信,也可以用于某一个客户端和 Hadoop 的某个模块进行通信,它同时为服务器和客户端的认证提供了支持。比如 NameNode 的启动之后,它将发起一个登陆请求,用于验证给自己配置的 Principal 和 keytab 是否有效(这里 108 行)。同时当有内部服务(如某个 datanode)的 rpc 请求到来的时候,它将使用登陆得到的认证主体 Subject 中的 doAs 方法来验证发送过来的认证信息,并进行权限验证。有客户端的 rpc 请求到来时,它将获取客户端的用户信息,并根据配置的 ACL(访问控制列表)进行权限验证(实现见这里的 1287 行,及这里)。为了缓存认证信息,避免没必要的重新认证,程序需要维护当前登录的账号的信息,这也就是为什么 UserGroupInformation 在设计上定义了很多静态的属性。同时我们可以注意到很多 synchronized 关键字附加到了某些静态方法上,这是为了支持多线程访问这些全局缓存的信息。

KerberosAuthenticationHandler 的实现是为了支持在 HTTP 服务中进行 Kerberos 认证,这个类最终会封装为一个 Web 服务器中的 Filter 实现对所有 HTTP 请求的权限验证(这里的 AuthFilter 及其基类 AuthenticationFilter)。由于基于 Servlet 的 Web 服务器有很成熟的接口设计,这个模块的实现也相对独立和简单。可以看到它在 init 的时候使用 GSS API 完成了登录,在 authenticate 的时候,将判断是否有有效的认证信息,如果没有将返回协商认证的 HTTP 头部消息以便客户端去完成认证,如果有将进行认证并提供服务。

Web 服务器认证实现示例

对于一个运行于 Hadoop 集群的 Spark 应用,我们通常是通过 spark-submit 命令行工具来向集群提交任务的。这一机制对于 spark 应用的开发者看起来很灵活,但如果我们想进行更多的统一管理,比如限制资源使用、提升易用性等等,这样的机制就略显不足了。这个时候一般的做法是将运行 spark 应用的这一能力封装为一个服务,以便进行统一的管理。Livy 就是为实现这样的功能而开发的一个开源工具。

使用 Livy,我们可以使用 REST 的接口向集群提交 spark 任务。在这里 Livy 其实相当于是整个 Hadoop 大数据集群的一个扩展服务。Livy 在实现的时候如何进行权限的支持呢?当我们去查看 Livy 的源代码的时候,我们会发现,要为每个请求添加 Kerberos 认证,几乎只需要一行代码,这里的 237 行即为那行关键的代码。这里 Livy 就是有效的利用了上面的 KerberosAuthenticationHandler 进行实现的。

搭建Hadoop安全认证

简单起见,我们这里的集群所有的组件将运行在同一台机器上。对于 keytab 的配置,我们也从简,只配置一个 kerberos 的 service 账号供所有服务使用。

建立测试用例

TDD 是敏捷最重要的实践之一,可以有效的帮助我们确定目标,验证目标,它可以带领我们走得又快又稳。跟随 TDD 的思想,我们先从测试的角度来看这个问题。有了前面的基础知识,假设我们已经有了一套安全的 Hadoop 集群,那么我们应当可以从集群读写文件,运行 MapReduce 任务。我们可以编写读写文件的测试用例如下:

public class HdfsTest {
    TestConfig testConfig = new TestConfig();

    
    public void should_read_write_files_from_hdfs() throws IOException {
        testConfig.configKerberos();

        Configuration conf = new Configuration();
        conf.addResource(new Path(testConfig.hdfsSiteFilePath()));
        conf.addResource(new Path(testConfig.coreSiteFilePath()));
        UserGroupInformation.setConfiguration(conf);
        UserGroupInformation.loginUserFromKeytab(testConfig.keytabUser(), testConfig.keytabFilePath());

        FileSystem fileSystem = FileSystem.get(conf);
        Path path = new Path("/user/root/input/core-site.xml");
        if (fileSystem.exists(path)) {
            boolean deleteSuccess = fileSystem.delete(path, false);
            assertTrue(deleteSuccess);
        }

        String fileContent = FileUtils.readFileToString(new File(testConfig.coreSiteFilePath()));
        try (FSDataOutputStream fileOut = fileSystem.create(path)) {
            fileOut.write(fileContent.getBytes("utf-8"));
        }

        assertTrue(fileSystem.exists(path));

        try (FSDataInputStream in = fileSystem.open(path)) {
            String fileContentRead = IOUtils.toString(in);
            assertEquals(fileContent, fileContentRead);
        }

        fileSystem.close();
    }
}

(完整代码请参考这里

到这里我们的任务目标就明确了,只要上面的测试能通过,我们的集群就应该搭建好了。

(如果有条件,下面的内容请大家结合代码及参考文档,一边读文章,一边动手实践,否则可能会遗漏很多细节。)

建立基本集群

我们先跟随官网的教程搭建一个非安全的集群。

这里我选择的 Hadoop 版本为 2.7.7(我这里是为了和实际项目中用到的版本保持一致,大家可以自行尝试其他版本,思路和大部分的脚本都应该是相同的)。我们选择伪分布式模式(Pseudo-Distributed)来进行尝试,这种模式下,每个组件会运行为一个独立的 java 进程,与真实的分布式环境类似。

我们还是使用容器来进行试验,启动一个容器,并依次运行下面的命令:

(注意,下面如果使用docker跑容器,那么要外部访问的话就把所有的端口暴露出来,这里我建议直接在虚拟机上面跑)

docker run -it --name shd -h shd centos:7 bash

需要的安装包

链接:https://pan.baidu.com/s/1hJogxtc4Kz5nk90VdZN0aA 
提取码:yyds 
--来自百度网盘超级会员V5的分享

在容器中运行下面的命令:

# 建立并切换到我们的工作目录
mkdir /hd && cd /hd
# 下载软件、解压、进入根目录
yum install wget vim less -y
wget https://archive.apache.org/dist/hadoop/common/hadoop-2.7.7/hadoop-2.7.7.tar.gz
tar xf hadoop-2.7.7.tar.gz
ln -sv hadoop-2.7.7/ hadoop
cd hadoop
# 配置hadoop,这里的shd修改成自己的域名或者是ip,我的是hadoop104,根据自己的情况修改
echo hadoop104 > etc/hadoop/slaves
cat > etc/hadoop/core-site.xml << EOF
<configuration>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://0.0.0.0:9000</value>
    </property>
</configuration>
EOF
cat > etc/hadoop/hdfs-site.xml << EOF
<configuration>
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>/hd/data/hdfs/namenode</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>/hd/data/hdfs/datanode</value>
    </property>
</configuration>
EOF
# 配置ssh,测试:是否能通过`ssh localhost`免密登录
yum install openssh-clients openssh-server -y
echo 'root:screencast' | chpasswd
sed -i 's/PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
sed 's@session\s*required\s*pam_loginuid.so@session optional pam_loginuid.so@g' -i /etc/pam.d/sshd
echo "export VISIBLE=now" >> /etc/profile
ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key -P '' && ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key -P ''
/usr/sbin/sshd
ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa
cat ~/.ssh/id_rsa.pub >> ~/.ssh/authorized_keys
chmod 0600 ~/.ssh/authorized_keys
# 安装jdk,并配置环境变量
yum install -y java-1.8.0-openjdk-devel
echo 'export JAVA_HOME=/usr/lib/jvm/java' >> ~/.bashrc
export JAVA_HOME=/usr/lib/jvm/java
# 启动hdfs
bin/hdfs namenode -format
sbin/start-dfs.sh
# 测试
bin/hdfs dfs -mkdir /user
bin/hdfs dfs -mkdir /user/root
bin/hdfs dfs -put etc/hadoop /input
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar grep /input /output 'dfs[a-z.]+'
bin/hdfs dfs -cat /output/*  # 这里的结果将显示配置文件里面关于dfs的内容

到这里我们的非安全的单机模式集群应该就能运行起来了。但是在这个集群里面我们还没法运行分布式任务,因为目前仅仅是一个 HDFS 分布式文件系统。如果用 jps 查看一下有哪些 java 进程,将发现我们启动了三个进程 NameNode SecondaryNameNode DataNode

下一步,我们还需要配置并启动用于管理分布式集群任务的关键组件 Yarn。运行如下这些命令,即可启动 Yarn

# 配置Yarn
cat > etc/hadoop/mapred-site.xml << EOF
<configuration>
    <property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>
    <property>
        <name>mapreduce.jobhistory.address</name>
        <value>0.0.0.0:10020</value>
    </property>
    <property>
        <name>mapreduce.jobhistory.webapp.address</name>
        <value>0.0.0.0:19888</value>
    </property>
</configuration>
EOF
cat > etc/hadoop/yarn-site.xml << EOF
<configuration>
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>
    <property>
        <name>yarn.log-aggregation-enable</name>
        <value>true</value>
    </property>
    <!-- fix node unhealthy issue -->
    <!-- `yarn node -list -all` report node unhealthy with message indicate no disk space (disk space check failed) -->
    <property>
        <name>yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage</name>
        <value>99.9</value>
    </property>
    <!-- to fix issue: 'Failed while trying to construct...' (http://blog.51yip.com/hadoop/2066.html) -->
    <property>
         <name>yarn.log.server.url</name>
         <value>http://hadoop104:19888/jobhistory/logs</value>
    </property>
</configuration>
EOF
#文件修改完以后记得查看下是否修改成功
# 启动Yarn:启动之后我们将能通过`./bin/yarn node -list -all`查看到一个RUNNIN的node
sbin/start-yarn.sh
# 启动History server用于查看应用日志
sbin/mr-jobhistory-daemon.sh start historyserver
# 测试:我们将能看到下面的命令从0%到100%按进度完成。
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar  wordcount /input/* /output/wc/
# 验证:运行`./bin/hadoop dfs -cat /output/wc/part-r-00000`还将看到计算出来的结果。
# 验证:运行`./bin/yarn application -list -appStates FINISHED`可以看到已运行完成的任务,及其日志的地址。

执行上面的命令启动 Yarn 及 historyserver 之后,我们将发现有三个额外的进程 ResourceManager NodeManager JobHistoryServer 随之启动了。

(注意,如果是直接虚拟机启动的话,那么直接访问对应的端口就行了)

如果我们的容器所在主机有一个浏览器可以用,那么我们可以通过访问 http://${SHD_DOCKER_IP}:8088/cluster/apps 将能看到上面的 wordcount 程序运行的状态及日志。这里的 SHD_DOCKER_IP 可以通过下面的命令查找出来。

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' shd

如果容器是在一个远端的主机上面启动的,我们可以用 ssh tunnel 的方式建立一个代理,通过代理来访问我们的集群。运行命令 ssh -f -N -D 127.0.0.1:3128 ${USER}@${REMOTE_DOCKER_HOST_IP} 即可建立这样的代理。然后我们运行 echo "${SHD_DOCKER_IP} shd" >> /etc/hosts 将容器的主机名加入到我们本地的 hosts。再使用 firefox 浏览器来配置代理(如下图),这样我们就可以通过本地的 firefox 来访问到远端的集群了。

 

我们将能看到如下的 web 应用,通过这个 web 应用,我们实际上还可以查询到更多的集群相关的信息。

http://hadoop104:50070/dfshealth.html#tab-overview

http://hadoop104:8088/cluster

可以看到,经过多年的优化,即便是一个非常复杂的分布式系统,我们现在也可以快速的上手了。几乎所有的配置都有相对合理的默认值,我们仅仅需要调整很少的配置。

Hadoop 本身内置了很多实用的工具,当我们遇到问题的时候,这些工具可以有效的辅助诊断问题。如果大家经过上面的步骤还是没法通过测试(命令行中的测试)。大家可能可以从以下几个方面去查找问题:

  1. 检查各个组件进程是否都启动起来了
  2. 检查各个组件的日志,比如,如果 datanode 启动失败,可能我们要查看 logs/hadoop-root-datanode-shd.log 日志做进一步分析
  3. 使用 bin/yarn node -list -all 检查 node 的状态
  4. 检查最终生成的配置 http://172.17.0.12:8042/conf 是否是我们所希望的,比如我们可能由于拼写错误导致配置不对

Kerberos 安全配置

在本系列第一篇文章中,我们尝试了搭建一个 kerberos 认证服务器,这里我们可以用与之前一致的方式先搭建起一个 kerberos 认证服务器。需要的执行脚本如下:

# 将kdc kdc.hadoop.com加入hosts,以便后续进行基于hosts文件的主机名解析
yum install net-tools -y
ip_addr=$(ifconfig eth0 | grep inet | awk '{print $2}')
echo "$ip_addr kdc-server kdc-server.hadoop.com" >> /etc/hosts

# 安装相关软件并进行配置
yum install krb5-server krb5-libs krb5-workstation -y
# 创建krb5配置文件,详细配置解释请参考:https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/krb5_conf.html
cat > /etc/krb5.conf <<EOF
#Configuration snippets may be placed in this directory as well
includedir /etc/krb5.conf.d/

[logging]
  default = FILE:/var/log/krb5.log
  kdc = FILE:/var/log/krb5kdc.log
  admin_server = FILE:/var/log/kadmind.log

[libdefaults]
  forcetcp = true
  default_realm = HADOOP.COM
  dns_lookup_realm = false
  dns_lookup_kdc = false
  ticket_lifetime = 24h
  renew_lifetime = 7d
  forwardable = true
  udp_preference_limit = 1
  default_tkt_enctypes = des-cbc-md5 des-cbc-crc des3-cbc-sha1
  default_tgs_enctypes = des-cbc-md5 des-cbc-crc des3-cbc-sha1
  permitted_enctypes = des-cbc-md5 des-cbc-crc des3-cbc-sha1

[realms]
  HADOOP.COM = {
    kdc = kdc-server.hadoop.com:2802
    admin_server = kdc-server.hadoop.com:2801
    default_domain = hadoop.com
  }

[domain_realm]
  .hadoop.com = HADOOP.COM
  hadoop.com = HADOOP.COM
EOF
# 创建kdc配置文件,详细配置解释请参考:https://web.mit.edu/kerberos/krb5-1.12/doc/admin/conf_files/kdc_conf.html
cat > /var/kerberos/krb5kdc/kdc.conf <<EOF
default_realm = HADOOP.COM

[kdcdefaults]
 kdc_ports = 0
 v4_mode = nopreauth

[realms]
 HADOOP.COM = {
    kdc_ports = 2800
    kdc_tcp_ports = 2802
    admin_keytab = /etc/kadm5.keytab
    database_name = /var/kerberos/krb5kdc/principal
    acl_file = /var/kerberos/krb5kdc/kadm5.acl
    key_stash_file = /var/kerberos/krb5kdc/stash
    max_life = 10h 0m 0s
    max_renewable_life = 7d 0h 0m 0s
    master_key_type = des3-hmac-sha1
    supported_enctypes = arcfour-hmac:normal des3-hmac-sha1:normal des-cbc-crc:normal des:normal des:v4 des:norealm des:onlyrealm des:afs3
    default_principal_flags = +preauth
}
EOF

echo -e '123456\n123456' | kdb5_util create -r HADOOP.COM -s  # 创建一个名为HADOOP.COM的域
/usr/sbin/krb5kdc && /usr/sbin/kadmind                        # 启动kdc及kadmind服务

配置 Hadoop 安全支持

前面我们分析了 Kerberos 的运行原理,及 Hadoop 的相关源代码,可以知道,为了启动安全支持,每一个集群节点的每一个 hadoop 组件都将需要单独的 Kerberos 账号及其 keytab 文件,每个组件最好还能用不同的账户启动。这里由于我们使用伪分布式模式来部署集群,所有的组件都运行在同一个节点,简单起见,我们这里将使用 root 账号来启动集群,并让所有的组件使用同一个 kerberos 账号。

首先我们生成账号如下:

mkdir /hd/conf/
# 生成hadoop集群需要的账号
kadmin.local addprinc -randkey root/hadoop104@HADOOP.COM
kadmin.local addprinc -randkey HTTP/hadoop104@HADOOP.COM
kadmin.local xst -k /hd/conf/hadoop.keytab root/hadoop104@HADOOP.COM HTTP/hadoop104@HADOOP.COM
# 生成测试用的普通账号
kadmin.local addprinc -randkey root@HADOOP.COM
kadmin.local xst -k /hd/conf/root.keytab root@HADOOP.COM

接下来我们来完成 hadoop 的配置,由于配置文件内容比较多,我统一整理到了 github 的一个 repo 中,下面的配置将主要通过 copy 这些文件来生成,而辅以说明主要修改的地方。如果大家有兴趣知道确切的修改之处,可以备份这些文件,然后用 diff 来查看修改,或者用 git 对配置文件进行版本管理,然后查看修改。

配置集群

可以运行命令也可以直接修改配置文件

配置 core-site.xml

wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/core-site.xml -O etc/hadoop/core-site.xml
sed -i 's/hd01-7/hadoop104/g' etc/hadoop/core-site.xml
<configuration>
    <property>
        <name>fs.defaultFS</name>
        <value>hdfs://hadoop104:9000</value>
    </property>

    <property>
        <name>hadoop.proxyuser.root.hosts</name>
        <value>*</value>
    </property>
    <property>
        <name>hadoop.proxyuser.root.groups</name>
        <value>*</value>
    </property>

    <property>
        <name>hadoop.proxyuser.HTTP.hosts</name>
        <value>*</value>
    </property>
    <property>
        <name>hadoop.proxyuser.HTTP.groups</name>
        <value>*</value>
    </property>


    <property>
      <name>hadoop.security.authorization</name>
      <value>true</value>
    </property>
    <property>
      <name>hadoop.security.authentication</name>
      <value>kerberos</value>
    </property>

</configuration>

这里主要加入的配置项及其解释如下:

hadoop.proxyuser.root.hosts=*           # 配置root用户(组件启动时认证的kerberos账户)可以以任意客户端认证过的用户(proxy user)来执行操作,详见:https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/Superusers.html
hadoop.proxyuser.root.groups=*
hadoop.proxyuser.HTTP.hosts=*
hadoop.proxyuser.HTTP.groups=*
hadoop.security.authorization=true
hadoop.security.authentication=kerberos

配置 hdfs-site.xml

wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/hdfs-site.xml -O etc/hadoop/hdfs-site.xml
sed -i 's/hd01-7/hadoop104/g' etc/hadoop/hdfs-site.xml

<configuration>
    <property>
        <name>dfs.replication</name>
        <value>1</value>
    </property>
    <property>
        <name>dfs.namenode.name.dir</name>
        <value>/hd/data/hdfs/namenode</value>
    </property>
    <property>
        <name>dfs.datanode.data.dir</name>
        <value>/hd/data/hdfs/datanode</value>
    </property>

    <property>
        <name>dfs.block.access.token.enable</name>
        <value>true</value>
    </property>
    <property>
        <name>dfs.namenode.keytab.file</name>
        <value>/hd/conf/hadoop.keytab</value>
    </property>
    <property>
        <name>dfs.namenode.kerberos.principal</name>
        <value>root/_HOST@HADOOP.COM</value>
    </property>
    <property>
        <name>dfs.namenode.kerberos.internal.spnego.principal</name>
        <value>HTTP/_HOST@HADOOP.COM</value>
    </property>
    <property>
        <name>dfs.web.authentication.kerberos.principal</name>
        <value>HTTP/_HOST@HADOOP.COM</value>
    </property>
    <property>
        <name>dfs.web.authentication.kerberos.keytab</name>
        <value>/hd/conf/hadoop.keytab</value>
    </property>
    <property>
        <name>dfs.datanode.keytab.file</name>
        <value>/hd/conf/hadoop.keytab</value>
    </property>
    <property>
        <name>dfs.datanode.kerberos.principal</name>
        <value>root/_HOST@HADOOP.COM</value>
    </property>

    <property>
        <name>dfs.datanode.address</name>
        <value>hadoop104:1004</value>
    </property>
    <property>
        <name>dfs.datanode.http.address</name>
        <value>hadoop104:1006</value>
    </property>

    <property>
        <name>dfs.journalnode.keytab.file</name>
        <value>/hd/conf/hadoop.keytab</value>
    </property>
    <property>
        <name>dfs.journalnode.kerberos.principal</name>
        <value>root/_HOST@HADOOP.COM</value>
    </property>
    <property>
        <name>dfs.journalnode.kerberos.internal.spnego.principal</name>
        <value>HTTP/_HOST@HADOOP.COM</value>
    </property>


</configuration>

这里主要加入的配置项如下:

dfs.block.access.token.enable=true
dfs.namenode.keytab.file=/hd/conf/hadoop.keytab
dfs.namenode.kerberos.principal=root/_HOST@HADOOP.COM
dfs.namenode.kerberos.internal.spnego.principal=HTTP/_HOST@HADOOP.COM
dfs.web.authentication.kerberos.principal=HTTP/_HOST@HADOOP.COM
dfs.web.authentication.kerberos.keytab=/hd/conf/hadoop.keytab
dfs.datanode.keytab.file=/hd/conf/hadoop.keytab
dfs.datanode.kerberos.principal=root/_HOST@HADOOP.COM
dfs.datanode.address=hadoop104:1004
dfs.datanode.http.address=hadoop104:1006
dfs.journalnode.keytab.file=/hd/conf/hadoop.keytab
dfs.journalnode.kerberos.principal=root/_HOST@HADOOP.COM
dfs.journalnode.kerberos.internal.spnego.principal=HTTP/_HOST@HADOOP.COM

配置 mapred-site.xml

wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/mapred-site.xml -O etc/hadoop/mapred-site.xml
sed -i 's/hd01-7/hadoop104/g' etc/hadoop/mapred-site.xml
<configuration>
<property>
        <name>mapreduce.framework.name</name>
        <value>yarn</value>
    </property>



<!-- to fix issue: 'Failed while trying to construct...' (http://blog.51yip.com/hadoop/2066.html) -->
    <property>
        <name>mapreduce.jobhistory.address</name>
        <value>hadoop104:10020</value>
    </property>
    <property>
        <name>mapreduce.jobhistory.webapp.address</name>
        <value>hadoop104:19888</value>
    </property>

<!-- kerberos auth -->
    <property>
      <name>mapreduce.jobhistory.principal</name>
      <value>root/_HOST@HADOOP.COM</value>
    </property>
    <property>
      <name>mapreduce.jobhistory.keytab</name>
      <value>/hd/conf/hadoop.keytab</value>
    </property>


</configuration>

这里主要加入的配置项如下:

mapreduce.jobhistory.address=hadoop104:10020
mapreduce.jobhistory.webapp.address=hadoop104:19888
mapreduce.jobhistory.principal=root/_HOST@HADOOP.COM
mapreduce.jobhistory.keytab=/hd/conf/hadoop.keytab

配置 yarn-site.xml

wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/yarn-site.xml -O etc/hadoop/yarn-site.xml
sed -i 's/hd01-7/hadoop104/g' etc/hadoop/yarn-site.xml
<configuration>

<!-- Site specific YARN configuration properties -->
    <property>
        <name>yarn.nodemanager.aux-services</name>
        <value>mapreduce_shuffle</value>
    </property>

    <property>
        <name>yarn.resourcemanager.hostname</name>
        <value>hadoop104</value>
    </property>
    <property>
        <name>yarn.log-aggregation-enable</name>
        <value>true</value>
    </property>

<!-- fix node unhealthy issue -->
<!-- `yarn node -list -all` report node unhealthy with message indicate no disk space (disk space check failed) -->
    <property>
        <name>yarn.nodemanager.disk-health-checker.max-disk-utilization-per-disk-percentage</name>
        <value>99.9</value>
    </property>

<!-- fix spark job submit issue -->
    <property>
        <name>yarn.nodemanager.pmem-check-enabled</name>
        <value>false</value>
    </property>

    <property>
        <name>yarn.nodemanager.vmem-check-enabled</name>
        <value>false</value>
    </property>


<!-- to fix issue: 'Failed while trying to construct...' (http://blog.51yip.com/hadoop/2066.html) -->
    <property>
         <name>yarn.log.server.url</name>
         <value>http://hadoop104:19888/jobhistory/logs</value>
    </property>


<!-- kerberose auth -->
    <property>
      <name>yarn.resourcemanager.principal</name>
      <value>root/_HOST@HADOOP.COM</value>
    </property>
    <property>
      <name>yarn.resourcemanager.keytab</name>
      <value>/hd/conf/hadoop.keytab</value>
    </property>
    <property>
      <name>yarn.resourcemanager.webapp.https.address</name>
      <value>${yarn.resourcemanager.hostname}:8090</value>
    </property>

    <property>
      <name>yarn.nodemanager.principal</name>
      <value>root/_HOST@HADOOP.COM</value>
    </property>
    <property>
      <name>yarn.nodemanager.keytab</name>
      <value>/hd/conf/hadoop.keytab</value>
    </property>

    <!-- <property>
               <name>yarn.nodemanager.container-executor.class</name>
      <value>org.apache.hadoop.yarn.server.nodemanager.LinuxContainerExecutor</value>
    </property>
    <property>
      <name>yarn.nodemanager.linux-container-executor.group</name>
      <value>root</value>
    </property>
    <property>
      <name>yarn.nodemanager.linux-container-executor.path</name>
      <value></value>
    </property> -->

    <property>
      <name>yarn.web-proxy.principal</name>
      <value>root/_HOST@HADOOP.COM</value>
    </property>
    <property>
      <name>yarn.web-proxy.keytab</name>
      <value>/hd/conf/hadoop.keytab</value>
    </property>


</configuration>

这里主要加入的配置项如下:

yarn.resourcemanager.principal=root/_HOST@HADOOP.COM
yarn.resourcemanager.keytab=/hd/conf/hadoop.keytab
yarn.resourcemanager.webapp.https.address=${yarn.resourcemanager.hostname}:8090
yarn.nodemanager.principal=root/_HOST@HADOOP.COM
yarn.nodemanager.keytab=/hd/conf/hadoop.keytab
yarn.web-proxy.principal=root/_HOST@HADOOP.COM
yarn.web-proxy.keytab=/hd/conf/hadoop.keytab

配置 hadoop-env.sh

wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/etc/hadoop/hadoop-env.sh -O etc/hadoop/hadoop-env.sh
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements.  See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership.  The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# Set Hadoop-specific environment variables here.

# The only required environment variable is JAVA_HOME.  All others are
# optional.  When running a distributed configuration it is best to
# set JAVA_HOME in this file, so that it is correctly defined on
# remote nodes.

# The java implementation to use.
export JAVA_HOME=${JAVA_HOME}
export JAVA_HOME=/usr/lib/jvm/java

export JSVC_HOME=/usr/bin


# The jsvc implementation to use. Jsvc is required to run secure datanodes
# that bind to privileged ports to provide authentication of data transfer
# protocol.  Jsvc is not required if SASL is configured for authentication of
# data transfer protocol using non-privileged ports.
#export JSVC_HOME=${JSVC_HOME}

export HADOOP_CONF_DIR=${HADOOP_CONF_DIR:-"/etc/hadoop"}

# Extra Java CLASSPATH elements.  Automatically insert capacity-scheduler.
for f in $HADOOP_HOME/contrib/capacity-scheduler/*.jar; do
  if [ "$HADOOP_CLASSPATH" ]; then
    export HADOOP_CLASSPATH=$HADOOP_CLASSPATH:$f
  else
    export HADOOP_CLASSPATH=$f
  fi
done

# The maximum amount of heap to use, in MB. Default is 1000.
#export HADOOP_HEAPSIZE=
#export HADOOP_NAMENODE_INIT_HEAPSIZE=""

export HADOOP_JAAS_DEBUG=true
export HADOOP_OPTS="-Djava.net.preferIPv4Stack=true -Dsun.security.krb5.debug=true -Dsun.security.spnego.debug"
export HADOOP_SECURE_DN_USER=root
export HADOOP_HDFS_USER=root

# Extra Java runtime options.  Empty by default.
export HADOOP_OPTS="$HADOOP_OPTS -Djava.net.preferIPv4Stack=true"

# Command specific options appended to HADOOP_OPTS when specified
export HADOOP_NAMENODE_OPTS="-Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,RFAS} -Dhdfs.audit.logger=${HDFS_AUDIT_LOGGER:-INFO,NullAppender} $HADOOP_NAMENODE_OPTS"
export HADOOP_DATANODE_OPTS="-Dhadoop.security.logger=ERROR,RFAS $HADOOP_DATANODE_OPTS"

export HADOOP_SECONDARYNAMENODE_OPTS="-Dhadoop.security.logger=${HADOOP_SECURITY_LOGGER:-INFO,RFAS} -Dhdfs.audit.logger=${HDFS_AUDIT_LOGGER:-INFO,NullAppender} $HADOOP_SECONDARYNAMENODE_OPTS"

export HADOOP_NFS3_OPTS="$HADOOP_NFS3_OPTS"
export HADOOP_PORTMAP_OPTS="-Xmx512m $HADOOP_PORTMAP_OPTS"

# The following applies to multiple commands (fs, dfs, fsck, distcp etc)
export HADOOP_CLIENT_OPTS="-Xmx512m $HADOOP_CLIENT_OPTS"
#HADOOP_JAVA_PLATFORM_OPTS="-XX:-UsePerfData $HADOOP_JAVA_PLATFORM_OPTS"

# On secure datanodes, user to run the datanode as after dropping privileges.
# This **MUST** be uncommented to enable secure HDFS if using privileged ports
# to provide authentication of data transfer protocol.  This **MUST NOT** be
# defined if SASL is configured for authentication of data transfer protocol
# using non-privileged ports.
export HADOOP_SECURE_DN_USER=${HADOOP_SECURE_DN_USER}

# Where log files are stored.  $HADOOP_HOME/logs by default.
#export HADOOP_LOG_DIR=${HADOOP_LOG_DIR}/$USER

# Where log files are stored in the secure data environment.
export HADOOP_SECURE_DN_LOG_DIR=${HADOOP_LOG_DIR}/${HADOOP_HDFS_USER}

###
# HDFS Mover specific parameters
###
# Specify the JVM options to be used when starting the HDFS Mover.
# These options will be appended to the options specified as HADOOP_OPTS
# and therefore may override any similar flags set in HADOOP_OPTS
#
# export HADOOP_MOVER_OPTS=""

###
# Advanced Users Only!
###

# The directory where pid files are stored. /tmp by default.
# NOTE: this should be set to a directory that can only be written to by 
#       the user that will run the hadoop daemons.  Otherwise there is the
#       potential for a symlink attack.
export HADOOP_PID_DIR=${HADOOP_PID_DIR}
export HADOOP_SECURE_DN_PID_DIR=${HADOOP_PID_DIR}

# A string representing this instance of hadoop. $USER by default.
export HADOOP_IDENT_STRING=$USER

主要加入的配置项如下:

export JSVC_HOME=/usr/bin             # 指定jsvc的路径,以便运行安全模式的datanode
export HADOOP_JAAS_DEBUG=true         # 开启Kerberos认证的debug日志
export HADOOP_OPTS="-Djava.net.preferIPv4Stack=true -Dsun.security.krb5.debug=true -Dsun.security.spnego.debug"  # 开启Kerberos认证的debug日志
export HADOOP_SECURE_DN_USER=root     # 运行安全模式的datanode组件的用户
export HADOOP_HDFS_USER=root          # 运行hdfs组件的用户

修复启动脚本

由于我们开启了 Kerberos 的调试日志,原来的脚本需要稍加修改才能使用。执行脚本如下:

wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/sbin/stop-dfs.sh -O sbin/stop-dfs.sh
wget https://raw.githubusercontent.com/gmlove/bigdata_conf/master/auth/hadoop/sbin/start-dfs.sh -O sbin/start-dfs.sh

stop-dfs.sh

#!/usr/bin/env bash

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

bin=`dirname "${BASH_SOURCE-$0}"`
bin=`cd "$bin"; pwd`

DEFAULT_LIBEXEC_DIR="$bin"/../libexec
HADOOP_LIBEXEC_DIR=${HADOOP_LIBEXEC_DIR:-$DEFAULT_LIBEXEC_DIR}
. $HADOOP_LIBEXEC_DIR/hdfs-config.sh

#---------------------------------------------------------
# namenodes

NAMENODES=$($HADOOP_PREFIX/bin/hdfs getconf -namenodes | tail -n 1)

echo "Stopping namenodes on [$NAMENODES]"

"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
  --config "$HADOOP_CONF_DIR" \
  --hostnames "$NAMENODES" \
  --script "$bin/hdfs" stop namenode

#---------------------------------------------------------
# datanodes (using default slaves file)

if [ -n "$HADOOP_SECURE_DN_USER" ]; then
  echo \
    "Attempting to stop secure cluster, skipping datanodes. " \
    "Run stop-secure-dns.sh as root to complete shutdown."
else
  "$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
    --config "$HADOOP_CONF_DIR" \
    --script "$bin/hdfs" stop datanode
fi

#---------------------------------------------------------
# secondary namenodes (if any)

SECONDARY_NAMENODES=$($HADOOP_PREFIX/bin/hdfs getconf -secondarynamenodes 2>/dev/null | tail -n 1)

if [ -n "$SECONDARY_NAMENODES" ]; then
  echo "Stopping secondary namenodes [$SECONDARY_NAMENODES]"

  "$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
      --config "$HADOOP_CONF_DIR" \
      --hostnames "$SECONDARY_NAMENODES" \
      --script "$bin/hdfs" stop secondarynamenode
fi

#---------------------------------------------------------
# quorumjournal nodes (if any)

SHARED_EDITS_DIR=$($HADOOP_PREFIX/bin/hdfs getconf -confKey dfs.namenode.shared.edits.dir 2>&- | tail -n 1)

case "$SHARED_EDITS_DIR" in
qjournal://*)
  JOURNAL_NODES=$(echo "$SHARED_EDITS_DIR" | sed 's,qjournal://\([^/]*\)/.*,\1,g; s/;/ /g; s/:[0-9]*//g')
  echo "Stopping journal nodes [$JOURNAL_NODES]"
  "$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
      --config "$HADOOP_CONF_DIR" \
      --hostnames "$JOURNAL_NODES" \
      --script "$bin/hdfs" stop journalnode ;;
esac

#---------------------------------------------------------
# ZK Failover controllers, if auto-HA is enabled
AUTOHA_ENABLED=$($HADOOP_PREFIX/bin/hdfs getconf -confKey dfs.ha.automatic-failover.enabled | tail -n 1)
if [ "$(echo "$AUTOHA_ENABLED" | tr A-Z a-z)" = "true" ]; then
  echo "Stopping ZK Failover Controllers on NN hosts [$NAMENODES]"
  "$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
    --config "$HADOOP_CONF_DIR" \
    --hostnames "$NAMENODES" \
    --script "$bin/hdfs" stop zkfc
fi
# eof

start-dfs.sh

#!/usr/bin/env bash

# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


# Start hadoop dfs daemons.
# Optinally upgrade or rollback dfs state.
# Run this on master node.

usage="Usage: start-dfs.sh [-upgrade|-rollback] [other options such as -clusterId]"

bin=`dirname "${BASH_SOURCE-$0}"`
bin=`cd "$bin"; pwd`

DEFAULT_LIBEXEC_DIR="$bin"/../libexec
HADOOP_LIBEXEC_DIR=${HADOOP_LIBEXEC_DIR:-$DEFAULT_LIBEXEC_DIR}
. $HADOOP_LIBEXEC_DIR/hdfs-config.sh

# get arguments
if [[ $# -ge 1 ]]; then
  startOpt="$1"
  shift
  case "$startOpt" in
    -upgrade)
      nameStartOpt="$startOpt"
    ;;
    -rollback)
      dataStartOpt="$startOpt"
    ;;
    *)
      echo $usage
      exit 1
    ;;
  esac
fi

#Add other possible options
nameStartOpt="$nameStartOpt $@"

#---------------------------------------------------------
# namenodes

NAMENODES=$($HADOOP_PREFIX/bin/hdfs getconf -namenodes | tail -n 1)

echo "Starting namenodes on [$NAMENODES]"

"$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
  --config "$HADOOP_CONF_DIR" \
  --hostnames "$NAMENODES" \
  --script "$bin/hdfs" start namenode $nameStartOpt

#---------------------------------------------------------
# datanodes (using default slaves file)

if [ -n "$HADOOP_SECURE_DN_USER" ]; then
  echo \
    "Attempting to start secure cluster, skipping datanodes. " \
    "Run start-secure-dns.sh as root to complete startup."
else
  "$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
    --config "$HADOOP_CONF_DIR" \
    --script "$bin/hdfs" start datanode $dataStartOpt
fi

#---------------------------------------------------------
# secondary namenodes (if any)

SECONDARY_NAMENODES=$($HADOOP_PREFIX/bin/hdfs getconf -secondarynamenodes 2>/dev/null  | tail -n 1)

if [ -n "$SECONDARY_NAMENODES" ]; then
  echo "Starting secondary namenodes [$SECONDARY_NAMENODES]"

  "$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
      --config "$HADOOP_CONF_DIR" \
      --hostnames "$SECONDARY_NAMENODES" \
      --script "$bin/hdfs" start secondarynamenode
fi

#---------------------------------------------------------
# quorumjournal nodes (if any)

SHARED_EDITS_DIR=$($HADOOP_PREFIX/bin/hdfs getconf -confKey dfs.namenode.shared.edits.dir 2>&-  | tail -n 1)

case "$SHARED_EDITS_DIR" in
qjournal://*)
  JOURNAL_NODES=$(echo "$SHARED_EDITS_DIR" | sed 's,qjournal://\([^/]*\)/.*,\1,g; s/;/ /g; s/:[0-9]*//g')
  echo "Starting journal nodes [$JOURNAL_NODES]"
  "$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
      --config "$HADOOP_CONF_DIR" \
      --hostnames "$JOURNAL_NODES" \
      --script "$bin/hdfs" start journalnode ;;
esac

#---------------------------------------------------------
# ZK Failover controllers, if auto-HA is enabled
AUTOHA_ENABLED=$($HADOOP_PREFIX/bin/hdfs getconf -confKey dfs.ha.automatic-failover.enabled | tail -n 1)
if [ "$(echo "$AUTOHA_ENABLED" | tr A-Z a-z)" = "true" ]; then
  echo "Starting ZK Failover Controllers on NN hosts [$NAMENODES]"
  "$HADOOP_PREFIX/sbin/hadoop-daemons.sh" \
    --config "$HADOOP_CONF_DIR" \
    --hostnames "$NAMENODES" \
    --script "$bin/hdfs" start zkfc
fi

# eof

主要修改为将通过 hdfs getconf SOME_CONFIG 命令拿到的配置,修改为通过 hdfs getconf SOME_CONFIG >/dev/null | tail -n 1 去获取配置。这里的 tail -n 1 可以去掉命令运行中的 Kerberos 调试日志。

启动集群

启动集群并运行测试如下:

yum install -y apache-commons-daemon-jsvc.x86_64     # 安装jsvc以便可以用安全模式启动datanode,详见:https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-common/SecureMode.html#Secure_DataNode
sbin/start-dfs.sh && ./sbin/start-secure-dns.sh && sbin/start-yarn.sh && sbin/mr-jobhistory-daemon.sh start historyserver     # 依次启动集群的其他组件
# 测试:我们将能看到下面的命令从0%到100%按进度完成。
bin/hadoop jar share/hadoop/mapreduce/hadoop-mapreduce-examples-2.7.7.jar  wordcount /input/* /output/wcc/
# 验证:运行`./bin/hadoop dfs -cat output/wc/part-r-00000`还将看到计算出来的结果。
# 验证:运行`./bin/yarn application -list -appStates FINISHED`可以看到已运行完成的任务,及其日志的地址。

如果我们无需再测试了,可以用以下命令停止集群:

sbin/stop-dfs.sh && ./sbin/stop-secure-dns.sh && sbin/stop-yarn.sh && sbin/mr-jobhistory-daemon.sh stop historyserver

运行最初定义的测试

执行命令如下:

# 加入相关的hosts
SHD_DOCKER_IP=$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' shd)
echo "${SHD_DOCKER_IP} shd kdc-server kdc-server.hadoop.com" >> /etc/hosts
# 下载源代码
git clone https://github.com/gmlove/bigdata_conf.git
# 更新配置文件
cd bigdata_conf
cd test/src/test && mv resources resources.1
docker cp shd:/hd/hadoop/etc/hadoop/hdfs-site.xml ./resources/
docker cp shd:/hd/hadoop/etc/hadoop/core-site.xml ./resources/
docker cp shd:/hd/hadoop/etc/hadoop/yarn-site.xml ./resources/
docker cp shd:/etc/krb5.conf ./resources/
docker cp shd:/hd/conf/root.keytab ./resources/
cp ./resources.1/log4j.properties ./resources/
# 运行测试
mvn -Dtest=test.HdfsTest test

运行上面的命令,我们将能看到测试成功执行。

如果容器在一个远端的主机上启动

如果容器是在一个远端的主机上面启动的,我们还是可以通过 ssh tunnel 的方式将远端的端口映射到本地来执行此测试。不过,我们需要对前面步骤中的内容作出一些修改。主要的修改是将涉及到的 hostname 配置从 shd 改为 localhost。这是由于在做端口映射之后,所有的服务均会通过 localhost 来访问,如果我们还是用 shd,则集群在进行 Kerberos 认证时,主机名验证会出错。

这个任务还是挺有意思的,可以有效的检验我们对于网络、Hadoop 集群、Kerberos 认证机制等的理解。有兴趣的小伙伴可以尝试实验一下,本文就不赘述了。

总结

搭建一套安全的 hadoop 集群,确实不容易,即使我们只是一个伪分布式环境,还做了各种配置简化,也需要花费一番功夫,更别提真正在生产环境中搭建一套集群了。如果是生产可用,我们可能还需要关心机架、集群网络情况、稳定性、性能、跨地域高可用、不停机升级等等一系列的问题。在实际企业应用中,这些大数据基础设施运维实际上是一个比较复杂的工作,这些工作更可能是由一个单独的运维团队去完成的。这里我们所完成的例子的主要价值不在于生产可用,而在于它可以帮助我们理解 hadoop 集群的安全机制,以便指导我们日常的开发工作。另一个价值是,这里的例子实际上完全可以作为我们平时测试用的一套小集群,简单而又功能完整,我们完全可以将这里完成的工作制作为一个 docker 镜像(后续文章将尝试制作此镜像),随时启动这样一套集群,这对于我们测试一些集群集成问题时将带来很大的便利。

大家如果有自己实践,相信在这个过程中可能还会碰到其他的问题,欢迎留言交流,一起学习。

在这篇文章里,我们搭建了一个安全的 hadoop 集群,那么大数据相关的其他组件应该要如何安全的和 hadoop 集群进行整合呢?下一篇文章我们将选取几个典型的组件来分析并进行实践,欢迎持续关注。

参考

Hadoop安全认证机制 (三) | Bright LGM's Blog

附录

Kerberos相关概念

 

Kerberos工作原理

      也就是说kdc里面存储了客户端还有服务的秘匙,客户端先请求kdc认证,得到服务端的私钥,然后通过服务端的私钥加密信息发送给服务端,服务端揭秘,用客户端的私钥加密发送给客户端,然后双方通过session key进行共同信任的加密揭秘秘钥串进行通信。还有就是他们与时间相关,默认session key的有效时间是8-10小时,所以服务器间的时间要尽量的同步,最少不超过5分钟。

Kerberos优点

Kerberos认证流程

实际操作一波 

前提条件

 

KDC相关操作

#域名前期规划
域名: mydomain.com
领域名: MYREALM.COM 

# 安装密匙分发中心(KDC)
yum install krb5-server krb5-libs krb5-workstation -y

#配置下/etc/hosts
vi /etc/hosts
192.168.10.102 kdc-server.hadoop.com

#配置KDC,krb5.conf是最高层的配置,配置KDC的位置,管理服务器,主机名与Kerberos领域名的映射
vi /etc/krb5.conf

#配置如下
# Configuration snippets may be placed in this directory as well
includedir /etc/krb5.conf.d/
 
[logging]
 default = FILE:/var/log/krb5.log
 kdc = FILE:/var/log/krb5kdc.log
 admin_server = FILE:/var/log/kadmind.log
  
[kdc]
 profile = /var/kerberos/krb5kdc/kdc.conf
 
[libdefaults]
 forwardable = true
 default_realm = MYREALM.COM
 dns_lookup_realm = false
 dns_lookup_kdc = false
 ticket_lifetime = 24h
 renew_lifetime = 7d
  
[realms]
  MYREALM.COM = {
    kdc = hadoop102
    admin_server = hadoop102
  }
 
[domain_realm]
 .mydomain.com = MYREALM.COM
 mydomain.com = MYREALM.COM


#kdc.conf文件包括kdc配置中Kerberos票据,领域相关配置,kdc数据库和登录详细信息
#修改配置
vi /var/kerberos/krb5kdc/kdc.conf

[kdcdefaults]
 kdc_ports = 88
 
[realms]
 MYREALM.COM  = {
    profile = /etc/krb5.conf
	supported_enctypes = arcfour-hmac:normal des3-hmac-sha1:normal des-cbc-crc:normal des:normal des:v4 des:norealm des:onlyrealm des:afs3
	allow-null-ticket-addresses = true
    database_name = /var/kerberos/krb5kdc/principal
	acl_file = /var/kerberos/krb5kdc/kadm4.acl
	admin_database_lockfile = /var/kerberos/krb5kdc/kadm5_adb.lock
	admin_keytab = FILE:/var/kerberos/krb5kdc/kadm5.keytab
	key_stash_file = /var/kerberos/krb5kdc/.k5stash
    kdc_ports = 88
	kadmind_port = 748
    max_life = 10h 0m 0s
    max_renewable_life = 7d 0h 0m 0s
}

 

KDC数据库 

#建立KDC数据库,用于存储用户密码信息,这个数据库可以是一个文件,或者是ldap存储
kdb5_util create -r MYREALM.COM -s

命令数据以后会初始下密码

cd /var/kerberos/krb5kdc/

我生成的文件没有下面描述的最后一个文件,没有影响 

 启动Kerberos守护进程

/usr/sbin/krb5kdc && /usr/sbin/kadmind

创建管理员标识和密码 

#创建管理员标识和密码,下面表示账号为admin,密码为admin,admin\nadmin这个表示第一次输入密码和第二次密码
echo -e 'admin\nadmin' | kadmin.local addprinc admin
#验证管理员认证确保KDC支持认证
kinit admin@MYREALM.COM

输入的密码就是刚才设置的admin 

 

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

工作变成艺术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值