在 Samba 4.11 上使用多个 SPN 和 AES256 进行 Kerberos SPNEGO 认证。

问题描述 投票:0回答:1

我有一个用 Kotlin 写的 HTTP 服务,使用 Tomcat,它在多个域中监听,这些域需要通过 Kerberos 进行身份验证。在 Samba 4.9 上,我们有一个用户,他有多个 SPN,并启用了 AES256 加密。我们为该用户生成了一个包含所有SPN的keytab。

升级到 Samba 4.11 后,一个用户的多个 SPN 停止工作。错误信息 Client 'HTTP/[email protected]' not found in Kerberos database while getting initial credentials 被抛出。我们通过创建多个用户,每个SPN一个,并将UPN设置为单个SPN的值来解决这个问题。之后我们为每个用户生成keytabs,然后我们将其合并。

问题是,当我收到一个带有 aes256-cts-hmac-sha1-96, java.security.GeneralSecurityException: Checksum failed 抛出,并且只在一个域中工作,也就是我用作主域的那个域。arcfour-hmac-md5 在所有域上都能正常工作,但我需要支持AES加密。

我在我们旧的Samba 4.9上测试了这个方案,同样的情况也会发生。如果我们有多个用户,每个用户只有一个SPN,并且所有用户都有一个keytab。Checksum failed 也被抛出。

所以,要么我设法让一个用户使用多个SPN在Samba 4.11上工作,要么我必须摆脱掉这个 Checksum failed 当使用AES加密时。

java -version

openjdk version "11.0.6" 2020-01-14
OpenJDK Runtime Environment 18.9 (build 11.0.6+10)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.6+10, mixed mode)

JAVA_OPTS

-Dsun.security.krb5.disableReferrals=true
-Dsun.security.krb5.debug=true
-Dsun.security.spnego.debug=true

.java.login.config

example {
    com.sun.security.auth.module.Krb5LoginModule required
            keyTab="/root/HTTP.keytab"
            principal="HTTP/[email protected]"
            debug=true
            storeKey=true
            useKeyTab=true;
};

HTTP.keytab

Vno  Type                     Principal                          
  2  aes256-cts-hmac-sha1-96  HTTP/[email protected]
  2  aes128-cts-hmac-sha1-96  HTTP/[email protected]
  2  arcfour-hmac-md5         HTTP/[email protected]
  2  des-cbc-md5-deprecated   HTTP/[email protected]
  2  des-cbc-crc-deprecated   HTTP/[email protected]
  2  aes256-cts-hmac-sha1-96  HTTP/[email protected]
  2  aes128-cts-hmac-sha1-96  HTTP/[email protected]
  2  arcfour-hmac-md5         HTTP/[email protected]
  2  des-cbc-md5-deprecated   HTTP/[email protected]
  2  des-cbc-crc-deprecated   HTTP/[email protected]

HealthServlet.kt

import org.ietf.jgss.GSSCredential
import org.ietf.jgss.GSSManager
import org.ietf.jgss.Oid
import java.io.IOException
import java.security.PrivilegedActionException
import java.security.PrivilegedExceptionAction
import java.util.Base64
import javax.security.auth.Subject
import javax.security.auth.login.LoginContext
import javax.security.auth.login.LoginException
import javax.servlet.ServletException
import javax.servlet.annotation.WebServlet
import javax.servlet.http.HttpServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

