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..c0d02025ea411 --- /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); } 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());