From 58f6269eedcd9a97183fa42a56f4d959664440a7 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Tue, 10 Aug 2021 23:25:01 +0300 Subject: [PATCH 1/3] Set elastic password from stored hash In package installations, we will be generating the password of the elastic user on installation and we will be storing the hash of it to the elasticsearch.keystore. This change ensures that this password hash will be picked up by the Security plugin in the starting node and will be set as the password of the elastic user in the security index. --- .../xpack/core/XPackSettings.java | 5 +++ .../xpack/security/Security.java | 34 +++++++++++++++---- .../xpack/security/SecurityTests.java | 15 ++++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java index 0563e72c7abfb..1c86e01b72987 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/XPackSettings.java @@ -8,6 +8,8 @@ package org.elasticsearch.xpack.core; import org.apache.logging.log4j.LogManager; +import org.elasticsearch.common.settings.SecureSetting; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.ssl.SslVerificationMode; import org.elasticsearch.jdk.JavaVersion; import org.elasticsearch.common.settings.Setting; @@ -213,6 +215,8 @@ private XPackSettings() { public static final String TRANSPORT_SSL_PREFIX = SecurityField.setting("transport.ssl."); private static final SSLConfigurationSettings TRANSPORT_SSL = SSLConfigurationSettings.withPrefix(TRANSPORT_SSL_PREFIX, true); + public static final Setting ELASTIC_PASSWORD_HASH = SecureSetting.secureString("autoconfiguration.password_hash", null); + /** Returns all settings created in {@link XPackSettings}. */ public static List> getAllSettings() { ArrayList> settings = new ArrayList<>(); @@ -232,6 +236,7 @@ public static List> getAllSettings() { settings.add(USER_SETTING); settings.add(PASSWORD_HASHING_ALGORITHM); settings.add(ENROLLMENT_ENABLED); + settings.add(ELASTIC_PASSWORD_HASH); return Collections.unmodifiableList(settings); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index fcb755088abba..2ebef26e2fe7e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -32,6 +32,7 @@ import org.elasticsearch.common.network.NetworkService; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.IndexScopedSettings; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; @@ -131,6 +132,7 @@ import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; +import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest; import org.elasticsearch.xpack.core.security.action.user.DeleteUserAction; import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesAction; import org.elasticsearch.xpack.core.security.action.user.GetUsersAction; @@ -335,6 +337,7 @@ import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; import static org.elasticsearch.xpack.core.XPackSettings.API_KEY_SERVICE_ENABLED_SETTING; +import static org.elasticsearch.xpack.core.XPackSettings.ELASTIC_PASSWORD_HASH; import static org.elasticsearch.xpack.core.XPackSettings.HTTP_SSL_ENABLED; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; import static org.elasticsearch.xpack.security.operator.OperatorPrivileges.OPERATOR_PRIVILEGES_ENABLED; @@ -375,6 +378,8 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin, private final List securityExtensions = new ArrayList<>(); private final SetOnce transportReference = new SetOnce<>(); private final SetOnce scriptServiceReference = new SetOnce<>(); + private final SetOnce elasticPasswordHash = new SetOnce<>(); + private final SetOnce nativeUsersStore = new SetOnce<>(); public Security(Settings settings, final Path configPath) { this(settings, configPath, Collections.emptyList()); @@ -387,9 +392,6 @@ public Security(Settings settings, final Path configPath) { this.enabled = XPackSettings.SECURITY_ENABLED.get(settings); if (enabled) { runStartupChecks(settings); - // we load them all here otherwise we can't access secure settings since they are closed once the checks are - // fetched - Automatons.updateConfiguration(settings); } else { this.bootstrapChecks.set(Collections.emptyList()); @@ -411,6 +413,7 @@ protected Clock getClock() { } protected SSLService getSslService() { return XPackPlugin.getSharedSslService(); } protected XPackLicenseState getLicenseState() { return XPackPlugin.getSharedLicenseState(); } + protected SecureString getElasticPasswordHash() { return this.elasticPasswordHash.get(); } @Override public Collection createComponents(Client client, ClusterService clusterService, ThreadPool threadPool, @@ -439,8 +442,6 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste scriptServiceReference.set(scriptService); - // We need to construct the checks here while the secure settings are still available. - // If we wait until #getBoostrapChecks the secure settings will have been cleared/closed. final List checks = new ArrayList<>(); checks.addAll(Arrays.asList( new ApiKeySSLBootstrapCheck(), @@ -465,6 +466,12 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste securityIndex.set(SecurityIndexManager.buildSecurityIndexManager(client, clusterService, SECURITY_MAIN_INDEX_DESCRIPTOR)); + // Store this because when the listener we register will be called, secure settings will be closed + if (ELASTIC_PASSWORD_HASH.exists(settings)) { + elasticPasswordHash.set(ELASTIC_PASSWORD_HASH.get(settings)); + securityIndex.get().addStateListener(this::possiblySetElasticPassword); + } + final TokenService tokenService = new TokenService( settings, Clock.systemUTC(), @@ -480,6 +487,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste // realms construction final NativeUsersStore nativeUsersStore = new NativeUsersStore(settings, client, securityIndex.get()); + this.nativeUsersStore.set(nativeUsersStore); final NativeRoleMappingStore nativeRoleMappingStore = new NativeRoleMappingStore(settings, client, securityIndex.get(), scriptService); final AnonymousUser anonymousUser = new AnonymousUser(settings); @@ -506,7 +514,6 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste components.add(reservedRealm); securityIndex.get().addStateListener(nativeRoleMappingStore::onSecurityIndexStateChange); - final CacheInvalidatorRegistry cacheInvalidatorRegistry = new CacheInvalidatorRegistry(); cacheInvalidatorRegistry.registerAlias("service", Set.of("file_service_account_token", "index_service_account_token")); components.add(cacheInvalidatorRegistry); @@ -612,6 +619,21 @@ auditTrailService, failureHandler, threadPool, anonymousUser, getAuthorizationEn return components; } + protected void possiblySetElasticPassword(SecurityIndexManager.State previousState, SecurityIndexManager.State currentState) { + if (previousState.equals(SecurityIndexManager.State.UNRECOVERED_STATE) + && currentState.equals(SecurityIndexManager.State.UNRECOVERED_STATE) == false + && securityIndex.get().indexExists() == false + && elasticPasswordHash.get() != null) { + final ChangePasswordRequest request = new ChangePasswordRequest(); + request.username("elastic"); + request.passwordHash(elasticPasswordHash.get().getChars()); + nativeUsersStore.get().changePassword(request, ActionListener.wrap( + r -> {}, + e -> logger.warn("failed to set the elastic user password from the value of [" + ELASTIC_PASSWORD_HASH.getKey() + "]"))); + elasticPasswordHash.get().close(); + } + } + private AuthorizationEngine getAuthorizationEngine() { AuthorizationEngine authorizationEngine = null; String extensionName = null; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java index 4ae0ca66a8574..7b4f8e117b43d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityTests.java @@ -25,6 +25,7 @@ import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.network.NetworkModule; import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.MockSecureSettings; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.settings.SettingsModule; @@ -93,6 +94,7 @@ import static java.util.Collections.emptyMap; import static org.elasticsearch.cluster.metadata.IndexMetadata.INDEX_FORMAT_SETTING; +import static org.elasticsearch.xpack.core.XPackSettings.ELASTIC_PASSWORD_HASH; import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; import static org.elasticsearch.xpack.security.support.SecurityIndexManager.INTERNAL_MAIN_INDEX_FORMAT; import static org.hamcrest.Matchers.containsString; @@ -651,6 +653,19 @@ public void testSecurityStatusMessageInLog() throws Exception{ } } + public void testNoElasticPasswordHashInKeystore() throws Exception { + createComponents(Settings.EMPTY); + assertNull(security.getElasticPasswordHash()); + } + + public void testElasticPasswordHashInKeystore() throws Exception { + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString(ELASTIC_PASSWORD_HASH.getKey(), "some_hash_here"); + Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); + createComponents(settings); + assertThat(security.getElasticPasswordHash().toString(), equalTo("some_hash_here")); + } + private void logAndFail(Exception e) { logger.error("unexpected exception", e); fail("unexpected exception " + e.getMessage()); From 0356619c9f302c62fd877ff52677f77e4522908c Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 11 Aug 2021 13:32:42 +0300 Subject: [PATCH 2/3] add a packaging test for verification --- ...PackageSecurityAutoconfigurationTests.java | 44 +++++++++++++++++++ .../packaging/util/ServerUtils.java | 7 +++ 2 files changed, 51 insertions(+) create mode 100644 qa/os/src/test/java/org/elasticsearch/packaging/test/PackageSecurityAutoconfigurationTests.java diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageSecurityAutoconfigurationTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageSecurityAutoconfigurationTests.java new file mode 100644 index 0000000000000..9ce5fd89fb746 --- /dev/null +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageSecurityAutoconfigurationTests.java @@ -0,0 +1,44 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.packaging.test; + +import org.elasticsearch.packaging.util.Shell; +import org.junit.BeforeClass; + +import static org.elasticsearch.packaging.util.Packages.assertInstalled; +import static org.elasticsearch.packaging.util.Packages.assertRemoved; +import static org.elasticsearch.packaging.util.Packages.installPackage; +import static org.elasticsearch.packaging.util.Packages.verifyPackageInstallation; +import static org.elasticsearch.packaging.util.ServerUtils.validateCredentials; +import static org.hamcrest.CoreMatchers.containsString; +import static org.junit.Assume.assumeTrue; + +public class PackageSecurityAutoconfigurationTests extends PackagingTestCase { + + @BeforeClass + public static void filterDistros() { + assumeTrue("rpm or deb", distribution.isPackage()); + } + + public void test10ElasticPasswordHash() throws Exception { + assertRemoved(distribution()); + installation = installPackage(sh, distribution()); + assertInstalled(distribution()); + verifyPackageInstallation(installation, distribution(), sh); + Shell.Result keystoreListResult = installation.executables().keystoreTool.run("list"); + // Keystore should be created already by the installation and it should contain only "keystore.seed" at this point + assertThat(keystoreListResult.stdout, containsString("keystore.seed")); + // With future changes merged, this would be automatically populated on installation. For now, add it manually + installation.executables().keystoreTool. + // $2a$10$R2oFwbHR/9x9.e/bQpJ6IeHKUVP08KHQ9LcZPMlWeyuQuYboR82fm is the hash of thisisalongenoughpassword + run("add -x autoconfiguration.password_hash", "$2a$10$R2oFwbHR/9x9.e/bQpJ6IeHKUVP08KHQ9LcZPMlWeyuQuYboR82fm"); + startElasticsearch(); + validateCredentials("elastic", "thisisalongenoughpassword", null); + } +} diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java b/qa/os/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java index 453ace7ea39d7..70ddac43c3156 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/util/ServerUtils.java @@ -272,6 +272,13 @@ public static void runElasticsearchTests(String username, String password) throw makeRequest(Request.Delete("http://localhost:9200/library"), username, password, null); } + public static void validateCredentials(String username, String password, Path caCert) throws Exception { + final HttpResponse response = execute(Request.Get("http://localhost:9200/"), username, password, caCert); + if (response.getStatusLine().getStatusCode() == 401) { + throw new RuntimeException("Failed to authenticate as [" + username + ":" + password + "]"); + } + } + public static String makeRequest(Request request) throws Exception { return makeRequest(request, null, null, null); } From c82c3084e545560b0d12c1733d115f59b2c32ed8 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 11 Aug 2021 14:59:30 +0300 Subject: [PATCH 3/3] spotless fix --- .../packaging/test/PackageSecurityAutoconfigurationTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageSecurityAutoconfigurationTests.java b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageSecurityAutoconfigurationTests.java index 9ce5fd89fb746..c0d02025ea411 100644 --- a/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageSecurityAutoconfigurationTests.java +++ b/qa/os/src/test/java/org/elasticsearch/packaging/test/PackageSecurityAutoconfigurationTests.java @@ -36,7 +36,7 @@ public void test10ElasticPasswordHash() throws Exception { assertThat(keystoreListResult.stdout, containsString("keystore.seed")); // With future changes merged, this would be automatically populated on installation. For now, add it manually installation.executables().keystoreTool. - // $2a$10$R2oFwbHR/9x9.e/bQpJ6IeHKUVP08KHQ9LcZPMlWeyuQuYboR82fm is the hash of thisisalongenoughpassword + // $2a$10$R2oFwbHR/9x9.e/bQpJ6IeHKUVP08KHQ9LcZPMlWeyuQuYboR82fm is the hash of thisisalongenoughpassword run("add -x autoconfiguration.password_hash", "$2a$10$R2oFwbHR/9x9.e/bQpJ6IeHKUVP08KHQ9LcZPMlWeyuQuYboR82fm"); startElasticsearch(); validateCredentials("elastic", "thisisalongenoughpassword", null);