diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java index e0f8d4adb076f..c2107f206e5f0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/DefaultAuthenticationFailureHandler.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.core.security.authc; import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestStatus; @@ -20,12 +21,15 @@ import java.util.Map; import java.util.stream.Collectors; +import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR; import static org.elasticsearch.xpack.core.security.support.Exceptions.authenticationError; +import static org.elasticsearch.xpack.core.security.support.Exceptions.internalServerError; /** * The default implementation of a {@link AuthenticationFailureHandler}. This * handler will return an exception with a RestStatus of 401 and default failure - * response headers like 'WWW-Authenticate' + * response headers like 'WWW-Authenticate' or an ElasticSecurityException with a + * RestStatus of 500 (INTERNAL_SERVER_ERROR) */ public class DefaultAuthenticationFailureHandler implements AuthenticationFailureHandler { private volatile Map> defaultFailureResponseHeaders; @@ -126,7 +130,7 @@ public ElasticsearchSecurityException authenticationRequired(String action, Thre /** * Creates an instance of {@link ElasticsearchSecurityException} with - * {@link RestStatus#UNAUTHORIZED} status. + * {@link RestStatus#UNAUTHORIZED} or {@link RestStatus#INTERNAL_SERVER_ERROR}status. *

* Also adds default failure response headers as configured for this * {@link DefaultAuthenticationFailureHandler} @@ -137,7 +141,10 @@ public ElasticsearchSecurityException authenticationRequired(String action, Thre * @param message error message * @param t cause, if it is an instance of * {@link ElasticsearchSecurityException} asserts status is - * RestStatus.UNAUTHORIZED and adds headers to it, else it will + * RestStatus.UNAUTHORIZED and adds headers to it, + * if it is an instance of {@link ElasticsearchStatusException} + * with status code 500 (INTERNAL_SERVER_ERROR) set status to + * RestStatus.INTERNAL_SERVER_ERROR, else it will * create a new instance of {@link ElasticsearchSecurityException} * @param args error message args * @return instance of {@link ElasticsearchSecurityException} @@ -160,6 +167,9 @@ private ElasticsearchSecurityException createAuthenticationError(final String me } else { containsNegotiateWithToken = false; } + } else if (t instanceof ElasticsearchStatusException && ((ElasticsearchStatusException) t).status() == INTERNAL_SERVER_ERROR) { + ese = internalServerError(message, t, args); + containsNegotiateWithToken = false; } else { ese = authenticationError(message, t, args); containsNegotiateWithToken = false; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java index 9b2652883455f..a23c9a1bf3d2f 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/Exceptions.java @@ -34,4 +34,8 @@ public static ElasticsearchSecurityException authorizationError(String msg, Obje public static ElasticsearchSecurityException authorizationError(String msg, Exception cause, Object... args) { return new ElasticsearchSecurityException(msg, RestStatus.FORBIDDEN, cause, args); } + + public static ElasticsearchSecurityException internalServerError(String msg, Throwable cause, Object... args) { + return new ElasticsearchSecurityException(msg, RestStatus.INTERNAL_SERVER_ERROR, cause, args); + } } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java index c6753a3dff1a5..c7fdebdcd3a9b 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/test/SecuritySingleNodeTestCase.java @@ -56,9 +56,9 @@ public abstract class SecuritySingleNodeTestCase extends ESSingleNodeTestCase { private static SecuritySettingsSource SECURITY_DEFAULT_SETTINGS = null; - private static CustomSecuritySettingsSource customSecuritySettingsSource = null; private static RestClient restClient = null; private static SecureString BOOTSTRAP_PASSWORD = null; + protected static CustomSecuritySettingsSource customSecuritySettingsSource = null; @BeforeClass public static void generateBootstrapPassword() { @@ -239,9 +239,9 @@ protected boolean transportSSLEnabled() { return randomBoolean(); } - private class CustomSecuritySettingsSource extends SecuritySettingsSource { + protected class CustomSecuritySettingsSource extends SecuritySettingsSource { - private CustomSecuritySettingsSource(boolean sslEnabled, Path configDir, ESIntegTestCase.Scope scope) { + public CustomSecuritySettingsSource(boolean sslEnabled, Path configDir, ESIntegTestCase.Scope scope) { super(sslEnabled, configDir, scope); } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/PasswordHashAndBootstrapPasswordIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/PasswordHashAndBootstrapPasswordIntegTests.java new file mode 100644 index 0000000000000..1b2b590b7d9ac --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/PasswordHashAndBootstrapPasswordIntegTests.java @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.esnative; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.cluster.health.ClusterIndexHealth; +import org.elasticsearch.common.Priority; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.SecuritySingleNodeTestCase; +import org.elasticsearch.xpack.core.security.user.ElasticUser; + +import java.util.Map; + +import static java.util.Collections.singletonMap; +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class PasswordHashAndBootstrapPasswordIntegTests extends SecuritySingleNodeTestCase { + + @Override + public Settings nodeSettings() { + Settings customSettings = customSecuritySettingsSource.nodeSettings(0, Settings.EMPTY); + MockSecureSettings mockSecuritySettings = new MockSecureSettings(); + mockSecuritySettings.setString("autoconfiguration.password_hash", // password1 + "{PBKDF2_STRETCH}1000$JnmgicthPZkczB8MaQeJiV6IX43h7mSfPSzESqnYYSA=$OZKH5XFNK+M65mcKal6zgugWRcpl6wUXmSQZ6hPy+iw="); + mockSecuritySettings.setString("bootstrap.password", "password"); + Settings.Builder builder = Settings.builder().put(customSettings, true); + builder.setSecureSettings(mockSecuritySettings); + return builder.build(); + } + + @Override + protected boolean addMockHttpTransport() { + return false; + } + + @Override + protected boolean transportSSLEnabled() { return false; } + + public void testBootstrapPwdAuthenticatePwdHashNotIndexNotCreated() { + ElasticsearchStatusException e = expectThrows( ElasticsearchStatusException.class, + () -> client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, + new SecureString("password1".toCharArray())))) + .admin() + .cluster() + .prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus() + .get() ); + + assertThat(e.status(), equalTo(RestStatus.UNAUTHORIZED)); + + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, + new SecureString("password".toCharArray())))) + .admin() + .cluster() + .prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus() + .get(); + + assertThat(response, notNullValue()); + assertThat(response.isTimedOut(), equalTo(false)); + assertThat(response.status(), equalTo(RestStatus.OK)); + assertThat(response.getStatus(), equalTo(ClusterHealthStatus.GREEN)); + boolean securityIndexCreated = false; + for (Map.Entry indexEntry: response.getIndices().entrySet()) { + if (indexEntry.getKey().startsWith(".security")) { + securityIndexCreated = true; + break; + } + } + assertFalse(securityIndexCreated); + } +} diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/PasswordHashPromotionIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/PasswordHashPromotionIntegTests.java new file mode 100644 index 0000000000000..e6e244d0a876f --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/esnative/PasswordHashPromotionIntegTests.java @@ -0,0 +1,155 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.authc.esnative; + +import org.elasticsearch.ElasticsearchStatusException; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.health.ClusterHealthStatus; +import org.elasticsearch.cluster.health.ClusterIndexHealth; +import org.elasticsearch.common.Priority; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.test.SecuritySettingsSource; +import org.elasticsearch.test.SecuritySingleNodeTestCase; +import org.elasticsearch.xpack.core.security.action.user.PutUserAction; +import org.elasticsearch.xpack.core.security.action.user.PutUserRequest; +import org.elasticsearch.xpack.core.security.action.user.PutUserResponse; +import org.elasticsearch.xpack.core.security.user.ElasticUser; + +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import static java.util.Collections.singletonMap; +import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; + +public class PasswordHashPromotionIntegTests extends SecuritySingleNodeTestCase { + + @Override + public Settings nodeSettings() { + Settings customSettings = customSecuritySettingsSource.nodeSettings(0, Settings.EMPTY); + MockSecureSettings mockSecuritySettings = new MockSecureSettings(); + mockSecuritySettings.setString("autoconfiguration.password_hash", + "{PBKDF2_STRETCH}1000$JnmgicthPZkczB8MaQeJiV6IX43h7mSfPSzESqnYYSA=$OZKH5XFNK+M65mcKal6zgugWRcpl6wUXmSQZ6hPy+iw="); + Settings.Builder builder = Settings.builder().put(customSettings, false); // don't bring in bootstrap.password + builder.setSecureSettings(mockSecuritySettings); + return builder.build(); + } + + @Override + protected boolean addMockHttpTransport() { + return false; + } + + @Override + protected boolean transportSSLEnabled() { return false; } + + public void testAuthenticate() { + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, + new SecureString("password1".toCharArray())))) + .admin() + .cluster() + .prepareHealth() + .setWaitForEvents(Priority.LANGUID) + .setWaitForNodes(Integer.toString(1)) + .setWaitForGreenStatus() + .get(); + + assertThat(response, notNullValue()); + assertThat(response.isTimedOut(), equalTo(false)); + assertThat(response.status(), equalTo(RestStatus.OK)); + assertThat(response.getStatus(), equalTo(ClusterHealthStatus.GREEN)); + assertTrue(securityIndexExists()); + + // Now as the document exists let's try to authenticate again + response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, + new SecureString("password1".toCharArray())))) + .admin() + .cluster() + .prepareHealth() + .get(); + + assertThat(response, notNullValue()); + assertThat(response.status(), equalTo(RestStatus.OK)); + } + + public void testInvalidPasswordHashNoSecurityIndex() { + ElasticsearchStatusException e = expectThrows( ElasticsearchStatusException.class, + () -> client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, + new SecureString("password".toCharArray())))) + .admin() + .cluster() + .prepareHealth() + .get() ); + + assertThat(e.status(), equalTo(RestStatus.UNAUTHORIZED)); + assertFalse(securityIndexExists()); + } + + public void testSecurityIndexExistsButElasticuserNot() throws Exception { + // Create a user to create the Index + createUser("user", SecuritySettingsSource.TEST_PASSWORD_HASHED.toCharArray(), Strings.EMPTY_ARRAY); + assertTrue(securityIndexExists()); + + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue(ElasticUser.NAME, + new SecureString("password1".toCharArray())))) + .admin() + .cluster() + .prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus() + .get(); + assertThat(response, notNullValue()); + assertThat(response.isTimedOut(), equalTo(false)); + assertThat(response.status(), equalTo(RestStatus.OK)); + } + + // TODO: Add a test a test where we index the document for the elastic user manually and then we try to authenticate with the + // autoconfigured password hash and it fails + + private void createUser(String username, char[] password, String[] roles) throws ExecutionException, InterruptedException { + final PutUserRequest putUserRequest = new PutUserRequest(); + putUserRequest.username(username); + putUserRequest.roles(roles); + putUserRequest.passwordHash(password); + PlainActionFuture listener = new PlainActionFuture<>(); + final Client client = client().filterWithHeader( + Map.of("Authorization", basicAuthHeaderValue("test_user", new SecureString("x-pack-test-password" + .toCharArray())))); + client.execute(PutUserAction.INSTANCE, putUserRequest, listener); + final PutUserResponse putUserResponse = listener.get(); + assertTrue(putUserResponse.created()); + } + + private boolean securityIndexExists () { + ClusterHealthResponse response = client() + .filterWithHeader(singletonMap("Authorization", basicAuthHeaderValue("test_user", + new SecureString("x-pack-test-password".toCharArray())))) + .admin() + .cluster() + .prepareHealth().setWaitForEvents(Priority.LANGUID).setWaitForGreenStatus() + .get(); + + boolean securityIndexExists = false; + for (Map.Entry indexEntry: response.getIndices().entrySet()) { + if (indexEntry.getKey().startsWith(".security")) { + securityIndexExists = true; + break; + } + } + return securityIndexExists; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/GenerateInitialBuiltinUsersPasswordListener.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/GenerateInitialBuiltinUsersPasswordListener.java index 586a7f084c978..563f7b03c717b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/GenerateInitialBuiltinUsersPasswordListener.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/GenerateInitialBuiltinUsersPasswordListener.java @@ -48,6 +48,7 @@ public void accept(SecurityIndexManager.State previousState, SecurityIndexManage elasticPassword.getChars(), DocWriteRequest.OpType.CREATE, WriteRequest.RefreshPolicy.IMMEDIATE, + this::outputOnError, ActionListener.wrap(result -> { nativeUsersStore .updateReservedUser( @@ -55,6 +56,7 @@ public void accept(SecurityIndexManager.State previousState, SecurityIndexManage kibanaSystemPassword.getChars(), DocWriteRequest.OpType.CREATE, WriteRequest.RefreshPolicy.IMMEDIATE, + this::outputOnError, ActionListener.wrap( r -> { outputOnSuccess(elasticPassword, kibanaSystemPassword); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java index 47bdae1d846d2..51a2ca9dd9b1e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/NativeUsersStore.java @@ -10,6 +10,7 @@ import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.DocWriteRequest; @@ -22,6 +23,7 @@ import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.ContextPreservingActionListener; import org.elasticsearch.action.support.TransportActions; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.action.update.UpdateResponse; import org.elasticsearch.client.Client; @@ -35,6 +37,7 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.IndexNotFoundException; import org.elasticsearch.index.engine.DocumentMissingException; +import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.search.SearchHit; @@ -48,6 +51,7 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm; import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.user.ElasticUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.security.user.User.Fields; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -61,6 +65,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import static org.elasticsearch.rest.RestStatus.INTERNAL_SERVER_ERROR; import static org.elasticsearch.search.SearchService.DEFAULT_KEEPALIVE_SETTING; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; @@ -258,6 +263,7 @@ public void onFailure(Exception e) { request.passwordHash(), DocWriteRequest.OpType.INDEX, request.getRefreshPolicy(), + listener::onFailure, listener ); } else { @@ -284,9 +290,10 @@ public void updateReservedUser( char[] passwordHash, DocWriteRequest.OpType opType, RefreshPolicy refresh, + Consumer consumer, ActionListener listener ) { - securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { + securityIndex.prepareIndexIfNeededThenExecute(consumer, () -> { executeAsyncWithOrigin( client.threadPool().getThreadContext(), SECURITY_ORIGIN, @@ -309,6 +316,17 @@ public void updateReservedUser( }); } + void storeAutoconfiguredElasticUser(ReservedUserInfo elasticUserInfo, ActionListener listener) { + updateReservedUser(ElasticUser.NAME, elasticUserInfo.passwordHash, DocWriteRequest.OpType.CREATE, + WriteRequest.RefreshPolicy.IMMEDIATE, (e) -> { + if (e instanceof VersionConflictEngineException) { + listener.onFailure(e); + } else { + listener.onFailure(new ElasticsearchStatusException(e.getMessage(), INTERNAL_SERVER_ERROR, e.getCause())); + } + }, listener.delegateFailure((l, v) -> l.onResponse(elasticUserInfo.deepClone()))); + } + /** * Asynchronous method to put a user. A put user request without a password hash is treated as an update and will fail with a * {@link ValidationException} if the user does not exist. If a password hash is provided, then we issue a update request with an diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java index 2dea4b8b1f2c1..f5fd789fea1af 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealm.java @@ -16,6 +16,7 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Nullable; import org.elasticsearch.env.Environment; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.XPackSettings; @@ -59,14 +60,15 @@ public class ReservedRealm extends CachingUsernamePasswordRealm { private final ReservedUserInfo bootstrapUserInfo; public static final Setting BOOTSTRAP_ELASTIC_PASSWORD = SecureSetting.secureString("bootstrap.password", KeyStoreWrapper.SEED_SETTING); - public static final Setting AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH = - SecureSetting.secureString("autoconfig.password_hash", null); + public static final Setting AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH = + SecureSetting.secureString("autoconfiguration.password_hash", null); private final NativeUsersStore nativeUsersStore; private final AnonymousUser anonymousUser; private final boolean realmEnabled; private final boolean anonymousEnabled; private final SecurityIndexManager securityIndex; + private final boolean autoconfigured; private final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(logger.getName()); @@ -83,8 +85,18 @@ public ReservedRealm(Environment env, Settings settings, NativeUsersStore native this.anonymousEnabled = AnonymousUser.isAnonymousEnabled(settings); this.securityIndex = securityIndex; final Hasher reservedRealmHasher = Hasher.resolve(XPackSettings.PASSWORD_HASHING_ALGORITHM.get(settings)); - final char[] hash = BOOTSTRAP_ELASTIC_PASSWORD.get(settings).length() == 0 ? new char[0] : - reservedRealmHasher.hash(BOOTSTRAP_ELASTIC_PASSWORD.get(settings)); + char[] hash; + autoconfigured = + AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH.exists(settings) && BOOTSTRAP_ELASTIC_PASSWORD.exists(settings) == false; + if (autoconfigured) { + hash = AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH.get(settings).getChars(); + } else { + if (BOOTSTRAP_ELASTIC_PASSWORD.get(settings).length() == 0) { + hash = new char[0]; + } else{ + hash = reservedRealmHasher.hash(BOOTSTRAP_ELASTIC_PASSWORD.get(settings)); + } + } bootstrapUserInfo = new ReservedUserInfo(hash, true); } @@ -95,7 +107,7 @@ protected void doAuthenticate(UsernamePasswordToken token, ActionListener { + getUserInfo(token.principal(), token.credentials(), ActionListener.wrap((userInfo) -> { AuthenticationResult result; if (userInfo != null) { try { @@ -134,7 +146,7 @@ protected void doLookupUser(String username, ActionListener listener) { } else if (AnonymousUser.isAnonymousUsername(username, config.settings())) { listener.onResponse(anonymousEnabled ? anonymousUser : null); } else { - getUserInfo(username, ActionListener.wrap((userInfo) -> { + getUserInfo(username, null, ActionListener.wrap((userInfo) -> { if (userInfo != null) { listener.onResponse(getUser(username, userInfo)); } else { @@ -170,7 +182,6 @@ private User getUser(String username, ReservedUserInfo userInfo) { } } - public void users(ActionListener> listener) { if (realmEnabled == false) { listener.onResponse(anonymousEnabled ? Collections.singletonList(anonymousUser) : Collections.emptyList()); @@ -212,24 +223,33 @@ public void users(ActionListener> listener) { } - private void getUserInfo(final String username, ActionListener listener) { + private void getUserInfo(final String username, @Nullable SecureString credentials, ActionListener listener) { if (securityIndex.indexExists() == false) { - listener.onResponse(getDefaultUserInfo(username)); + getAutoconfiguredOrDefaultUser(username, credentials, listener); } else { nativeUsersStore.getReservedUserInfo(username, ActionListener.wrap((userInfo) -> { if (userInfo == null) { - listener.onResponse(getDefaultUserInfo(username)); + getAutoconfiguredOrDefaultUser(username, credentials, listener); } else { listener.onResponse(userInfo); } }, (e) -> { logger.error((Supplier) () -> - new ParameterizedMessage("failed to retrieve password hash for reserved user [{}]", username), e); + new ParameterizedMessage("failed to retrieve password hash for reserved user [{}]", username), e); listener.onResponse(null); })); } } + private void getAutoconfiguredOrDefaultUser(final String username, SecureString credentials, + ActionListener listener) { + if (autoconfigured && username.equals(ElasticUser.NAME) && bootstrapUserInfo.verifyPassword(credentials)) { + nativeUsersStore.storeAutoconfiguredElasticUser(bootstrapUserInfo, listener); + } else { + listener.onResponse(getDefaultUserInfo(username)); + } + } + private void logDeprecatedUser(final User user){ Map metadata = user.metadata(); if (Boolean.TRUE.equals(metadata.get(MetadataUtils.DEPRECATED_METADATA_KEY))) { @@ -250,5 +270,6 @@ private ReservedUserInfo getDefaultUserInfo(String username) { public static void addSettings(List> settingsList) { settingsList.add(BOOTSTRAP_ELASTIC_PASSWORD); + settingsList.add(AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHash.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHash.java index c96aeead444a0..60e77fbdb8db5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHash.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHash.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; -import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH; +import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH; import static org.elasticsearch.xpack.security.tool.CommandUtils.generatePassword; /** @@ -53,7 +53,7 @@ protected void execute(Terminal terminal, OptionSet options, Environment env) th SecureString elasticPassword = new SecureString(generatePassword(20)); KeyStoreWrapper nodeKeystore = KeyStoreWrapper.bootstrap(env.configFile(), () -> new SecureString(new char[0])) ) { - nodeKeystore.setString(AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), hasher.hash(elasticPassword)); + nodeKeystore.setString(AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), hasher.hash(elasticPassword)); nodeKeystore.save(env.configFile(), new char[0]); terminal.print(Terminal.Verbosity.NORMAL, elasticPassword.toString()); } catch (Exception e) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java index 0b55398e373c6..e2dc7cdb70b28 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java @@ -114,7 +114,7 @@ public SecuritySettingsSource(boolean sslEnabled, Path parentFolder, Scope scope } } - Path nodePath(final int nodeOrdinal) { + public Path nodePath(final int nodeOrdinal) { return parentFolder.resolve(subfolderPrefix + "-" + nodeOrdinal); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java index 5d3f2f518bb19..1ef7d9c5d5f41 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/esnative/ReservedRealmTests.java @@ -51,6 +51,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Matchers.anyObject; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; @@ -386,6 +387,53 @@ public void testBootstrapElasticPasswordFailsOnceElasticUserExists() throws Exce assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); } + public void testPasswordHashFailsOnElasticUserExists() throws Exception { + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + mockSecureSettings.setString("autoconfiguration.password_hash", "{PBKDF2_STRETCH}1000$JnmgicthPZkczB8MaQeJiV6IX" + + "43h7mSfPSzESqnYYSA=$OZKH5XFNK+M65mcKal6zgugWRcpl6wUXmSQZ6hPy+iw="); + Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + when(securityIndex.indexExists()).thenReturn(true); + final Hasher hasher = Hasher.resolve("pbkdf2_stretch_1000"); + doAnswer(getAnswer(true, new SecureString("password".toCharArray()), hasher)) + .when(usersStore).getReservedUserInfo(eq("elastic"), anyActionListener()); + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, + new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + PlainActionFuture listener = new PlainActionFuture<>(); + SecureString password = new SecureString("password1".toCharArray()); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), password), listener); + assertFailedAuthentication(listener, "elastic"); + } + + @SuppressWarnings("unchecked") + public void testPasswordHashSucceedOnIndexExistsButElasticuserNot() throws Exception { + MockSecureSettings mockSecureSettings = new MockSecureSettings(); + mockSecureSettings.setString("autoconfiguration.password_hash", "{PBKDF2_STRETCH}1000$JnmgicthPZkczB8MaQeJiV6IX" + + "43h7mSfPSzESqnYYSA=$OZKH5XFNK+M65mcKal6zgugWRcpl6wUXmSQZ6hPy+iw="); + Settings settings = Settings.builder().setSecureSettings(mockSecureSettings).build(); + ReservedUserInfo elasticUserInfo = + new ReservedUserInfo(mockSecureSettings.getString("autoconfiguration.password_hash").getChars(), true); + when(securityIndex.indexExists()).thenReturn(true); + doAnswer((i) -> { + @SuppressWarnings("rawtypes") + ActionListener callback = (ActionListener) i.getArguments()[1]; + callback.onResponse(null); + return null; + }).when(usersStore).getReservedUserInfo(eq(ElasticUser.NAME), anyActionListener()); + doAnswer((i) -> { + @SuppressWarnings("rawtypes") + ActionListener callbackOnCreate = (ActionListener) i.getArguments()[1]; + callbackOnCreate.onResponse(elasticUserInfo); + return null; + }).when(usersStore).storeAutoconfiguredElasticUser(anyObject(), anyActionListener()); + final ReservedRealm reservedRealm = new ReservedRealm(mock(Environment.class), settings, usersStore, + new AnonymousUser(Settings.EMPTY), securityIndex, threadPool); + PlainActionFuture listener = new PlainActionFuture<>(); + SecureString password = new SecureString("password1".toCharArray()); + reservedRealm.doAuthenticate(new UsernamePasswordToken(new ElasticUser(true).principal(), password), listener); + final AuthenticationResult result = listener.get(); + assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + } + public void testBootstrapElasticPasswordWorksBeforeSecurityIndexExists() throws ExecutionException, InterruptedException { MockSecureSettings mockSecureSettings = new MockSecureSettings(); mockSecureSettings.setString("bootstrap.password", "foobar"); diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHashTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHashTests.java index 2b279eb9b3c11..a8c765a446ca9 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHashTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/AutoConfigGenerateElasticPasswordHashTests.java @@ -31,7 +31,7 @@ import java.util.Map; import static org.elasticsearch.test.SecurityIntegTestCase.getFastStoredHashAlgoForTests; -import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH; +import static org.elasticsearch.xpack.security.authc.esnative.ReservedRealm.AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.emptyString; import static org.hamcrest.Matchers.equalTo; @@ -98,7 +98,7 @@ public void testSuccessfullyGenerateAndStoreHash() throws Exception { assertNotNull(keyStoreWrapper); keyStoreWrapper.decrypt(new char[0]); assertThat(keyStoreWrapper.getSettingNames(), - containsInAnyOrder(AUTOCONFIG_BOOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), "keystore.seed")); + containsInAnyOrder(AUTOCONFIG_BOOTSTRAP_ELASTIC_PASSWORD_HASH.getKey(), "keystore.seed")); } public void testExistingKeystoreWithWrongPassword() throws Exception {