@WebServlet("/healthz")
class HealthServlet : HttpServlet() {
    @Throws(ServletException::class, IOException::class)
    override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) {
        val authorization = req.getHeader("Authorization") ?: let {
            resp.addHeader("WWW-Authenticate", "Negotiate")
            resp.status = HttpServletResponse.SC_UNAUTHORIZED
            return
        }

        val negotiate = authorization.substringAfter(' ')
        val token = Base64.getDecoder().decode(negotiate)

        // Get own Kerberos credentials for accepting connection
        val manager = GSSManager.getInstance()
        val spnegoOid = Oid("1.3.6.1.5.5.2")

        var serverCreds: GSSCredential? = null
        this.loginAndAction(PrivilegedExceptionAction {
            serverCreds = manager.createCredential(null, GSSCredential.DEFAULT_LIFETIME, spnegoOid, GSSCredential.ACCEPT_ONLY)
        })

        val context = manager.createContext(serverCreds as GSSCredential)

        val respToken = context!!.acceptSecContext(token, 0, token.size)
        val respNegotiate = Base64.getEncoder().encodeToString(respToken)

        // Send a token to the peer if one was generated by
        // acceptSecContext
        if (respToken != null) {
            System.err.println("Will send token of size " + token.size + " from acceptSecContext.")

            resp.addHeader("WWW-Authenticate", "Negotiate $respNegotiate")
            resp.status = HttpServletResponse.SC_OK

            resp.writer.println(context.srcName)
        }

        System.err.println("Context Established! ")
        System.err.println("Client principal is " + context.srcName)
        System.err.println("Server principal is " + context.targName)

        /*
         * If mutual authentication did not take place, then
         * only the client was authenticated to the
         * server. Otherwise, both client and server were
         * authenticated to each other.
         */
        if (context.mutualAuthState)
            System.err.println("Mutual authentication took place!")
    }

    @Throws(LoginException::class, PrivilegedActionException::class)
    private fun <T> loginAndAction(action: PrivilegedExceptionAction<T>) {
        val context = LoginContext("example")
        context.login()

        // Perform action as authenticated user
        val subject = context.subject
        println(subject)

        Subject.doAs(subject, action)
        context.logout()
    }
}

日志

Debug is  true storeKey true useTicketCache false useKeyTab true doNotPrompt false ticketCache is null isInitiator true KeyTab is /root/HTTP.keytab refreshKrb5Config is false principal is HTTP/[email protected] tryFirstPass is false useFirstPass is false storePass is false clearPass is false
Looking for keys for: HTTP/[email protected]
Found unsupported keytype (1) for HTTP/[email protected]
Found unsupported keytype (3) for HTTP/[email protected]
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
Looking for keys for: HTTP/[email protected]
Found unsupported keytype (1) for HTTP/[email protected]
Found unsupported keytype (3) for HTTP/[email protected]
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 20 19 16 23.
>>> KrbAsReq creating message
getKDCFromDNS using UDP
>>> KrbKdcReq send: kdc=dc1.corp.example.com. UDP:88, timeout=30000, number of retries =3, #bytes=175
>>> KDCCommunication: kdc=dc1.corp.example.com. UDP:88, timeout=30000,Attempt =1, #bytes=175
>>> KrbKdcReq send: #bytes read=315
>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 16

>>>Pre-Authentication Data:
     PA-DATA type = 15

>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = CORP.EXAMPLE.COMa, s2kparams = 0000: 00 00 10 00                                        ....

>>> KdcAccessibility: remove dc1.corp.example.com.:88
>>> KDCRep: init() encoding tag is 126 req type is 11
>>>KRBError:
     sTime is Thu May 21 20:14:03 UTC 2020 1590092043000
     suSec is 748632
     error code is 25
     error Message is Additional pre-authentication required
     crealm is CORP.EXAMPLE.COM
     cname is HTTP/[email protected]
     sname is krbtgt/[email protected]
     eData provided.
     msgType is 30
>>>Pre-Authentication Data:
     PA-DATA type = 2
     PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
     PA-DATA type = 16

>>>Pre-Authentication Data:
     PA-DATA type = 15

>>>Pre-Authentication Data:
     PA-DATA type = 19
     PA-ETYPE-INFO2 etype = 18, salt = CORP.EXAMPLE.COMa, s2kparams = 0000: 00 00 10 00                                        ....

