0


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

机制。相比

JWT

Kerberos

是一个更为完整的认证协议,然而也正是因为其设计可以支持众多的功能,也给其理解和使用带来了困难。

这里之所以提到

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 并没有对 ClientSS 之间的通信协议进行限制,虽然和认证服务器进行通信需要基于 TCP/UDP,但 ClientSS 通信可以用任意协议进行

理解了上述通信流程之后,可以看到,相比

JWT

Kerberos

还进行了下面的额外验证:

  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 [email protected]           # 生成gml账号的keytab文件

kadmin.local addprinc -randkey root/[email protected]       # 创建名为root并和kdc主机进行绑定的服务账号
kadmin.local xst -k server.keytab root/[email protected]    # 创建用于服务器的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/[email protected]", "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. ClientServer 请求服务
  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/[email protected]
kadmin.local addprinc -randkey HTTP/[email protected]
kadmin.local xst -k /hd/conf/hadoop.keytab root/[email protected] HTTP/[email protected]
# 生成测试用的普通账号
kadmin.local addprinc -randkey [email protected]
kadmin.local xst -k /hd/conf/root.keytab [email protected]

接下来我们来完成 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/[email protected]</value>
    </property>
    <property>
        <name>dfs.namenode.kerberos.internal.spnego.principal</name>
        <value>HTTP/[email protected]</value>
    </property>
    <property>
        <name>dfs.web.authentication.kerberos.principal</name>
        <value>HTTP/[email protected]</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/[email protected]</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/[email protected]</value>
    </property>
    <property>
        <name>dfs.journalnode.kerberos.internal.spnego.principal</name>
        <value>HTTP/[email protected]</value>
    </property>

</configuration>

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

dfs.block.access.token.enable=true
dfs.namenode.keytab.file=/hd/conf/hadoop.keytab
dfs.namenode.kerberos.principal=root/[email protected]
dfs.namenode.kerberos.internal.spnego.principal=HTTP/[email protected]
dfs.web.authentication.kerberos.principal=HTTP/[email protected]
dfs.web.authentication.kerberos.keytab=/hd/conf/hadoop.keytab
dfs.datanode.keytab.file=/hd/conf/hadoop.keytab
dfs.datanode.kerberos.principal=root/[email protected]
dfs.datanode.address=hadoop104:1004
dfs.datanode.http.address=hadoop104:1006
dfs.journalnode.keytab.file=/hd/conf/hadoop.keytab
dfs.journalnode.kerberos.principal=root/[email protected]
dfs.journalnode.kerberos.internal.spnego.principal=HTTP/[email protected]

配置

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/[email protected]</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/[email protected]
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/[email protected]</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/[email protected]</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/[email protected]</value>
    </property>
    <property>
      <name>yarn.web-proxy.keytab</name>
      <value>/hd/conf/hadoop.keytab</value>
    </property>

</configuration>

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

yarn.resourcemanager.principal=root/[email protected]
yarn.resourcemanager.keytab=/hd/conf/hadoop.keytab
yarn.resourcemanager.webapp.https.address=${yarn.resourcemanager.hostname}:8090
yarn.nodemanager.principal=root/[email protected]
yarn.nodemanager.keytab=/hd/conf/hadoop.keytab
yarn.web-proxy.principal=root/[email protected]
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 [email protected]

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

标签: hadoop 大数据 hdfs

本文转载自: https://blog.csdn.net/S1124654/article/details/128791481
版权归原作者 顶尖高手养成计划 所有, 如有侵权,请联系我们删除。

“Hadoop安全之Kerberos”的评论:

还没有评论