Kerberos安全认证实践
  GQ7psP7UJw7k 2023年11月05日 48 0

1. 背景

客户端登陆服务端时,一般要进行认证。认证是指用户将自己的密码信息放到服务端,客户端访问服务端时,服务端检测到传输过来的密码和保存的密码一致,就认为用户有权访问服务端。如果不一致,那么用户无法通过密码证明自己的身份,就无权访问服务端。

这种传统的认证方式有以下问题:

  1. 服务端要额外进行任务工作,增加了服务端的代码复杂度。
  2. 客户端在网络中传输账号密码容易泄漏。

Kerberos认证系统就完美解决上述问题。

2. Kerberos认证系统介绍

2.1 Kerberos的账号和密码表示形式

Kerberos系统中的账号和密码不同与传统认证方式。如下所示:

  1. 为了避免服务端进行额外的认证工作,Kerberos独立出来一个Key Distribution Center(KDC)环境。客户端访问服务端时,会先访问KDC进行认证,如果认证通过,就访问服务端;如果认证失败,访问终止。这样服务端就不需要编写额外的认证逻辑。
  2. Kerberos对用户身份进行了细分。通过Kerberos principal(又称为主体)用于在kerberos加密系统中标记一个唯一的身份。principal分为三个部分:主名称、实例和领域。例如:joe/admin@EXAMPLE.COMhdfs/node2.example.com@EXAMPLE.COM。这三部分理论上可以随便设置,但是为了可读性,往往遵循一些规范:
    1. 主名称:一般表示具体的用户名和服务名称。joe/admin@EXAMPLE.COM 中joe就是主名称,他表示用户名job;hdfs/node2.example.com@EXAMPLE.COM 中hdfs就是主名称,它表示hdfs服务。可以随便设置名称,但是上述的设置方式更容易读懂其含义。
    2. 实例:一般系统中,只有一个主名称就够了,但是kerberos还增加了实例字段更加细分了用户身份。joe/admin@EXAMPLE.COMadmin字段表示joe是一个admin用户;hdfs/node2.example.com@EXAMPLE.COM node2.example.com 表示hdfs服务运行在node2.example.com服务中。可以省略实例字段,但加上实例字段,对于一个用户身份的划分将会更加细致。
    3. 领域:领域是KDC和客户端所处的一个网络。joe/admin@EXAMPLE.COM 的领域是EXAMPLE.COM ,不同的领域表示不同的网络,即不同的KDC。由于领域字段的限制,用户不能跨KDC进行认证。
    4. 备注:由于Kerberos将认证服务独立出来,因此,服务端也需要有自己的用户身份principal,这样避免有第三方伪造服务端。
  3. 密码:客户端和服务端会将自己的密码通过hash值生成keytab文件,上传到KDC中。客户端和服务端本地往往会保存自己专属的keytab,它们可以通过密码或者keytab向kerberos认证principal身份。

2.2 Kerberos服务认证过程

  1. Kerberos认证的基本思路是客户端输入自己的密码或者keytab,访问KDC,从KDC拿到访问服务端的凭证后,通过凭证访问服务端。
  2. 上述思路有一个问题:一个凭证只能访问一个服务端。如果客户端想要访问上万服务,那么他要输入上万次密码或者指定上万次的keytab重新认证,这种方式非常不方便。为了解决这个问题,Kerberos将KDC认证服务划分为两部分:Authentication Server(简称AS)和Ticket Granting Server(简称TGS)。
    1. AS是用户身份认证过程,它验证用户的princpal和密码的正确性,如果正确,AS返回Ticket-Granting Ticket(简称TGT)给客户端。此后,用户不需要再次输入密码认证,直接复用TGT进行后续认证,后续认证都不需要输入密码了。
    2. 本来通过AS认证后,客户端可以直接访问服务端,但是如果此时服务端被篡改,那么此时认证系统就不安全。因此为了验证服务端是否被篡改,KDC额外增加了TGS服务。客户端携带TGT和服务端信息发送给TGS,TGS返回给客户端对应的ticket,这个ticket是通过服务端的keytab加密的。客户端将ticket发送给服务端,如果服务端能够通过自有的keytab对ticket进行解密,说明服务端未被篡改,此时客户端和服务端正常通信。如果要访问其他服务端,再通过携带TGT和其他服务端的地址信息,发送给TGS,获取ticket后访问即可。
  3. 为了防止客户端的ticket被黑客窃取,在其他机器上访问服务端。Kerberos在发送认证请求时,携带了客户端IP,这样黑客在其他机器上执行认证时,由于IP不同会认证失败。
  4. 客户端获取的ticket还有一个隐患,如果ticket是永久有效的,那么一旦kerberos存在漏洞被第三方侵入,它就有权永久访问服务端。因此在ticket中设置时间戳和有效期,有效期一过,ticket就失效,设置时限会有效较低未知风险。