KRBError received: Need to use PA-ENC-TIMESTAMP/PA-PK-AS-REQ
KrbAsReqBuilder: PREAUTH FAILED/REQ, re-send AS-REQ
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 20 19 16 23.
Looking for keys for: HTTP/[email protected]
Found unsupported keytype (1) for HTTP/[email protected]
Found unsupported keytype (3) for HTTP/[email protected]
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
Looking for keys for: HTTP/[email protected]
Found unsupported keytype (1) for HTTP/[email protected]
Found unsupported keytype (3) for HTTP/[email protected]
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 20 19 16 23.
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsReq creating message
getKDCFromDNS using UDP
>>> KrbKdcReq send: kdc=dc1.corp.example.com. UDP:88, timeout=30000, number of retries =3, #bytes=264
>>> KDCCommunication: kdc=dc1.corp.example.com. UDP:88, timeout=30000,Attempt =1, #bytes=264
>>> KrbKdcReq send: #bytes read=199
>>> KrbKdcReq send: kdc=dc1.corp.example.com. TCP:88, timeout=30000, number of retries =3, #bytes=264
>>> KDCCommunication: kdc=dc1.corp.example.com. TCP:88, timeout=30000,Attempt =1, #bytes=264
>>>DEBUG: TCPClient reading 1511 bytes
>>> KrbKdcReq send: #bytes read=1511
>>> KdcAccessibility: remove dc1.corp.example.com.:88
Looking for keys for: HTTP/[email protected]
Found unsupported keytype (1) for HTTP/[email protected]
Found unsupported keytype (3) for HTTP/[email protected]
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsRep cons in KrbAsReq.getReply HTTP/a.example.com
principal is HTTP/[email protected]
Will use keytab
Commit Succeeded 

Subject:
    Principal: HTTP/[email protected]
    Private Credential: Ticket (hex) = 
... REDACTED ...

Client Principal = HTTP/[email protected]
Server Principal = krbtgt/[email protected]
Session Key = EncryptionKey: keyType=18 keyBytes (hex dump)=
... REDACTED ...


Forwardable Ticket false
Forwarded Ticket false
Proxiable Ticket false
Proxy Ticket false
Postdated Ticket false
Renewable Ticket false
Initial Ticket true
Auth Time = Thu May 21 20:14:03 UTC 2020
Start Time = Thu May 21 20:14:03 UTC 2020
End Time = Fri May 22 06:14:03 UTC 2020
Renew Till = null
Client Addresses  Null 
    Private Credential: /root/HTTP.keytab for HTTP/[email protected]

Found KeyTab /root/HTTP.keytab for HTTP/[email protected]
Found KeyTab /root/HTTP.keytab for HTTP/[email protected]
Found ticket for HTTP/[email protected] to go to krbtgt/[email protected] expiring on Fri May 22 06:14:03 UTC 2020
        [Krb5LoginModule]: Entering logout
        [Krb5LoginModule]: logged out Subject
