-
Notifications
You must be signed in to change notification settings - Fork 24.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Kerberos] Add Kerberos authentication support (#32263)
This commit adds support for Kerberos authentication with a platinum license. Kerberos authentication support relies on SPNEGO, which is triggered by challenging clients with a 401 response with the `WWW-Authenticate: Negotiate` header. A SPNEGO client will then provide a Kerberos ticket in the `Authorization` header. The tickets are validated using Java's built-in GSS support. The JVM uses a vm wide configuration for Kerberos, so there can be only one Kerberos realm. This is enforced by a bootstrap check that also enforces the existence of the keytab file. In many cases a fallback authentication mechanism is needed when SPNEGO authentication is not available. In order to support this, the DefaultAuthenticationFailureHandler now takes a list of failure response headers. For example, one realm can provide a `WWW-Authenticate: Negotiate` header as its default and another could provide `WWW-Authenticate: Basic` to indicate to the client that basic authentication can be used in place of SPNEGO. In order to test Kerberos, unit tests are run against an in-memory KDC that is backed by an in-memory ldap server. A QA project has also been added to test against an actual KDC, which is provided by the krb5kdc fixture. Closes #30243
- Loading branch information
Showing
34 changed files
with
3,525 additions
and
65 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
...main/java/org/elasticsearch/xpack/core/security/authc/kerberos/KerberosRealmSettings.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.core.security.authc.kerberos; | ||
|
||
import org.elasticsearch.common.settings.Setting; | ||
import org.elasticsearch.common.settings.Setting.Property; | ||
import org.elasticsearch.common.unit.TimeValue; | ||
import org.elasticsearch.common.util.set.Sets; | ||
|
||
import java.util.Set; | ||
|
||
/** | ||
* Kerberos Realm settings | ||
*/ | ||
public final class KerberosRealmSettings { | ||
public static final String TYPE = "kerberos"; | ||
|
||
/** | ||
* Kerberos key tab for Elasticsearch service<br> | ||
* Uses single key tab for multiple service accounts. | ||
*/ | ||
public static final Setting<String> HTTP_SERVICE_KEYTAB_PATH = | ||
Setting.simpleString("keytab.path", Property.NodeScope); | ||
public static final Setting<Boolean> SETTING_KRB_DEBUG_ENABLE = | ||
Setting.boolSetting("krb.debug", Boolean.FALSE, Property.NodeScope); | ||
public static final Setting<Boolean> SETTING_REMOVE_REALM_NAME = | ||
Setting.boolSetting("remove_realm_name", Boolean.FALSE, Property.NodeScope); | ||
|
||
// Cache | ||
private static final TimeValue DEFAULT_TTL = TimeValue.timeValueMinutes(20); | ||
private static final int DEFAULT_MAX_USERS = 100_000; // 100k users | ||
public static final Setting<TimeValue> CACHE_TTL_SETTING = Setting.timeSetting("cache.ttl", DEFAULT_TTL, Setting.Property.NodeScope); | ||
public static final Setting<Integer> CACHE_MAX_USERS_SETTING = | ||
Setting.intSetting("cache.max_users", DEFAULT_MAX_USERS, Property.NodeScope); | ||
|
||
private KerberosRealmSettings() { | ||
} | ||
|
||
/** | ||
* @return the valid set of {@link Setting}s for a {@value #TYPE} realm | ||
*/ | ||
public static Set<Setting<?>> getSettings() { | ||
return Sets.newHashSet(HTTP_SERVICE_KEYTAB_PATH, CACHE_TTL_SETTING, CACHE_MAX_USERS_SETTING, SETTING_KRB_DEBUG_ENABLE, | ||
SETTING_REMOVE_REALM_NAME); | ||
} | ||
} |
114 changes: 114 additions & 0 deletions
114
...org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandlerTests.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
package org.elasticsearch.xpack.core.security.authc; | ||
|
||
import org.elasticsearch.ElasticsearchSecurityException; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.util.concurrent.ThreadContext; | ||
import org.elasticsearch.rest.RestRequest; | ||
import org.elasticsearch.rest.RestStatus; | ||
import org.elasticsearch.test.ESTestCase; | ||
import org.elasticsearch.xpack.core.XPackField; | ||
import org.mockito.Mockito; | ||
|
||
import java.util.Arrays; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import static org.hamcrest.Matchers.contains; | ||
import static org.hamcrest.Matchers.equalTo; | ||
import static org.hamcrest.Matchers.is; | ||
import static org.hamcrest.Matchers.notNullValue; | ||
import static org.hamcrest.Matchers.sameInstance; | ||
|
||
public class DefaultAuthenticationFailureHandlerTests extends ESTestCase { | ||
|
||
public void testAuthenticationRequired() { | ||
final boolean testDefault = randomBoolean(); | ||
final String basicAuthScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""; | ||
final String bearerAuthScheme = "Bearer realm=\"" + XPackField.SECURITY + "\""; | ||
final DefaultAuthenticationFailureHandler failuerHandler; | ||
if (testDefault) { | ||
failuerHandler = new DefaultAuthenticationFailureHandler(); | ||
} else { | ||
final Map<String, List<String>> failureResponeHeaders = new HashMap<>(); | ||
failureResponeHeaders.put("WWW-Authenticate", Arrays.asList(basicAuthScheme, bearerAuthScheme)); | ||
failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders); | ||
} | ||
assertThat(failuerHandler, is(notNullValue())); | ||
final ElasticsearchSecurityException ese = | ||
failuerHandler.authenticationRequired("someaction", new ThreadContext(Settings.builder().build())); | ||
assertThat(ese, is(notNullValue())); | ||
assertThat(ese.getMessage(), equalTo("action [someaction] requires authentication")); | ||
assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); | ||
if (testDefault) { | ||
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme); | ||
} else { | ||
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme); | ||
} | ||
} | ||
|
||
public void testExceptionProcessingRequest() { | ||
final String basicAuthScheme = "Basic realm=\"" + XPackField.SECURITY + "\" charset=\"UTF-8\""; | ||
final String bearerAuthScheme = "Bearer realm=\"" + XPackField.SECURITY + "\""; | ||
final String negotiateAuthScheme = randomFrom("Negotiate", "Negotiate Ijoijksdk"); | ||
final Map<String, List<String>> failureResponeHeaders = new HashMap<>(); | ||
failureResponeHeaders.put("WWW-Authenticate", Arrays.asList(basicAuthScheme, bearerAuthScheme, negotiateAuthScheme)); | ||
final DefaultAuthenticationFailureHandler failuerHandler = new DefaultAuthenticationFailureHandler(failureResponeHeaders); | ||
|
||
assertThat(failuerHandler, is(notNullValue())); | ||
final boolean causeIsElasticsearchSecurityException = randomBoolean(); | ||
final boolean causeIsEseAndUnauthorized = causeIsElasticsearchSecurityException && randomBoolean(); | ||
final ElasticsearchSecurityException eseCause = (causeIsEseAndUnauthorized) | ||
? new ElasticsearchSecurityException("unauthorized", RestStatus.UNAUTHORIZED, null, (Object[]) null) | ||
: new ElasticsearchSecurityException("different error", RestStatus.BAD_REQUEST, null, (Object[]) null); | ||
final Exception cause = causeIsElasticsearchSecurityException ? eseCause : new Exception("other error"); | ||
final boolean withAuthenticateHeader = randomBoolean(); | ||
final String selectedScheme = randomFrom(bearerAuthScheme, basicAuthScheme, negotiateAuthScheme); | ||
if (withAuthenticateHeader) { | ||
eseCause.addHeader("WWW-Authenticate", Collections.singletonList(selectedScheme)); | ||
} | ||
|
||
if (causeIsElasticsearchSecurityException) { | ||
if (causeIsEseAndUnauthorized) { | ||
final ElasticsearchSecurityException ese = failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause, | ||
new ThreadContext(Settings.builder().build())); | ||
assertThat(ese, is(notNullValue())); | ||
assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); | ||
assertThat(ese, is(sameInstance(cause))); | ||
if (withAuthenticateHeader == false) { | ||
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme); | ||
} else { | ||
if (selectedScheme.contains("Negotiate ")) { | ||
assertWWWAuthenticateWithSchemes(ese, selectedScheme); | ||
} else { | ||
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme); | ||
} | ||
} | ||
assertThat(ese.getMessage(), equalTo("unauthorized")); | ||
} else { | ||
expectThrows(AssertionError.class, () -> failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause, | ||
new ThreadContext(Settings.builder().build()))); | ||
} | ||
} else { | ||
final ElasticsearchSecurityException ese = failuerHandler.exceptionProcessingRequest(Mockito.mock(RestRequest.class), cause, | ||
new ThreadContext(Settings.builder().build())); | ||
assertThat(ese, is(notNullValue())); | ||
assertThat(ese.getHeader("WWW-Authenticate"), is(notNullValue())); | ||
assertThat(ese.getMessage(), equalTo("error attempting to authenticate request")); | ||
assertWWWAuthenticateWithSchemes(ese, basicAuthScheme, bearerAuthScheme, negotiateAuthScheme); | ||
} | ||
|
||
} | ||
|
||
private void assertWWWAuthenticateWithSchemes(final ElasticsearchSecurityException ese, final String... schemes) { | ||
assertThat(ese.getHeader("WWW-Authenticate").size(), is(schemes.length)); | ||
assertThat(ese.getHeader("WWW-Authenticate"), contains(schemes)); | ||
} | ||
} |
Oops, something went wrong.