ОБНОВЛЕНИЕ: на основании комментария от @ Michael-O ниже, это кажется правильным способом решения этой проблемы, если поставщик LDAP JNDI или реализация SASL канонизируют имя хоста, выполнив пересылку, затем обратный поиск DNS перед выдачей в запросе билета службы KRN. Я попытаюсь обратиться к списку безопасности Open JDK и посмотреть, не появятся ли оттуда какие-либо ответы.
Я пытаюсь выполнить рекурсивный поиск LDAP по корневому DN на сервере Active Directory, используя сеанс, который аутентифицируется через GSSAPI с использованием Subject из Kerberos LoginContext.
Я могу успешно подключиться к серверу с URL ldap://dc1.example.com
. Для InitidalDirContext java.naming.referral установлено значение follow
.
Когда я выполняю поиск (&(objectClass=user)([email protected]))
по корневому DN dc=example,dc=com
, я получаю один SearchResult:
CN=Sample User,OU=ExampleUsers,DC=example,DC=com
И несколько ссылок на продолжение:
ldap://example.com/CN=Configuration,DC=example,DC=com
ldap://ForestDnsZones.example.com/DC=ForestDnsZones,DC=example,DC=com
ldap://DomainDnsZones.example.com/DC=DomainDnsZones,DC=example,DC=com
Я могу нормально перебирать SearchResult, но как только я встречаю продолжение, я получаю PartialResultsException. Я проверил DNS, и все указанные выше имена хостов разрешаются правильно. Исключение, которое я получаю, выглядит так:
javax.naming.PartialResultException
[Root exception is javax.naming.AuthenticationException: GSSAPI
[Root exception is javax.security.sasl.SaslException: GSS initiate failed
[Caused by GSSException: No valid credentials provided
(Mechanism level: Server not found in Kerberos database (7))]]].
Если посмотреть на трассировку Kerberos, эта ошибка имеет смысл. При попытке следить за продолжением библиотека LDAP пытается выполнить привязку к ldap://example.com
. Поскольку мы используем GSSAPI для аутентификации, это вызывает запрос билета службы для ldap/example.com
. Ответ, который я вижу в журнале:
>>>KRBError:
sTime is Thu Aug 21 14:27:20 EDT 2014 0000000000000
suSec is 414575
error code is 7
error Message is Server not found in Kerberos database
realm is EXAMPLE.COM
sname is ldap/example.com
msgType is 30
Я проверил Active Directory и убедился, что ни на одном из контроллеров домена нет атрибута servicePrincipalName со значением ldap/example.com
. Я попытался вручную добавить имя участника-службы для ldap/example.com
в учетную запись компьютера контроллера домена SAVANT-DC1. Это работает временно, но Active Directory, кажется, автоматически очищает запись SPN через пару минут.
Похоже, решение было бы сделать одно из
- Получите Active Directory, чтобы возвращать продолжения, содержащие имя контроллера домена вместо домена. Мы знаем, что можем получить билеты на обслуживание для SPN в форме
ldap/dc1.example.com
. - Каким-то образом продолжение карты на конце java для перенаправления
ldap://example.com
наldap://dc1.example.com
Я не могу понять, как это сделать (1).
Я пробовал сделать (2), используя Пример ручной обработки переходов по JNDI в качестве руководства. Я переключил свойство java.naming.referral на throw
и написал собственный обработчик перехода, который вручную переопределяет свойство java.naming.provider.url в контексте перехода. Однако LdapReferralException.getReferralContext()
, похоже, игнорирует свойство среды java.naming.provider.url. Глядя на код OpenJDK на LdapReferralContext.java, похоже, подтверждает это (строка 105).
Вот где я: я не могу перехватывать и управлять ссылками на стороне Java, потому что они рассматриваются как черный ящик JNDI API. Я не могу вручную создать имя участника-службы LDAP на стороне AD, потому что оно не будет оставаться постоянным в каталоге. Что еще мне не хватает?
Вот код, который я запускаю
import java.io.File;
import java.security.PrivilegedExceptionAction;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.ReferralException;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag;
import javax.security.auth.login.Configuration;
import javax.security.auth.login.LoginContext;
public class LdapContinuationDemoAction implements PrivilegedExceptionAction<Object> {
private final String ldapUrl;
private final String ldapDn;
private final String username;
public static void main(String[] argv) {
try {
String username = "[email protected]";
String password = "Password1";
String ldapUrl = "ldap://dc1.example.com";
String searchDn = "dc=example,dc=com";
String pwd = System.getProperty("user.dir");
String krb5Conf = new File(pwd, "krb5.conf").getAbsolutePath();
System.setProperty("java.security.krb5.conf", krb5Conf);
System.setProperty("sun.security.krb5.debug", "true");
// Login to the domain via Kerberos
LoginContext loginCtx = new LoginContext("doesn't matter", null,
getUsernamePasswordHandler(username, password),
getKrb5Configuration());
System.out.println("********************************");
System.out.println(" KRB5 Login");
System.out.println("********************************");
loginCtx.login();
// Execute the LDAP search as the user logged in above
LdapContinuationDemoAction action = new LdapContinuationDemoAction(ldapUrl,
searchDn, username);
Subject.doAs(loginCtx.getSubject(), action);
} catch( Exception e) {
System.out.println();
System.out.println("*** ERROR: " + e);
}
}
private LdapContinuationDemoAction(String ldapUrl, String ldapDn,
String username) {
this.ldapUrl = ldapUrl;
this.ldapDn = ldapDn;
this.username = username;
}
// Perform a recursive LDAP search for a user principal and print the results
@Override
public Object run() throws Exception {
System.out.println("********************************");
System.out.println(" LDAP Login");
System.out.println("********************************");
//Setup the directory context environment
Properties dirCtxProps = new Properties();
dirCtxProps.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
dirCtxProps.put(Context.PROVIDER_URL, this.ldapUrl);
dirCtxProps.put(Context.SECURITY_AUTHENTICATION, "GSSAPI");
dirCtxProps.put("java.naming.ldap.attributes.binary", "objectSID");
dirCtxProps.put(Context.REFERRAL, "follow");
DirContext dirCtx = new InitialDirContext(dirCtxProps);
// enable recursive searching
SearchControls ctrls = new SearchControls();
ctrls.setSearchScope(SearchControls.SUBTREE_SCOPE);
// do the search
NamingEnumeration<SearchResult> results = dirCtx.search(this.ldapDn,
"(&(objectClass=user)(userPrincipalName={0}))",
new Object[] { this.username }, ctrls);
System.out.println("********************************");
System.out.println(" LDAP User Info");
System.out.println("********************************");
int resultNum = 0;
while (results.hasMore()) {
resultNum++;
Attributes userAttr = results.next().getAttributes();
System.out.println("ldap result " + resultNum + ": User DN: "
+ userAttr.get("distinguishedName").get());
System.out.println();
}
return null;
}
// JAAS callback handler for username and password Kerberos authn
private static CallbackHandler getUsernamePasswordHandler(
final String username, final String password) {
final CallbackHandler handler = new CallbackHandler() {
@Override
public void handle(final Callback[] callback) {
for (int i = 0; i < callback.length; i++) {
if (callback[i] instanceof NameCallback) {
final NameCallback nameCallback = (NameCallback) callback[i];
nameCallback.setName(username);
} else if (callback[i] instanceof PasswordCallback) {
final PasswordCallback passCallback = (PasswordCallback) callback[i];
passCallback.setPassword(password.toCharArray());
} else {
System.err.println("Unsupported Callback: "
+ callback[i].getClass().getName());
}
}
}
};
return handler;
}
// dynamically build a Kerberos JAAS configuration so we don't need a login.conf
private static Configuration getKrb5Configuration() {
return new Configuration() {
@Override
public AppConfigurationEntry[] getAppConfigurationEntry(String name) {
Map<String, String> options = new HashMap<String, String>();
options.put("client", "true");
return new AppConfigurationEntry[] {
new AppConfigurationEntry(
"com.sun.security.auth.module.Krb5LoginModule",
LoginModuleControlFlag.REQUIRED, options)
};
}
};
}
}
Вот мой krb5.conf:
[libdefaults]
default_realm = EXAMPLE.COM
[realms]
EXAMPLE.COM = {
kdc = dc1.example.com
default_domain = example.com
}
[domain_realm]
.example.com = EXAMPLE.COM
example.com = EXAMPLE.COM
Вот вывод из приведенного выше кода
********************************
KRB5 Login
********************************
Config name: C:\src\scratch\krb5\krb5.conf
>>> KdcAccessibility: reset
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23 1 3.
>>> KrbAsReq creating message
>>> KrbKdcReq send: kdc=dc1.example.com UDP:88, timeout=30000, number of retries =3, #bytes=158
>>> KDCCommunication: kdc=dc1.example.com UDP:88, timeout=30000,Attempt =1, #bytes=158
>>> KrbKdcReq send: #bytes read=227
>>>Pre-Authentication Data:
PA-DATA type = 19
PA-ETYPE-INFO2 etype = 18, salt = EXAMPLE.COMexample_user, s2kparams = null
PA-ETYPE-INFO2 etype = 23, salt = null, s2kparams = null
PA-ETYPE-INFO2 etype = 3, salt = EXAMPLE.COMexample_user, s2kparams = null
>>>Pre-Authentication Data:
PA-DATA type = 2
PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
PA-DATA type = 16
>>>Pre-Authentication Data:
PA-DATA type = 15
>>> KdcAccessibility: remove dc1.example.com
>>> KDCRep: init() encoding tag is 126 req type is 11
>>>KRBError:
sTime is Thu Aug 21 16:35:42 EDT 2014 0000000000000
suSec is 659371
error code is 25
error Message is Additional pre-authentication required
realm is EXAMPLE.COM
sname is krbtgt/EXAMPLE.COM
eData provided.
msgType is 30
>>>Pre-Authentication Data:
PA-DATA type = 19
PA-ETYPE-INFO2 etype = 18, salt = EXAMPLE.COMexample_user, s2kparams = null
PA-ETYPE-INFO2 etype = 23, salt = null, s2kparams = null
PA-ETYPE-INFO2 etype = 3, salt = EXAMPLE.COMexample_user, s2kparams = null
>>>Pre-Authentication Data:
PA-DATA type = 2
PA-ENC-TIMESTAMP
>>>Pre-Authentication Data:
PA-DATA type = 16
>>>Pre-Authentication Data:
PA-DATA type = 15
KrbAsReqBuilder: PREAUTH FAILED/REQ, re-send AS-REQ
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23 1 3.
Using builtin default etypes for default_tkt_enctypes
default etypes for default_tkt_enctypes: 18 17 16 23 1 3.
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsReq creating message
>>> KrbKdcReq send: kdc=dc1.example.com UDP:88, timeout=30000, number of retries =3, #bytes=240
>>> KDCCommunication: kdc=dc1.example.com UDP:88, timeout=30000,Attempt =1, #bytes=240
>>> KrbKdcReq send: #bytes read=1425
>>> KdcAccessibility: remove dc1.example.com
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
>>> KrbAsRep cons in KrbAsReq.getReply example_user
********************************
LDAP Login
********************************
Found ticket for [email protected] to go to krbtgt/[email protected] expiring on Fri Aug 22 02:35:42 EDT 2014
Entered Krb5Context.initSecContext with state=STATE_NEW
Found ticket for [email protected] to go to krbtgt/[email protected] expiring on Fri Aug 22 02:35:42 EDT 2014
Service ticket not found in the subject
>>> Credentials acquireServiceCreds: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 16 23 1 3.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbKdcReq send: kdc=dc1.example.com UDP:88, timeout=30000, number of retries =3, #bytes=1392
>>> KDCCommunication: kdc=dc1.example.com UDP:88, timeout=30000,Attempt =1, #bytes=1392
>>> KrbKdcReq send: #bytes read=1398
>>> KdcAccessibility: remove dc1.example.com
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbApReq: APOptions are 00000000 00000000 00000000 00000000
>>> EType: sun.security.krb5.internal.crypto.Aes256CtsHmacSha1EType
Krb5Context setting mySeqNumber to: 774790609
Krb5Context setting peerSeqNumber to: 0
Created InitSecContextToken:
0000: 01 00 6E 00 00 00 00 00 00 00 A0 03 02 01 05 A1 ..n..)0..%......
0010: 03 02 01 0E A2 00 00 00 00 00 00 00 00 A3 82 04 ................
0020: 00 00 00 00 00 00 00 00 2D A0 03 02 01 05 A1 0E 5a..10..-.......
0030: 1B 0C 55 54 42 53 41 56 2E 4C 4F 43 41 4C A2 2A ..EXAMPLE.COM.*
0040: 30 28 A0 03 02 01 00 A1 21 30 1F 1B 04 6C 64 61 0(......!0...lda
0050: 70 1B 17 73 61 76 61 6E 74 2D 64 63 31 2E 75 74 p..dc1.ut
0060: 62 73 61 76 2E 6C 6F 63 61 6C A3 82 03 E8 30 82 bsav.local....0.
0070: 03 E4 A0 03 02 01 12 A1 03 02 01 08 A2 82 03 D6 ................
---8<--- Snipping a bunch of binary
Krb5Context.unwrap: token=[05 04 01 ff 00 0c 00 0c 00 00 00 00 2e 2e 5d d1 f5 d2 e8 21 c1 23 92 20 61 f4 77 a8 07 a0 00 00 ]
Krb5Context.unwrap: data=[07 a0 00 00 ]
Krb5Context.wrap: data=[01 01 00 00 ]
Krb5Context.wrap: token=[05 04 00 ff 00 0c 00 00 00 00 00 00 2e 2e 5d d1 00 00 00 00 00 00 00 00 fa b6 79 67 ce db 58 d2 ]
********************************
LDAP User Info
********************************
ldap result 1: User DN: CN=Sample User,OU=ExampleUsers,DC=example,DC=com
Found ticket for [email protected] to go to krbtgt/[email protected] expiring on Fri Aug 22 02:35:42 EDT 2014
Entered Krb5Context.initSecContext with state=STATE_NEW
Found ticket for [email protected] to go to krbtgt/[email protected] expiring on Fri Aug 22 02:35:42 EDT 2014
Found ticket for [email protected] to go to ldap/[email protected] expiring on Fri Aug 22 02:35:42 EDT 2014
Service ticket not found in the subject
>>> Credentials acquireServiceCreds: same realm
Using builtin default etypes for default_tgs_enctypes
default etypes for default_tgs_enctypes: 18 17 16 23 1 3.
>>> CksumType: sun.security.krb5.internal.crypto.RsaMd5CksumType
>>> EType: sun.security.krb5.internal.crypto.ArcFourHmacEType
>>> KrbKdcReq send: kdc=dc1.example.com UDP:88, timeout=30000, number of retries =3, #bytes=1381
>>> KDCCommunication: kdc=dc1.example.com UDP:88, timeout=30000,Attempt =1, #bytes=1381
>>> KrbKdcReq send: #bytes read=94
>>> KdcAccessibility: remove dc1.example.com
>>> KDCRep: init() encoding tag is 126 req type is 13
>>>KRBError:
sTime is Thu Aug 21 16:35:46 EDT 2014 0000000000000
suSec is 918178
error code is 7
error Message is Server not found in Kerberos database
realm is EXAMPLE.COM
sname is ldap/example.com
msgType is 30
KrbException: Server not found in Kerberos database (7)
at sun.security.krb5.KrbTgsRep.<init>(KrbTgsRep.java:70)
at sun.security.krb5.KrbTgsReq.getReply(KrbTgsReq.java:192)
at sun.security.krb5.KrbTgsReq.sendAndGetCreds(KrbTgsReq.java:203)
at sun.security.krb5.internal.CredentialsUtil.serviceCreds(CredentialsUtil.java:311)
at sun.security.krb5.internal.CredentialsUtil.acquireServiceCreds(CredentialsUtil.java:115)
at sun.security.krb5.Credentials.acquireServiceCreds(Credentials.java:442)
at sun.security.jgss.krb5.Krb5Context.initSecContext(Krb5Context.java:641)
at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:248)
at sun.security.jgss.GSSContextImpl.initSecContext(GSSContextImpl.java:179)
at com.sun.security.sasl.gsskerb.GssKrb5Client.evaluateChallenge(GssKrb5Client.java:193)
at com.sun.jndi.ldap.sasl.LdapSasl.saslBind(LdapSasl.java:123)
at com.sun.jndi.ldap.LdapClient.authenticate(LdapClient.java:232)
at com.sun.jndi.ldap.LdapCtx.connect(LdapCtx.java:2740)
at com.sun.jndi.ldap.LdapCtx.<init>(LdapCtx.java:316)
at com.sun.jndi.ldap.LdapCtxFactory.getUsingURL(LdapCtxFactory.java:193)
at com.sun.jndi.ldap.LdapCtxFactory.getLdapCtxInstance(LdapCtxFactory.java:152)
at com.sun.jndi.url.ldap.ldapURLContextFactory.getObjectInstance(ldapURLContextFactory.java:52)
at javax.naming.spi.NamingManager.getURLObject(NamingManager.java:601)
at javax.naming.spi.NamingManager.processURL(NamingManager.java:381)
at javax.naming.spi.NamingManager.processURLAddrs(NamingManager.java:361)
at javax.naming.spi.NamingManager.getObjectInstance(NamingManager.java:333)
at com.sun.jndi.ldap.LdapReferralContext.<init>(LdapReferralContext.java:111)
at com.sun.jndi.ldap.LdapReferralException.getReferralContext(LdapReferralException.java:150)
at com.sun.jndi.ldap.LdapNamingEnumeration.hasMoreReferrals(LdapNamingEnumeration.java:357)
at com.sun.jndi.ldap.LdapNamingEnumeration.hasMoreImpl(LdapNamingEnumeration.java:226)
at com.sun.jndi.ldap.LdapNamingEnumeration.hasMore(LdapNamingEnumeration.java:189)
at LdapContinuationDemoAction.run(LdapContinuationDemoAction.java:123)
at java.security.AccessController.doPrivileged(Native Method)
at javax.security.auth.Subject.doAs(Subject.java:415)
at LdapContinuationDemoAction.main(LdapContinuationDemoAction.java:52)
Caused by: KrbException: Identifier doesn't match expected value (906)
at sun.security.krb5.internal.KDCRep.init(KDCRep.java:143)
at sun.security.krb5.internal.TGSRep.init(TGSRep.java:66)
at sun.security.krb5.internal.TGSRep.<init>(TGSRep.java:61)
at sun.security.krb5.KrbTgsRep.<init>(KrbTgsRep.java:55)
... 29 more
*** ERROR: java.security.PrivilegedActionException: javax.naming.PartialResultException [Root exception is javax.naming.AuthenticationException: GSSAPI [Root exception is javax.security.sasl.SaslException: GSS initiate failed [Caused by GSSException: No valid credentials provided (Mechanism level: Server not found in Kerberos database (7))]]]
example.com
:dc1.example.com
иdc2.example.com
. Я мог бы поставитьldap/example.com
подdc1
. Однако, когда мой клиент разрешаетldap://example.com
, из-за циклического перебора DNS запрос на соединение может быть обслуженdc2
. Посколькуdc2
не владеет SPN, он не сможет проверить билет Kerberos. - person Dominic A.   schedule 03.09.2014