Entered SpNegoContext.acceptSecContext with state=STATE_NEW
SpNegoContext.acceptSecContext: receiving token = ... REDACTED ...
SpNegoToken NegTokenInit: reading Mechanism Oid = 1.2.840.113554.1.2.2
SpNegoToken NegTokenInit: reading Mechanism Oid = 1.2.752.43.14.3
SpNegoToken NegTokenInit: reading Mech Token
SpNegoContext.acceptSecContext: received token of type = SPNEGO NegTokenInit
SpNegoContext: negotiated mechanism = 1.2.840.113554.1.2.2
Entered Krb5Context.acceptSecContext with state=STATE_NEW
Looking for keys for: HTTP/[email protected]
Found unsupported keytype (1) for HTTP/[email protected]
Found unsupported keytype (3) for HTTP/[email protected]
Added key: 23version: 2
Added key: 17version: 2
Added key: 18version: 2
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
Servlet.service() for servlet [HealthServlet] in context with path [] threw exception [Servlet execution threw an exception] with root cause
java.security.GeneralSecurityException: Checksum failed
    at java.security.jgss/sun.security.krb5.internal.crypto.dk.AesDkCrypto.decryptCTS(AesDkCrypto.java:451)
    at java.security.jgss/sun.security.krb5.internal.crypto.dk.AesDkCrypto.decrypt(AesDkCrypto.java:272)
    at java.security.jgss/sun.security.krb5.internal.crypto.Aes256.decrypt(Aes256.java:76)
    at java.security.jgss/sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType.decrypt(Aes256CtsHmacSha1EType.java:100)
    at java.security.jgss/sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType.decrypt(Aes256CtsHmacSha1EType.java:94)
    at java.security.jgss/sun.security.krb5.EncryptedData.decrypt(EncryptedData.java:180)
    at java.security.jgss/sun.security.krb5.KrbApReq.authenticate(KrbApReq.java:281)
    at java.security.jgss/sun.security.krb5.KrbApReq.<init>(KrbApReq.java:149)
    at java.security.jgss/sun.security.jgss.krb5.InitSecContextToken.<init>(InitSecContextToken.java:139)
    at java.security.jgss/sun.security.jgss.krb5.Krb5Context.acceptSecContext(Krb5Context.java:832)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:361)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:303)
    at java.security.jgss/sun.security.jgss.spnego.SpNegoContext.GSS_acceptSecContext(SpNegoContext.java:905)
    at java.security.jgss/sun.security.jgss.spnego.SpNegoContext.acceptSecContext(SpNegoContext.java:556)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:361)
    at java.security.jgss/sun.security.jgss.GSSContextImpl.acceptSecContext(GSSContextImpl.java:303)
    at HealthServlet.doGet(HealthServlet.kt:43)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:634)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:741)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:853)
    at org.apache.tomcat.util.net.Nio2Endpoint$SocketProcessor.doRun(Nio2Endpoint.java:1676)
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
    at org.apache.tomcat.util.net.AbstractEndpoint.processSocket(AbstractEndpoint.java:1087)
    at org.apache.tomcat.util.net.Nio2Endpoint$Nio2SocketWrapper$2.completed(Nio2Endpoint.java:589)
    at org.apache.tomcat.util.net.Nio2Endpoint$Nio2SocketWrapper$2.completed(Nio2Endpoint.java:567)
    at java.base/sun.nio.ch.Invoker.invokeUnchecked(Invoker.java:127)
    at java.base/sun.nio.ch.Invoker$2.run(Invoker.java:219)
    at java.base/sun.nio.ch.AsynchronousChannelGroupImpl$1.run(AsynchronousChannelGroupImpl.java:112)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
    at java.base/java.lang.Thread.run(Thread.java:834)
java kerberos samba spnego
1个回答
0
投票

原文

到目前为止,我找到的唯一的解决方案是给Samba打补丁,将其回归到旧的SPN行为。

diff --git a/source4/heimdal/kdc/kerberos5.c b/source4/heimdal/kdc/kerberos5.c
index 27d38ad84b7..fdf249bc08d 100644
--- a/source4/heimdal/kdc/kerberos5.c
+++ b/source4/heimdal/kdc/kerberos5.c
@@ -762,9 +762,9 @@ kdc_check_flags(krb5_context context,
        return KRB5KDC_ERR_POLICY;
    }

-   if(!client->flags.client){
+   if (!is_as_req && !client->flags.client){
        kdc_log(context, config, 0,
-           "Principal may not act as client -- %s", client_name);
+           "Principal may only act as client in AS-REQ -- %s", client_name);
        return KRB5KDC_ERR_POLICY;
    }

@@ -1056,7 +1056,7 @@ _kdc_as_rep(krb5_context context,
      */

     ret = _kdc_db_fetch(context, config, client_princ,
-           HDB_F_GET_CLIENT | flags, NULL,
+           HDB_F_GET_ANY | flags, NULL,
            &clientdb, &client);
     if(ret == HDB_ERR_NOT_FOUND_HERE) {
    kdc_log(context, config, 5, "client %s does not have secrets at this KDC, need to proxy", client_name);

编辑1

正如提交消息中所解释的那样,在AS-REQ中使用SPN的行为是不正确的。

https:/gitlab.comsamba-teamsamba-commita6182bd9512e6c78cfd2127790419418ab776be9。

所以,正确的做法应该是调查 Checksum failed Java的异常,而不是给Samba打补丁。

© www.soinside.com 2019 - 2024. All rights reserved.