Kerberos认证过程如下所示:

Untitled.png

  • 备注:Kerberos和非对成加密的区别:Kerberos用于认证,非对称是一种加密算法,两者涉及的领域不同,kerberos认证过程中,会使用堆成加密算法对数据进行加密。

Kerberos认证具体过程如下:

Untitled 1.png

3. 基于Java实现客户端和服务端的Kerberos认证

3.1 Java相关认证框架

1.JAAS:全称Java Authentication and Authorization Service。JAAS框架最终要的就是

LoginModule接口,它负责执行认证相关逻辑。如下有多个实现类。Kerberos认证相关实现类是Krb5LoginModule:

Untitled 2.png

JAAS只负责向AS服务认证,并获取TGT,不负责向TGS认证。如下所示,Krb5LoginModule.login方法开始向AS认证,它attemptAuthentication方法,该方法先从本地TGT的缓存中获取TGT,如果没有缓存,则通过principal和keytab向AS发送请求:

cred  = Credentials.acquireTGTFromCache
                    (principal, ticketCacheName);

builder = new KrbAsReqBuilder(principal, ktab);
                    if (isInitiator) {
                        cred = builder.action().getCreds();
                    }

KrbAsReqBuilder.action发送KrbAsReq请求:

public KrbAsReqBuilder action() throws KrbException, Asn1Exception, IOException {
        this.checkState(KrbAsReqBuilder.State.INIT, "Cannot call action");
        this.state = KrbAsReqBuilder.State.REQ_OK;
        return this.send().resolve();
    }

2.GSSAPI:Generic Security Services Application Program Interface。它是提供安全访问服务端应用程序的接口。GSSAPI内部会先通过JAAS获取TGT,然后基于GSS-API提供的initSecContext方法,获取能够访问服务端的server ticket,这个server ticket在GSS-API就是一个token。initSecContext方法如下:

byte[] GSSContext.initSecContext(byte[] inToken,
                                 int offset,
                                 int len)

   throws GSSException

客户端将token传递给服务端,服务端通过acceptSecContext方法接收客户端的server ticket,验证后将验证结果发送给客户端。认证完毕,客户端可以向服务端发送数据了。

GSSAPI认证流程的官方解释如下:

💡 In the case of the Kerberos V5 mechanism, there is no more than one round trip of tokens during context establishment. The client first sends a token generated by its initSecContext() containing the Kerberos AP-REQ message [2]. In order to generate the AP-REQ message, the Kerberos provider obtains a service ticket for the target server using the client's TGT. The service ticket is encrypted with the server's long-term secret key and is encapsulated as part of the AP-REQ message. After the server receives this token, it is passed to the acceptSecContext() method which decrypts the service ticket and authenticates the client. If mutual authentication was not requested, both the client and server side contexts would be established, and the server side acceptSecContext() would generate no output.

3.SASL:Simple Authentication and Security Layer**。**SASL是一个安全认证框架,它支持多种认证方式,可以通过配置指定认证方式。SASL封装了GSSAPI、DIGEST-MD5等认证过程,降低编码门槛。如下,时SASL的认证方式:

public enum SaslAuthMethod {
    KERBEROS((byte) 81, "GSSAPI"),
    TOKEN((byte) 82, "DIGEST-MD5"),
    PLAIN((byte) 83, "PLAIN");
}

3.2 基于GSSAPI进行客户端与服务端kerberos认证

为了简便,这里只给出关键代码,用于验证kerberos认证思想。

GSSAPI进行客户端与服务端kerberos认证的基本思路如下:

  1. 客户端获取TGT,获取service ticket。
  2. 客户端将service ticket发送给服务端。
  3. 服务端接收service ticket验证没有没问题,就返回响应信息给客户端。
  4. 客户端接收到响应后,正常请求下正式向服务端发起业务请求。

客户端关键代码如下:

