-
Notifications
You must be signed in to change notification settings - Fork 24.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support framework for Kerberos Realm #31023
Add support framework for Kerberos Realm #31023
Conversation
This commit adds framework to support Kerberos authN in elasticsearch. For Kerberos Realm, KerberosRealmSettings: Captures settings required for Kerberos KerberosAuthenticationToken: Handles extraction of token from request KerberosTicketValidator: Used for kerberos ticket validation and gss context establishment. Changes in plugin-security policy to add required permissions For testing, KerberosTestCase is the base class to start/stop kdc server and build test settings. MiniKdc(Hadoop) is a wrapper around SimpleKdcServer(ApacheDS), simplifies in memory testing with KDC.
Pinging @elastic/es-security |
The change removes MiniKdc dependency and disabling of license check for the same.
@elasticmachine test this, please. |
private String base64EncodedTicket; | ||
|
||
public KerberosAuthenticationToken(final String base64EncodedToken) { | ||
this.principalName = UNAUTHENTICATED_PRINCIPAL_NAME; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have a field that is always set to a constant value (that in itself is extremely generic)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we do not need it. Thanks.
try { | ||
decodedKerberosTicket = Base64.getDecoder().decode(base64Token); | ||
} catch (IllegalArgumentException iae) { | ||
rootCause = iae; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This flow is pretty weird - can we just handle the exception here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed this, Thanks.
throw unauthorized("invalid negotiate authentication header value, could not decode base64 token {}", rootCause, | ||
base64EncodedToken); | ||
} | ||
return new KerberosAuthenticationToken(base64EncodedToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we base64 decode the token, and then throw the result away? Shouldn't this token use the byte[]
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, did not realize I was again decoding in the ticket validator, good catch. Thank you.
|
||
private static ElasticsearchSecurityException unauthorized(final String message, final Throwable cause, final Object... args) { | ||
ElasticsearchSecurityException ese = new ElasticsearchSecurityException(message, RestStatus.UNAUTHORIZED, cause, args); | ||
ese.addHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this right? (It might be, I just want to know that we've thought about it).
If we receive an invalid Kerberos token, we request to negotiate again - are you sure that's what we want?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, this is according to RFC2616 https://tools.ietf.org/html/rfc2616#section-10.4.2, though the expectation mentioned is to respond possibly with a different scheme if it was refused for presented credentials. During extraction of the token, we haven't actually tried to validate/authenticate the credentials yet but the header itself was malformed. Looked at the Tomcat Spnego Authenticator, it responds with the same scheme, so I went with the decision.
We could respond with a different scheme but not sure if we should be doing that. For example, Kibana, in this case, would respond with login page along with www-authenticate negotiate header is what I understood from the discussion here
loginContext = serviceLogin(servicePrincipalName, keyTabPath.toString(), config.settings()); | ||
// create credentials | ||
GSSCredential serviceCreds = createCredentials(servicePrincipalName, nameType, gssManager, loginContext); | ||
// create gss context |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't feel like these comments add anything. They're just repeating what is already clear from the method being called.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, removed those. Thanks.
base64OutToken = Base64.getEncoder().encodeToString(outToken); | ||
} | ||
LOGGER.debug("validateTicket isGSSContextEstablished = {}, username = {}, outToken = {}", gssContext.isEstablished(), | ||
gssContext.getSrcName().toString(), base64OutToken); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably be a trace
log.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change done, Thanks.
}); | ||
} catch (PrivilegedActionException e) { | ||
RuntimeException rte = ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(e)); | ||
LOGGER.log(Level.DEBUG, "Could not dispose GSS Context", rte); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We use .debug
, not .log
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change done, Thanks.
}); | ||
} catch (PrivilegedActionException e) { | ||
RuntimeException rte = ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(e)); | ||
LOGGER.log(Level.DEBUG, "Could not close LoginContext", rte); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.debug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Change done, Thanks.
public void describeTo(Description description) { | ||
} | ||
}); | ||
KerberosAuthenticationToken.extractToken(threadContext); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Our standard pattern is to use LucenseTestCase.expectThrows
.
Is there a reason you're using a Rule
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed this. Thank you.
public void testEqualsHashCode() { | ||
final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken("base64EncodedToken"); | ||
EqualsHashCodeTestUtils.checkEqualsAndHashCode(kerberosAuthenticationToken, (original) -> { | ||
return new KerberosAuthenticationToken((String) original.credentials()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you include a Mutator in this test as well ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added additional checks with Mutator, Thank you.
Added few tests, modified KerberosAuthenticationToken to keep decoded bytes. Corresponding changes in KerberosTicketValidator and tests.
This was due to less minimum ticket lifetime set on kdc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I took a first pass and left comments. Please let me know when this is ready for review again.
public static final String TYPE = "kerberos"; | ||
|
||
/** | ||
* Kerberos Key tab for Elasticsearch HTTP Service and Kibana HTTP Service<br> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wouldn't mention kibana here since nothing is specific to kibana
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure, removed it. Thanks.
* Uses single key tab for multiple service accounts. | ||
*/ | ||
public static final Setting<String> HTTP_SERVICE_KEYTAB_PATH = | ||
Setting.simpleString("http.service.keytab.path", Setting.Property.NodeScope); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we just use keytab.path
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, done. Thanks.
*/ | ||
public static final Setting<String> HTTP_SERVICE_KEYTAB_PATH = | ||
Setting.simpleString("http.service.keytab.path", Setting.Property.NodeScope); | ||
public static final Setting<Boolean> SETTING_KRB_DEBUG_ENABLE = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this setting should not be dynamic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My understanding of Dynamic is it would not require a restart of the node as these settings can be changed from API. As this is debugging log to enable verbose logging on the node for jvm kerb module, we could turn on and off after capturing the logs was what I thought and made it dynamic. Could you please let me know if this is not advisable for some reason? Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There needs to be a settings update consumer to handle the update from the update settings api. There is not one in this PR and we can always make this dynamic later on if we find the need. No other realm has dynamic settings either iirc
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed Dynamic, will need to explore how Dynamic works. Thanks for the pointer.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For dynamic, you need the ClusterSettings instance and have to register a settings update consumer for the setting. Take a look at IPFilter
for an example of where we do this in the security code.
// Cache | ||
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); | ||
public static final Setting<TimeValue> CACHE_TTL_SETTING = Setting.timeSetting("cache.ttl", DEFAULT_TTL, Setting.Property.NodeScope); | ||
private static final int DEFAULT_MAX_USERS = 100_000; // 100k users |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you put the private static final values together and the public ones together rather than intermixing
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done thanks.
Setting.intSetting("cache.max_users", DEFAULT_MAX_USERS, Setting.Property.NodeScope); | ||
|
||
private KerberosRealmSettings() { | ||
/* Empty private constructor */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this comment isn't necessary and doesn't add much. Either remove it or add value by saying this class should never be instantiated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed it. Thanks.
* {@link InMemoryDirectoryServer}.<br> | ||
* Starts in memory Ldap server and then uses it as backend for Kdc Server. | ||
*/ | ||
@SuppressForbidden(reason = "Uses Apache Kdc which requires usage of java.io.File") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we really need this? Can you point me to where this trips the forbidden check? We should try to limit the suppress forbidden annotations to the smallest amount of code possible and this is on the whole class
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have moved to the methods where it is doing Path.toFile()
instead of whole class. Thanks.
try { | ||
oid = new Oid("1.3.6.1.5.5.2"); | ||
} catch (GSSException gsse) { | ||
ExceptionsHelper.convertToRuntime(gsse); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing throw
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed this, using the constant from KerberosTicketValidator. Thanks.
try { | ||
oldUseSubjectCredsOnlyFlag = getAndSetSystemProperty("javax.security.auth.useSubjectCredsOnly", "true"); | ||
|
||
LOGGER.info("SpnegoClient with princName : {}", userPrincipalName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/princName/principal name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, Thanks.
return Base64.getEncoder().encodeToString(outToken); | ||
} | ||
|
||
public void close() throws LoginException, GSSException, PrivilegedActionException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we make it implement closeable? Also mention that it should be closed in javadocs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, did that, Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
javadocs are missing about needing to be closed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, Thanks
} | ||
|
||
/** | ||
* Instead of jaas.conf, this requires refresh of {@link Configuration}. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add complete javadocs
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, thanks.
This commit addresses review comments. Also during testing found Arabic "ar" locale having problem with SimpleKdcServer so added a check for it. Removed usage of Junit rules, using expectThrows instead. Add missing java docs.
@tvernum @jaymode I have addressed your review comments. I think this is ready for another review. Important comments that I would want you guys to look at :-
For the comment on adding bootstrap check for Kerberos about keytab exists during startup, I will address it in the next PR. |
Other languages that do not work with Apache SimpleKdcServer "ja", "th", "hi". Changing the default during testing.
KdcConfigKey does not specify locale during getProperyKey causing problem loading ldap backend properties. This is workaround as it writes the key in the same locale as expected for that test by using same property key
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went through it again and left more feedback. Please let me know when its ready to be reviewed again.
@@ -18,43 +17,53 @@ | |||
import java.util.Objects; | |||
|
|||
/** | |||
* Holds on to base 64 encoded ticket, also helps extracting token from | |||
* {@link ThreadContext} | |||
* This class represents AuthenticationToken for Kerberos authentication using |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/represents/represents an
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, done.
* Holds on to base 64 encoded ticket, also helps extracting token from | ||
* {@link ThreadContext} | ||
* This class represents AuthenticationToken for Kerberos authentication using | ||
* SPNEGO mechanism. The token stores base 64 decoded token bytes, extracted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/SPNEGO mechanism/SPNEGO
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, done.
return null; | ||
} | ||
|
||
if (authorizationHeader.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, NEGOTIATE_AUTH_HEADER.trim(), 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why do we call trim on a constant value? There should be a whitespace after Negotiate for it to be valid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, you are right, we need not do it. actually not sure why I did that and the check should as "(n|N)egotiate ". I have also added the change in UsernamePasswordToken to do the check as "(b|B)asic ". Corrected it. Thank you.
return null; | ||
} | ||
|
||
final String base64EncodedToken = authorizationHeader.substring(NEGOTIATE_AUTH_HEADER.trim().length()).trim(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same thing here with trim. We shouldn't be calling trim all the time on a constant value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, done.
|
||
@Override | ||
public String principal() { | ||
return "<Unauthenticated Principal Name>"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove Name
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed it. Thanks.
/** | ||
* This class is used as a Spnego client and handles spnego interactions using | ||
* GSS context negotiation.<br> | ||
* Not thread safe |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why isn't this class thread safe?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It internally uses GSSContext which maintains sequencing for avoiding replay attacks, in this class to verify mutual authentication I am keeping the context available, so it is advisable to not use this instance across threads as GSSContext also mentions this. I have added the java doc for it. I wanted to elaborate on it but forgot to add TODO. Thanks.
return Base64.getEncoder().encodeToString(outToken); | ||
} | ||
|
||
public void close() throws LoginException, GSSException, PrivilegedActionException { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
javadocs are missing about needing to be closed
* @throws LoginException | ||
*/ | ||
private static LoginContext loginUsingPassword(final String principal, final SecureString password) throws LoginException { | ||
final Set<Principal> principals = new HashSet<>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use Collections.singleton
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for these ones they were already pointed in earlier review. Thanks.
final Set<Principal> principals = new HashSet<>(); | ||
principals.add(new KerberosPrincipal(principal)); | ||
|
||
final Subject subject = new Subject(false, principals, new HashSet<Object>(), new HashSet<Object>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
use Collections.emptySet
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for these ones they were already pointed in earlier review. Thanks.
|
||
@Override | ||
@SuppressForbidden( | ||
reason = "For testing application provides credentials, needs sys prop javax.security.auth.useSubjectCredsOnly") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This statement is pretty vague. Is this method really only used for a single system property?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes just wanted to do it for single sys property, updated the method name and modified it to be used to set reset same property. Thanks.
- Removed unwanted trim() and corrected the check for Negotiate - corrected basic authorization check to be case insensitive - Add missing documentation - Upgrade Apache SimpleKdcServer and its dependencies
@@ -32,8 +30,10 @@ | |||
|
|||
public static final String WWW_AUTHENTICATE = "WWW-Authenticate"; | |||
public static final String AUTH_HEADER = "Authorization"; | |||
public static final String NEGOTIATE_AUTH_HEADER = "Negotiate "; | |||
public static final String NEGOTIATE_SCHEME_NAME = "Negotiate"; | |||
public static final String NEGOTIATE_AUTH_HEADER = NEGOTIATE_SCHEME_NAME + " "; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This isn't the header. This is the prefix for the value of the authorization header. Either rename the variable or remove it
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed it. Thanks.
@@ -173,7 +173,7 @@ private static GSSCredential createCredentials(final GSSManager gssManager, | |||
|
|||
/** | |||
* Privileged wrapper for closing GSSContext, does not throw exceptions but logs | |||
* them as warning. | |||
* them as debug message. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/debug/a debug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed it, Thanks.
@@ -192,7 +192,7 @@ private static void privilegedDisposeNoThrow(final GSSContext gssContext) { | |||
|
|||
/** | |||
* Privileged wrapper for closing LoginContext, does not throw exceptions but | |||
* logs them as warning. | |||
* logs them as debug message. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/debug/a debug
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed it, Thanks.
String retVal = null; | ||
try { | ||
retVal = AccessController.doPrivileged(new PrivilegedExceptionAction<String>() { | ||
|
||
@Override | ||
@SuppressForbidden( | ||
reason = "For testing application provides credentials, needs sys prop javax.security.auth.useSubjectCredsOnly") | ||
reason = "For tests where we provide credentials, need to set reset javax.security.auth.useSubjectCredsOnly") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/set reset/set and reset
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed it, Thanks.
if (Strings.isNullOrEmpty(headerValue)) { | ||
return null; | ||
} | ||
if (headerValue.regionMatches(IGNORE_CASE_AUTH_HEADER_MATCH, 0, BASIC_AUTH_PREFIX, 0, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this change should be made in a separate PR against master. It is unrelated to kerberos and technically fixes a bug but is hidden here. Lets open up a new PR for this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, just was plain lazy. I will create a bug and a PR to address this. Thanks.
/* | ||
* arabic and other languages have problem due to handling of GeneralizedTime in | ||
* SimpleKdcServer For more look at : | ||
* org.apache.kerby.asn1.type.Asn1GeneralizedTime#toBytes() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is non-standard support for UTF-8 as implemented by Microsoft. This is controlled using the sun.security.krb5.msinterop.kstring
system property. This was added in JDK7 https://bugs.openjdk.java.net/browse/JDK-2182089 and will become the default in JDK 11 https://bugs.openjdk.java.net/browse/JDK-8200152. From that issue:
In fact, RFC 3961 requires it to be UTF-8 string, and MIT krb5 also uses UTF-8. It's time to change the default to UTF-8. The system property can be kept.
final KerberosAuthenticationToken kerbAuthnToken = KerberosAuthenticationToken.extractToken(authzHeader); | ||
assertNotNull(kerbAuthnToken); | ||
assertEquals(UNAUTHENTICATED_PRINCIPAL_NAME, kerbAuthnToken.principal()); | ||
assertTrue(kerbAuthnToken.credentials() instanceof byte[]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would be better as assertThat(kerbAuthnToken.credentials(), instanceOf(byte[].class))
. This is because the error message will be much better than is false expected true
- Removed basic authentication bug fix, will create a bug and raise PR against that. - Corrected variable name - Corrected statements in comments and javadoc - Readability in tests
case insensitive handling of basic auth header will be addressed in separate PR.
Hi, @jaymode I have addressed your review comments. I think this is ready for another review. Thank you for your time. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, but I left a couple of suggestions
settings.add(CACHE_TTL_SETTING); | ||
settings.add(CACHE_MAX_USERS_SETTING); | ||
settings.add(SETTING_KRB_DEBUG_ENABLE); | ||
return settings; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is fine - and you don't need to change it, but you can use Sets.newHashSet
here, which can be a little more readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, have addressed this.
// authorization scheme check is case-insensitive | ||
private static final boolean IGNORE_CASE_AUTH_HEADER_MATCH = true; | ||
|
||
private final byte[] base64DecodedToken; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems strange to name this variable based on the format it used to be in.
I think tokenBytes
or even decodedToken
would make more sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed it. Thanks.
|
||
@Override | ||
public String principal() { | ||
return "<Unauthenticated Principal>"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The way this was done in SAML with "<unauthenticated-saml-user>"
has proven to be confusing for users. The log messages report a token for <unauthenticated-saml-user>
and they get worried that authentication failed, or something like that.
I think we're probably better of with something like "<Kerberos Token>"
instead.
Perhaps we should have a separate PR to allow this to be null, for tokens that don't know their principal yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it can be confusing, I think null makes sense and then create authenticated token with correct principal once authentication is successful so we have the principal name, where required, say logging etc. I will take it up as separate PR. Thanks.
if (pve.getCause() instanceof GSSException) { | ||
throw (GSSException) pve.getCause(); | ||
} | ||
throw ExceptionsHelper.convertToRuntime((Exception) ExceptionsHelper.unwrapCause(pve)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unwrapCause
doesn't do what you think it does.
You just want pve.getException()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw the implementation for unwrapCause totally different than what I assumed. As it was taking Throwable assumed must be unwrapping root cause and I can convert to runtime. Thanks. Addressed this.
return null; | ||
}); | ||
} catch (PrivilegedActionException e) { | ||
LOGGER.debug("Could not dispose GSS Context", (Exception) ExceptionsHelper.unwrapCause(e)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use e.getException()
instead of unwrapCause
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, addressed this.
return null; | ||
}); | ||
} catch (PrivilegedActionException e) { | ||
LOGGER.debug("Could not close LoginContext", (Exception) ExceptionsHelper.unwrapCause(e)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use e.getException()
instead of unwrapCause
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, addressed this.
* closed using {@link LoginContext#logout()} after usage. | ||
* @throws PrivilegedActionException when privileged action threw exception | ||
*/ | ||
private static LoginContext serviceLogin(final String keytabFilePath, final boolean krbDebug) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: You're inconsistent in whether it's a keytab
or a keyTab
. I have no opinion, but pick one :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks, I see that. Some jaas properties refer it as keyTab and some places keytab. I will make it consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left a suggestion. LGTM
} | ||
} | ||
|
||
private String base64Encode(final byte[] outToken) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you rename this method? This has specific behavior for an outgoing token; ie check for null/empty
This change adds the framework to support Kerberos authN in elasticsearch.
ES is the service protected by Kerberos, each ES service node will have its
own keytab. Keytab is the file with Service principal name and encrypted key.
This can be then used to validate the authenticator coming in the request.
This change only adds support for SPNEGO mechanism and uses JGSS.
JVM options
-Djava.security.krb5.conf
can be used to specify krb5.conf withadditional settings if required.
For Kerberos Realm,
Usually keytab (stored in the config), cache settings and krb debug flag
Extracts the token from request header:
"Authorization: Negotiate "
If any error condition occurs, throws Exception with Rest status 401
Also adds response header "WWW-Authenticate: Negotiate"
gss context establishment.
On service side, we need to login first, uses Jaas to complete service login.
To avoid more file configurations, we generate the JAAS configuration with
required modules in memory. The token extracted from authnToken is
passed on to GSSContext which uses service credentials (keytab) to verify
the passed token and generates output token. If GSS context is established
it returns tuple of client-username and out token (can be empty). If out token
is present but context is yet not established then it will return tuple with no
username and out token. The out token needs to be returned as response
header 401 and "WWW-Authenticate: Negotiate " for ongoing
negotiation. This will continue till either it fails or successful authentication on
context establishment.
Few settings like Jaas config and kerberos keytab access requires permissions.
For testing,
and build test settings. SimpleKdcLdapServer(sourced from Hadoop) is a wrapper around
SimpleKdcServer(ApacheDS), which simplifies in memory testing with KDC and uses
in memory ldap server as its backend.