我有一个用 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加密时。
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)
-Dsun.security.krb5.disableReferrals=true
-Dsun.security.krb5.debug=true
-Dsun.security.spnego.debug=true
example {
com.sun.security.auth.module.Krb5LoginModule required
keyTab="/root/HTTP.keytab"
principal="HTTP/[email protected]"
debug=true
storeKey=true
useKeyTab=true;
};
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]
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)
到目前为止,我找到的唯一的解决方案是给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);
正如提交消息中所解释的那样,在AS-REQ中使用SPN的行为是不正确的。
https:/gitlab.comsamba-teamsamba-commita6182bd9512e6c78cfd2127790419418ab776be9。
所以,正确的做法应该是调查 Checksum failed
Java的异常,而不是给Samba打补丁。