this.socket = new Socket(srvIP, srvPort);
        //创建于服务端的连接
        this.socket = new Socket(srvIP, srvPort);
        this.inStream = new DataInputStream(socket.getInputStream());
        this.outStream = new DataOutputStream(socket.getOutputStream());
        //Oid用来表示GSS-API这种机制。 这个值是协议中定义的(RFC 1964),这一个部分是固定的        */
        Oid krb5Oid = new Oid("1.2.840.113554.1.2.2");
        
        byte[] token = new byte[0];
            // token is ignored on the first call
            // 这里进行kerberos认证,即调用JaaS的代码进行认证。认证完成后,返回
            // 这里我们可以把它GSS-api将JaaS的代码集成了。kerberos信息的确认是在此函数中进行的
            token = context.initSecContext(token, 0, token.length);

            // Send a token to the server if one was generated by
            // initSecContext
            if (token != null) {
                //向服务端发送service ticket
                System.out.println("Will send token of size " + token.length + " from initSecContext.");
                outStream.writeInt(token.length);
                outStream.write(token);
                outStream.flush();
            }
         //发送完ticket后,正式发送业务请求
        client.sendMessage();

服务端关键代码如下:

        //接收客户端连接
        socket = ss.accept();
            DataInputStream inStream =  new DataInputStream(socket.getInputStream());
            DataOutputStream outStream =  new DataOutputStream(socket.getOutputStream());
        GSSManager manager = GSSManager.getInstance();
        context = manager.createContext((GSSCredential) null);
        byte[] token = null;
            token = new byte[inStream.readInt()];
            System.out.println(
                    "Will read input token of size " + token.length + " for processing by acceptSecContext");
            inStream.readFully(token);
            //获取service ticket,并进行校验
            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.");
                outStream.writeInt(token.length);
                outStream.write(token);
                outStream.flush();
            }
        }
        //后续处理业务数据并发送数据给客户端

3.3 基于SASL进行客户端与服务端kerberos认证

基于SASL认证可以参考Hadoop的实现过程,Hadoop在https://issues.apache.org/jira/browse/HADOOP-6419提出了在RPC中整合了SASL框架,TGT依然使用JAAS的login方法进行获取,service ticket则是通过GSSAPI执行。

SASL根据不同的认证框架选择对应的认证方式,如下,对于Kerberos认证框架,会使用GSSAPI进行认证:

public static enum AuthMethod {
    SIMPLE((byte) 80, ""),
    KERBEROS((byte) 81, "GSSAPI"),
    @Deprecated
    DIGEST((byte) 82, "DIGEST-MD5"),
    TOKEN((byte) 82, "DIGEST-MD5"),
    PLAIN((byte) 83, "PLAIN");
}

客户端发送的请求头中也会添加GSSAPI的信息:

Untitled 3.png

客户端通过SaslRpcClient.saslConnect方法向服务端发送token信息,其中SaslRpcClient.evaluateChallenge方法会调用GssKrb5Client.initSecContext实现认证,获取service token信息:

Untitled 4.png

SaslInputStream流获取ticket信息:

Untitled 5.png

SaslRpcServer则会对相关信息进行解析:

Untitled 6.png

通过上述变更,客户端端通过以下代码就可以基于SASL实现基于Kerberos认证的方式访问服务端:

static void testKerberosRpc(String principal, String keytab) throws Exception {
    final Configuration newConf = new Configuration(conf);
    newConf.set(SERVER_PRINCIPAL_KEY, principal);
    UserGroupInformation.loginUserFromKeytab(principal, keytab);
    UserGroupInformation current = UserGroupInformation.getCurrentUser();
    System.out.println("UGI: " + current);

    Server server = RPC.getServer(TestSaslProtocol.class, new TestSaslImpl(),
        ADDRESS, 0, 5, true, newConf, null);
    TestSaslProtocol proxy = null;

    server.start();

    InetSocketAddress addr = NetUtils.getConnectAddress(server);
    try {
      proxy = (TestSaslProtocol) RPC.getProxy(TestSaslProtocol.class,
          TestSaslProtocol.versionID, addr, newConf);
      proxy.ping();
    } finally {
      server.stop();
      if (proxy != null) {
        RPC.stopProxy(proxy);
      }
    }
  }
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  dhQTAsTc5eYm   2023年12月23日   51   0   0 HadoopHadoopapacheapache
GQ7psP7UJw7k
最新推荐 更多

2024-05-03