From 20ca64087f76676ac56ff09fa4e4cd15282ebefc Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 28 Aug 2025 16:32:21 +0200 Subject: [PATCH 01/24] Add signing configuration for cross cluster api keys --- .../transport/RemoteClusterSettings.java | 16 +- .../CrossClusterApiKeySignerIntegTests.java | 127 +++++++++ ...igningConfigurationReloaderIntegTests.java | 268 ++++++++++++++++++ .../xpack/security/Security.java | 19 +- .../transport/CrossClusterApiKeySigner.java | 252 ++++++++++++++++ .../CrossClusterApiKeySignerReloader.java | 215 ++++++++++++++ .../CrossClusterApiKeySignerSettings.java | 106 +++++++ .../SecurityServerTransportInterceptor.java | 7 +- .../transport/X509CertificateSignature.java | 169 +++++++++++ .../X509CertificateSignatureTests.java | 40 +++ ...CrossClusterApiKeySignerReloaderTests.java | 157 ++++++++++ .../CrossClusterApiKeySignerTests.java | 104 +++++++ ...curityServerTransportInterceptorTests.java | 31 +- .../xpack/security/signature/signing.jks | Bin 0 -> 5237 bytes .../xpack/security/signature/signing_ec.crt | 11 + .../xpack/security/signature/signing_ec.key | 8 + .../xpack/security/signature/signing_rsa.crt | 20 ++ .../xpack/security/signature/signing_rsa.key | 27 ++ 18 files changed, 1560 insertions(+), 17 deletions(-) create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java create mode 100644 x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/signature/X509CertificateSignatureTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloaderTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing.jks create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_ec.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_ec.key create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_rsa.crt create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_rsa.key diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterSettings.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterSettings.java index f5b72bf6e40b8..f2bc9a24ffddb 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterSettings.java @@ -42,12 +42,14 @@ public class RemoteClusterSettings { + public static final String REMOTE_CLUSTER_SETTINGS_PREFIX = "cluster.remote."; + public static final TimeValue DEFAULT_INITIAL_CONNECTION_TIMEOUT = TimeValue.timeValueSeconds(30); /** * The initial connect timeout for remote cluster connections */ public static final Setting REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING = Setting.positiveTimeSetting( - "cluster.remote.initial_connect_timeout", + REMOTE_CLUSTER_SETTINGS_PREFIX + "initial_connect_timeout", DEFAULT_INITIAL_CONNECTION_TIMEOUT, Setting.Property.NodeScope ); @@ -59,13 +61,13 @@ public class RemoteClusterSettings { * The value of the setting is expected to be a boolean, {@code true} for nodes that can become gateways, {@code false} otherwise. */ public static final Setting REMOTE_NODE_ATTRIBUTE = Setting.simpleString( - "cluster.remote.node.attr", + REMOTE_CLUSTER_SETTINGS_PREFIX + "node.attr", Setting.Property.NodeScope ); public static final boolean DEFAULT_SKIP_UNAVAILABLE = true; public static final Setting.AffixSetting REMOTE_CLUSTER_SKIP_UNAVAILABLE = Setting.affixKeySetting( - "cluster.remote.", + REMOTE_CLUSTER_SETTINGS_PREFIX, "skip_unavailable", (ns, key) -> boolSetting( key, @@ -77,7 +79,7 @@ public class RemoteClusterSettings { ); public static final Setting.AffixSetting REMOTE_CLUSTER_PING_SCHEDULE = Setting.affixKeySetting( - "cluster.remote.", + REMOTE_CLUSTER_SETTINGS_PREFIX, "transport.ping_schedule", (ns, key) -> timeSetting( key, @@ -89,7 +91,7 @@ public class RemoteClusterSettings { ); public static final Setting.AffixSetting REMOTE_CLUSTER_COMPRESS = Setting.affixKeySetting( - "cluster.remote.", + REMOTE_CLUSTER_SETTINGS_PREFIX, "transport.compress", (ns, key) -> enumSetting( Compression.Enabled.class, @@ -102,7 +104,7 @@ public class RemoteClusterSettings { ); public static final Setting.AffixSetting REMOTE_CLUSTER_COMPRESSION_SCHEME = Setting.affixKeySetting( - "cluster.remote.", + REMOTE_CLUSTER_SETTINGS_PREFIX, "transport.compression_scheme", (ns, key) -> enumSetting( Compression.Scheme.class, @@ -115,7 +117,7 @@ public class RemoteClusterSettings { ); public static final Setting.AffixSetting REMOTE_CLUSTER_CREDENTIALS = Setting.affixKeySetting( - "cluster.remote.", + REMOTE_CLUSTER_SETTINGS_PREFIX, "credentials", key -> SecureSetting.secureString(key, null) ); diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java new file mode 100644 index 0000000000000..6e0ed12b75c97 --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java @@ -0,0 +1,127 @@ +/* + * 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.transport; + +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.ssl.PemKeyConfig; +import org.elasticsearch.test.SecurityIntegTestCase; + +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_CERT_PATH; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_ALIAS; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_PATH; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_SECURE_PASSWORD; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_PATH; + +public class CrossClusterApiKeySignerIntegTests extends SecurityIntegTestCase { + + private static final String DYNAMIC_TEST_CLUSTER_ALIAS = "dynamic_test_cluster"; + private static final String STATIC_TEST_CLUSTER_ALIAS = "static_test_cluster"; + + public void testSignWithPemKeyConfig() { + final CrossClusterApiKeySigner signer = internalCluster().getInstance( + CrossClusterApiKeySigner.class, + internalCluster().getRandomNodeName() + ); + final String[] testHeaders = randomArray(5, String[]::new, () -> randomAlphanumericOfLength(randomInt(20))); + + X509CertificateSignature signature = signer.sign(STATIC_TEST_CLUSTER_ALIAS, testHeaders); + signature.certificate().getPublicKey(); + + var keyConfig = new PemKeyConfig( + "signing_rsa.crt", + "signing_rsa.key", + new char[0], + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt").getParent() + ); + + assertEquals(signature.algorithm(), keyConfig.getKeys().getFirst().v2().getSigAlgName()); + assertEquals(signature.certificate(), keyConfig.getKeys().getFirst().v2()); + } + + public void testSignUnknownClusterAlias() { + final CrossClusterApiKeySigner signer = internalCluster().getInstance( + CrossClusterApiKeySigner.class, + internalCluster().getRandomNodeName() + ); + final String[] testHeaders = randomArray(5, String[]::new, () -> randomAlphanumericOfLength(randomInt(20))); + + X509CertificateSignature signature = signer.sign("unknowncluster", testHeaders); + assertNull(signature); + } + + public void testSeveralKeyStoreAliases() { + final CrossClusterApiKeySigner signer = internalCluster().getInstance( + CrossClusterApiKeySigner.class, + internalCluster().getRandomNodeName() + ); + + try { + // Create a new config without an alias. Since there are several aliases in the keystore, no signature should be generated + updateClusterSettings( + Settings.builder() + .put( + SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), + getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks") + ) + ); + + { + X509CertificateSignature signature = signer.sign(DYNAMIC_TEST_CLUSTER_ALIAS, "test", "test"); + assertNull(signature); + } + + // Add an alias from the keystore + updateClusterSettings( + Settings.builder() + .put(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), "wholelottakey") + ); + { + X509CertificateSignature signature = signer.sign(DYNAMIC_TEST_CLUSTER_ALIAS, "test", "test"); + assertNotNull(signature); + } + + // Add an alias not in the keystore + updateClusterSettings( + Settings.builder() + .put(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), "idonotexist") + ); + { + X509CertificateSignature signature = signer.sign(DYNAMIC_TEST_CLUSTER_ALIAS, "test", "test"); + assertNull(signature); + } + } finally { + updateClusterSettings( + Settings.builder() + .putNull(SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey()) + .putNull(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey()) + .setSecureSettings(new MockSecureSettings()) + ); + } + } + + @Override + protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) { + var builder = Settings.builder(); + MockSecureSettings secureSettings = (MockSecureSettings) builder.put(super.nodeSettings(nodeOrdinal, otherSettings)) + .put( + SIGNING_CERT_PATH.getConcreteSettingForNamespace(STATIC_TEST_CLUSTER_ALIAS).getKey(), + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt") + ) + .put( + SIGNING_KEY_PATH.getConcreteSettingForNamespace(STATIC_TEST_CLUSTER_ALIAS).getKey(), + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key") + ) + .getSecureSettings(); + secureSettings.setString( + SIGNING_KEYSTORE_SECURE_PASSWORD.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), + "secretpassword" + ); + return builder.build(); + } +} diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java new file mode 100644 index 0000000000000..f0779ca972ee6 --- /dev/null +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java @@ -0,0 +1,268 @@ +/* + * 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.transport; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.node.reload.NodesReloadSecureSettingsRequest; +import org.elasticsearch.action.admin.cluster.node.reload.NodesReloadSecureSettingsResponse; +import org.elasticsearch.action.admin.cluster.node.reload.TransportNodesReloadSecureSettingsAction; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.settings.KeyStoreWrapper; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.Environment; +import org.elasticsearch.test.SecurityIntegTestCase; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import javax.net.ssl.KeyManagerFactory; + +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_CERT_PATH; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_ALGORITHM; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_ALIAS; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_PATH; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_SECURE_PASSWORD; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_TYPE; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_PATH; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_SECURE_PASSPHRASE; + +public class CrossClusterSigningConfigurationReloaderIntegTests extends SecurityIntegTestCase { + + public void testAddAndRemoveClusterConfigsRuntime() throws Exception { + addAndRemoveClusterConfigsRuntime(randomClusterAliases(), clusterAlias -> { + updateClusterSettings( + Settings.builder() + .put( + SIGNING_CERT_PATH.getConcreteSettingForNamespace(clusterAlias).getKey(), + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt") + ) + .put( + SIGNING_KEY_PATH.getConcreteSettingForNamespace(clusterAlias).getKey(), + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key") + ) + ); + }, clusterAlias -> { + updateClusterSettings( + Settings.builder() + .putNull(SIGNING_CERT_PATH.getConcreteSettingForNamespace(clusterAlias).getKey()) + .putNull(SIGNING_KEY_PATH.getConcreteSettingForNamespace(clusterAlias).getKey()) + ); + }); + } + + public void testAddSecureSettingsConfigRuntime() throws Exception { + addAndRemoveClusterConfigsRuntime(randomClusterAliases(), clusterAlias -> { + writeSecureSettingsToKeyStoreAndReload( + Map.of( + SIGNING_KEYSTORE_SECURE_PASSWORD.getConcreteSettingForNamespace(clusterAlias).getKey(), + "secretpassword".toCharArray() + ) + ); + + updateClusterSettings( + Settings.builder() + .put( + SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(clusterAlias).getKey(), + getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks") + ) + .put(SIGNING_KEYSTORE_TYPE.getConcreteSettingForNamespace(clusterAlias).getKey(), "jks") + .put( + SIGNING_KEYSTORE_ALGORITHM.getConcreteSettingForNamespace(clusterAlias).getKey(), + KeyManagerFactory.getDefaultAlgorithm() + ) + .put(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(clusterAlias).getKey(), "wholelottakey") + ); + }, clusterAlias -> { + updateClusterSettings( + Settings.builder() + .putNull(SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(clusterAlias).getKey()) + .putNull(SIGNING_KEYSTORE_TYPE.getConcreteSettingForNamespace(clusterAlias).getKey()) + .putNull(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(clusterAlias).getKey()) + .putNull(SIGNING_KEYSTORE_ALGORITHM.getConcreteSettingForNamespace(clusterAlias).getKey()) + .setSecureSettings(new MockSecureSettings()) + ); + }); + } + + public void testDependentKeyConfigFilesUpdated() throws Exception { + final CrossClusterApiKeySigner signer = internalCluster().getInstance( + CrossClusterApiKeySigner.class, + internalCluster().getRandomNodeName() + ); + + String testClusterAlias = "test_cluster"; + + try { + // Write passphrase for ec key to keystore + writeSecureSettingsToKeyStoreAndReload( + Map.of(SIGNING_KEY_SECURE_PASSPHRASE.getConcreteSettingForNamespace(testClusterAlias).getKey(), "marshall".toCharArray()) + ); + + assertNull(signer.sign(testClusterAlias, "a_header")); + Path tempDir = createTempDir(); + Path signingCert = tempDir.resolve("signing.crt"); + Files.copy(getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt"), signingCert); + Path signingKey = tempDir.resolve("signing.key"); + Files.copy(getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key"), signingKey); + + Path updatedSigningCert = tempDir.resolve("updated_signing.crt"); + Files.copy(getDataPath("/org/elasticsearch/xpack/security/signature/signing_ec.crt"), updatedSigningCert); + Path updatedSigningKey = tempDir.resolve("updated_signing.key"); + Files.copy(getDataPath("/org/elasticsearch/xpack/security/signature/signing_ec.key"), updatedSigningKey); + + // Add the cluster + updateClusterSettings( + Settings.builder() + .put(SIGNING_CERT_PATH.getConcreteSettingForNamespace(testClusterAlias).getKey(), signingCert) + .put(SIGNING_KEY_PATH.getConcreteSettingForNamespace(testClusterAlias).getKey(), signingKey) + ); + + // Make sure a signature can be created + var signatureBefore = signer.sign(testClusterAlias, "test", "test"); + assertNotNull(signatureBefore); + + Files.move(updatedSigningCert, signingCert, StandardCopyOption.REPLACE_EXISTING); + Files.move(updatedSigningKey, signingKey, StandardCopyOption.REPLACE_EXISTING); + + assertBusy(() -> { + var signatureAfter = signer.sign(testClusterAlias, "test", "test"); + assertNotNull(signatureAfter); + assertNotEquals(signatureAfter, signatureBefore); + }); + } finally { + updateClusterSettings( + Settings.builder() + .putNull(SIGNING_CERT_PATH.getConcreteSettingForNamespace(testClusterAlias).getKey()) + .putNull(SIGNING_KEY_PATH.getConcreteSettingForNamespace(testClusterAlias).getKey()) + .setSecureSettings(new MockSecureSettings()) + ); + } + } + + public void testRemoveFileWithConfig() throws Exception { + try { + final CrossClusterApiKeySigner signer = internalCluster().getInstance( + CrossClusterApiKeySigner.class, + internalCluster().getRandomNodeName() + ); + + assertNull(signer.sign("test_cluster", "a_header")); + Path tempDir = createTempDir(); + Path signingCert = tempDir.resolve("signing.crt"); + Files.copy(getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt"), signingCert); + Path signingKey = tempDir.resolve("signing.key"); + Files.copy(getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key"), signingKey); + + // Add the cluster + updateClusterSettings( + Settings.builder() + .put("cluster.remote.test_cluster.signing.certificate", signingCert) + .put("cluster.remote.test_cluster.signing.key", signingKey) + ); + + // Make sure a signature can be created + var signatureBefore = signer.sign("test_cluster", "test", "test"); + assertNotNull(signatureBefore); + + // This should just fail the update, not remove any actual configs + Files.delete(signingCert); + Files.delete(signingKey); + + var signatureAfter = signer.sign("test_cluster", "test", "test"); + assertNotNull(signatureAfter); + assertEquals(signatureAfter, signatureBefore); + } finally { + updateClusterSettings( + Settings.builder() + .putNull("cluster.remote.test_cluster.signing.certificate") + .putNull("cluster.remote.test_cluster.signing.key") + .setSecureSettings(new MockSecureSettings()) + ); + } + } + + private void addAndRemoveClusterConfigsRuntime( + Set clusterAliases, + Consumer clusterCreator, + Consumer clusterRemover + ) throws Exception { + final CrossClusterApiKeySigner signer = internalCluster().getInstance( + CrossClusterApiKeySigner.class, + internalCluster().getRandomNodeName() + ); + final String[] testHeaders = randomArray(5, String[]::new, () -> randomAlphanumericOfLength(randomInt(20))); + + try { + for (var clusterAlias : clusterAliases) { + // Try to create a signature for a remote cluster that doesn't exist + assertNull(signer.sign(clusterAlias, testHeaders)); + clusterCreator.accept(clusterAlias); + // Make sure a signature can be created + assertNotNull(signer.sign(clusterAlias, testHeaders)); + } + for (var clusterAlias : clusterAliases) { + clusterRemover.accept(clusterAlias); + // Make sure no signature was created + assertBusy(() -> assertNull(signer.sign(clusterAlias, testHeaders))); + } + } finally { + var builder = Settings.builder(); + for (var clusterAlias : clusterAliases) { + CrossClusterApiKeySignerSettings.getDynamicSettings().forEach(setting -> { + builder.putNull(setting.getConcreteSettingForNamespace(clusterAlias).getKey()); + }); + } + if (clusterAliases.isEmpty() == false) { + updateClusterSettings(builder.setSecureSettings(new MockSecureSettings())); + } + } + } + + private Set randomClusterAliases() { + return randomUnique(() -> randomAlphaOfLengthBetween(1, randomInt(20)), randomInt(5)); + } + + private void writeSecureSettingsToKeyStoreAndReload(Map entries) { + char[] keyStorePassword = randomAlphaOfLengthBetween(1, randomInt(20)).toCharArray(); + internalCluster().getInstances(Environment.class).forEach(environment -> { + final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create(); + entries.forEach(keyStoreWrapper::setString); + try { + keyStoreWrapper.save(environment.configDir(), keyStorePassword, false); + logger.info(keyStoreWrapper.toString()); + } catch (Exception e) { + fail(e.getMessage()); + } + }); + PlainActionFuture future = new PlainActionFuture<>(); + reloadSecureSettings(keyStorePassword, future); + future.actionGet(); + } + + private static void reloadSecureSettings(char[] password, ActionListener listener) { + final var request = new NodesReloadSecureSettingsRequest(new String[0]); + try { + request.setSecureStorePassword(new SecureString(password)); + clusterAdmin().execute(TransportNodesReloadSecureSettingsAction.TYPE, request, listener); + } finally { + request.decRef(); + } + } + + @Override + public boolean transportSSLEnabled() { + // Needs to be enabled to allow updates to secure settings + return true; + } +} 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 013a29a80f738..80a5c373dca30 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 @@ -417,6 +417,9 @@ import org.elasticsearch.xpack.security.support.ReloadableSecurityComponent; import org.elasticsearch.xpack.security.support.SecurityMigrations; import org.elasticsearch.xpack.security.support.SecuritySystemIndices; +import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigner; +import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerReloader; +import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings; import org.elasticsearch.xpack.security.transport.SecurityHttpSettings; import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; import org.elasticsearch.xpack.security.transport.filter.IPFilter; @@ -603,6 +606,8 @@ public class Security extends Plugin private final SetOnce tokenService = new SetOnce<>(); private final SetOnce securityActionFilter = new SetOnce<>(); private final SetOnce crossClusterAccessAuthcService = new SetOnce<>(); + private final SetOnce crossClusterApiKeySigner = new SetOnce<>(); + private final SetOnce crossClusterApiKeySignerReloader = new SetOnce<>(); private final SetOnce sharedGroupFactory = new SetOnce<>(); private final SetOnce dlsBitsetCache = new SetOnce<>(); private final SetOnce> bootstrapChecks = new SetOnce<>(); @@ -1164,6 +1169,16 @@ Collection createComponents( DestructiveOperations destructiveOperations = new DestructiveOperations(settings, clusterService.getClusterSettings()); crossClusterAccessAuthcService.set(new CrossClusterAccessAuthenticationService(clusterService, apiKeyService, authcService.get())); components.add(crossClusterAccessAuthcService.get()); + crossClusterApiKeySigner.set(new CrossClusterApiKeySigner(environment)); + components.add(crossClusterApiKeySigner.get()); + crossClusterApiKeySignerReloader.set( + new CrossClusterApiKeySignerReloader( + resourceWatcherService, + clusterService.getClusterSettings(), + crossClusterApiKeySigner.get() + ) + ); + components.add(crossClusterApiKeySignerReloader.get()); securityInterceptor.set( new SecurityServerTransportInterceptor( settings, @@ -1174,7 +1189,8 @@ Collection createComponents( securityContext.get(), destructiveOperations, crossClusterAccessAuthcService.get(), - getLicenseState() + getLicenseState(), + crossClusterApiKeySigner.get() ) ); @@ -1544,6 +1560,7 @@ public static List> getSettings(List securityExten settingsList.add(CachingServiceAccountTokenStore.CACHE_MAX_TOKENS_SETTING); settingsList.add(SimpleRole.CACHE_SIZE_SETTING); settingsList.add(NativeRoleMappingStore.LAST_LOAD_CACHE_ENABLED_SETTING); + settingsList.addAll(CrossClusterApiKeySignerSettings.getSettings()); // hide settings settingsList.add(Setting.stringListSetting(SecurityField.setting("hide_settings"), Property.NodeScope, Property.Filtered)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java new file mode 100644 index 0000000000000..d6fed2c720a07 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -0,0 +1,252 @@ +/* + * 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.transport; + +import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.settings.SecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.ssl.SslKeyConfig; +import org.elasticsearch.common.ssl.SslUtil; +import org.elasticsearch.core.Nullable; +import org.elasticsearch.env.Environment; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; +import org.elasticsearch.transport.RemoteClusterSettings; +import org.elasticsearch.xpack.core.ssl.CertParsingUtils; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import javax.net.ssl.X509KeyManager; + +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.KEYSTORE_ALIAS_SUFFIX; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SETTINGS_PART_SIGNING; + +public class CrossClusterApiKeySigner { + private final Logger logger = LogManager.getLogger(getClass()); + private final Environment environment; + private static final Map SIGNATURE_ALGORITHM_BY_TYPE = Map.of("RSA", "SHA256withRSA", "EC", "SHA256withECDSA"); + + private final Map signingConfigByClusterAlias = new ConcurrentHashMap<>(); + + public CrossClusterApiKeySigner(Environment environment) { + this.environment = environment; + loadSigningConfigs(); + } + + public Map> getDependentFilesToClusterAliases() { + return signingConfigByClusterAlias.entrySet() + .stream() + .filter(entry -> entry.getValue().dependentFiles != null) + .flatMap(entry -> entry.getValue().dependentFiles.stream().map(path -> Map.entry(path, entry.getKey()))) + .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toSet()))); + } + + public void loadSigningConfig(String clusterAlias, @Nullable Settings settings, boolean updateSecureSettings) { + signingConfigByClusterAlias.compute(clusterAlias, (key, currentSigningConfig) -> { + var effectiveSettings = buildEffectiveSettings( + currentSigningConfig != null ? currentSigningConfig.settings : null, + settings, + updateSecureSettings + ); + assert effectiveSettings != null : "Signing config settings must not be null"; + logger.trace("Loading signing config for [{}] with settings [{}]", clusterAlias, effectiveSettings); + + SigningConfig signingConfig = new SigningConfig(null, null, effectiveSettings); + if (effectiveSettings.getByPrefix(SETTINGS_PART_SIGNING).isEmpty() == false) { + try { + SslKeyConfig keyConfig = CertParsingUtils.createKeyConfig( + effectiveSettings, + SETTINGS_PART_SIGNING + ".", + environment, + false + ); + if (keyConfig.hasKeyMaterial()) { + String alias = effectiveSettings.get(SETTINGS_PART_SIGNING + "." + KEYSTORE_ALIAS_SUFFIX); + var keyPair = Strings.isNullOrEmpty(alias) ? buildKeyPair(keyConfig) : buildKeyPair(keyConfig, alias); + if (keyPair != null) { + logger.trace("Key pair [{}] found for [{}]", keyPair, clusterAlias); + signingConfig = new SigningConfig(keyPair, keyConfig.getDependentFiles(), effectiveSettings); + } + } else { + logger.error(Strings.format("No signing credentials found in signing config for cluster [%s]", clusterAlias)); + } + } catch (Exception e) { + // Since this can be called by the settings applier we don't want to surface an error here + logger.error(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); + } + } else { + logger.trace("No signing settings found for [{}]", clusterAlias); + } + return signingConfig; + }); + } + + public X509CertificateSignature sign(String clusterAlias, String... headers) { + SigningConfig signingConfig = signingConfigByClusterAlias.get(clusterAlias); + if (signingConfig == null || signingConfig.keyPair() == null) { + logger.trace("No signing config found for [{}] returning empty signature", clusterAlias); + return null; + } + var keyPair = signingConfig.keyPair(); + try { + String algorithm = keyPair.signatureAlgorithm(); + Signature signature = Signature.getInstance(algorithm); + signature.initSign(keyPair.privateKey); + signature.update(getSignableBytes(headers)); + final byte[] sigBytes = signature.sign(); + return new X509CertificateSignature(keyPair.certificate, algorithm, new BytesArray(sigBytes)); + } catch (GeneralSecurityException e) { + throw new ElasticsearchSecurityException( + Strings.format("Failed to sign cross cluster headers for cluster [%s]", clusterAlias), + e + ); + } + } + + private void loadSigningConfigs() { + this.environment.settings().getGroups(RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, true).forEach((alias, settings) -> { + loadSigningConfig(alias, settings, false); + }); + } + + /** + * Build the effective remote cluster settings by merging the currently configured (if any) and new/updated settings + *

+ * - If newSettings is null - use existing settings, used to refresh the dependent files + * - If updateSecureSettings is true - merge secure settings from newSettings with current settings, used by secure settings refresh + * - If updateSecureSettings is false - merge new settings with existing secure settings, used for regular settings update + */ + private Settings buildEffectiveSettings( + @Nullable Settings currentSettings, + @Nullable Settings newSettings, + boolean updateSecureSettings + ) { + if (currentSettings == null && newSettings == null) { + return Settings.EMPTY; + } + if (newSettings == null) { + return currentSettings; + } + if (currentSettings == null || newSettings.isEmpty()) { + return newSettings; + } + + Settings secureSettingsSource = updateSecureSettings ? newSettings : currentSettings; + Settings settingsSource = updateSecureSettings ? currentSettings : newSettings; + + SecureSettings secureSettings = Settings.builder().put(secureSettingsSource, true).getSecureSettings(); + return Settings.builder().put(settingsSource, false).setSecureSettings(secureSettings).build(); + } + + void reloadSigningConfigs(Set clusterAliases) { + clusterAliases.forEach(alias -> loadSigningConfig(alias, null, true)); + } + + private X509KeyPair buildKeyPair(SslKeyConfig keyConfig) { + final X509KeyManager keyManager = keyConfig.createKeyManager(); + if (keyManager == null) { + return null; + } + + final Set aliases = SIGNATURE_ALGORITHM_BY_TYPE.keySet() + .stream() + .map(keyType -> keyManager.getServerAliases(keyType, null)) + .filter(Objects::nonNull) + .flatMap(Arrays::stream) + .collect(Collectors.toSet()); + + logger.trace("KeyConfig [{}] has compatible entries: [{}]", keyConfig, aliases); + + return switch (aliases.size()) { + case 0 -> throw new IllegalStateException("Cannot find a signing key in [" + keyConfig + "]"); + case 1 -> { + final String aliasFromKeyStore = aliases.iterator().next(); + final X509Certificate[] chain = keyManager.getCertificateChain(aliasFromKeyStore); + yield new X509KeyPair(chain[0], keyManager.getPrivateKey(aliasFromKeyStore)); + } + default -> throw new IllegalStateException( + "The configured signing key store has multiple signing keys [" + + aliases + + "] but no alias has been specified in signing configuration." + ); + }; + } + + private X509KeyPair buildKeyPair(SslKeyConfig keyConfig, String alias) { + assert alias != null; + + final X509KeyManager keyManager = keyConfig.createKeyManager(); + if (keyManager == null) { + return null; + } + + final String keyType = keyManager.getPrivateKey(alias).getAlgorithm(); + if (SIGNATURE_ALGORITHM_BY_TYPE.containsKey(keyType) == false) { + throw new IllegalStateException( + Strings.format( + "The key associated with alias [%s] uses unsupported key algorithm type [%s], only %s is supported", + alias, + keyType, + SIGNATURE_ALGORITHM_BY_TYPE.keySet() + ) + ); + } + + final X509Certificate[] chain = keyManager.getCertificateChain(alias); + logger.trace("KeyConfig [{}] has entry for alias: [{}] [{}]", keyConfig, alias, chain != null); + + return chain != null ? new X509KeyPair(chain[0], keyManager.getPrivateKey(alias)) : null; + } + + private static byte[] getSignableBytes(final String... headers) { + return String.join("\n", headers).getBytes(StandardCharsets.UTF_8); + } + + private static String calculateFingerprint(X509Certificate certificate) { + try { + return SslUtil.calculateFingerprint(certificate, "SHA-1"); + } catch (CertificateEncodingException e) { + return ""; + } + } + + private record X509KeyPair(X509Certificate certificate, PrivateKey privateKey, String signatureAlgorithm, String fingerprint) { + X509KeyPair(X509Certificate certificate, PrivateKey privateKey) { + this( + Objects.requireNonNull(certificate), + Objects.requireNonNull(privateKey), + Optional.ofNullable(SIGNATURE_ALGORITHM_BY_TYPE.get(privateKey.getAlgorithm())) + .orElseThrow( + () -> new IllegalArgumentException( + "Unsupported Key Type [" + privateKey.getAlgorithm() + "] for [" + privateKey + "]" + ) + ), + calculateFingerprint(certificate) + ); + } + } + + private record SigningConfig(@Nullable X509KeyPair keyPair, @Nullable Collection dependentFiles, Settings settings) {} + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java new file mode 100644 index 0000000000000..adcd4deb671d0 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java @@ -0,0 +1,215 @@ +/* + * 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.transport; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.SecureSettings; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.Strings; +import org.elasticsearch.transport.RemoteClusterSettings; +import org.elasticsearch.watcher.FileChangesListener; +import org.elasticsearch.watcher.FileWatcher; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.watcher.ResourceWatcherService.Frequency; +import org.elasticsearch.xpack.security.support.ReloadableSecurityComponent; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; + +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.getDynamicSettings; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.getSecureSettings; + +/** + * Responsible for reloading a provided {@link CrossClusterApiKeySigner} when updates are received from the following sources: + * - Dynamic cluster settings + * - Reloadable secure settings + * - File changes in any of the files pointed to by the cluster settings + */ +public final class CrossClusterApiKeySignerReloader implements ReloadableSecurityComponent { + + private static final Logger logger = LogManager.getLogger(CrossClusterApiKeySignerReloader.class); + private final CrossClusterApiKeySigner apiKeySigner; + private final Map monitoredPathToChangeListener = new HashMap<>(); + + public CrossClusterApiKeySignerReloader( + ResourceWatcherService resourceWatcherService, + ClusterSettings clusterSettings, + CrossClusterApiKeySigner apiKeySigner + ) { + this.apiKeySigner = apiKeySigner; + clusterSettings.addAffixGroupUpdateConsumer(getDynamicSettings(), (key, val) -> { + apiKeySigner.loadSigningConfig(key, val.getByPrefix(RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX + key + "."), false); + logger.info("Updated signing configuration for [{}] due to updated cluster settings", key); + watchDependentFilesForClusterAliases( + apiKeySigner::reloadSigningConfigs, + resourceWatcherService, + apiKeySigner.getDependentFilesToClusterAliases() + ); + }); + + watchDependentFilesForClusterAliases( + apiKeySigner::reloadSigningConfigs, + resourceWatcherService, + apiKeySigner.getDependentFilesToClusterAliases() + ); + } + + private void watchDependentFilesForClusterAliases( + Consumer> reloadConsumer, + ResourceWatcherService resourceWatcherService, + Map> dependentFilesToClusterAliases + ) { + dependentFilesToClusterAliases.forEach((path, clusterAliases) -> { + var existingChangeListener = monitoredPathToChangeListener.get(path); + if (existingChangeListener != null) { + logger.trace("Found existing listener for file [{}], adding clusterAliases {}", path, clusterAliases); + existingChangeListener.addClusterAliases(clusterAliases); + return; + } + + logger.trace("Adding listener for file [{}] for clusters {}", path, clusterAliases); + + ChangeListener changeListener = new ChangeListener(clusterAliases, path, reloadConsumer); + FileWatcher fileWatcher = new FileWatcher(path); + fileWatcher.addListener(changeListener); + try { + resourceWatcherService.add(fileWatcher, Frequency.HIGH); + monitoredPathToChangeListener.put(path, changeListener); + } catch (IOException | SecurityException e) { + logger.error(Strings.format("failed to start watching file [%s]", path), e); + } + }); + } + + private record ChangeListener(Set clusterAliases, Path file, Consumer> reloadConsumer) + implements + FileChangesListener { + + public void addClusterAliases(Set clusterAliases) { + this.clusterAliases.addAll(clusterAliases); + } + + @Override + public void onFileCreated(Path file) { + onFileChanged(file); + } + + @Override + public void onFileDeleted(Path file) { + onFileChanged(file); + } + + @Override + public void onFileChanged(Path file) { + if (this.file.equals(file)) { + reloadConsumer.accept(this.clusterAliases); + logger.info("Updated signing configuration for [{}] config(s) due to update of file [{}]", clusterAliases.size(), file); + } + } + } + + @Override + public void reload(Settings settings) { + try { + // The secure settings provided to reload are only available in the scope of this method call since after that the keystore is + // closed. Since the secure settings will potentially be used later when the signing config is used to sign headers, the + // settings need to be retrieved from the keystore and cached + Settings cachedSettings = Settings.builder().setSecureSettings(extractSecureSettings(settings, getSecureSettings())).build(); + cachedSettings.getGroups(RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, true) + .forEach((clusterAlias, settingsForCluster) -> { + // Only update signing config if settings were found, since empty config means config deletion + if (settingsForCluster.isEmpty() == false) { + apiKeySigner.loadSigningConfig(clusterAlias, settingsForCluster, true); + logger.info("Updated signing configuration for [{}] due to reload of secure settings", clusterAlias); + } + }); + } catch (GeneralSecurityException e) { + logger.error("Keystore exception while reloading signing configuration after reload of secure settings", e); + } + } + + /** + * Extracts the {@link SecureSettings}` out of the passed in {@link Settings} object. The {@code Setting} argument has to have the + * {@code SecureSettings} open/available. Normally {@code SecureSettings} are available only under specific callstacks (eg. during node + * initialization or during a `reload` call). The returned copy can be reused freely as it will never be closed (this is a bit of + * cheating, but it is necessary in this specific circumstance). Only works for secure settings of type string (not file). + * + * @param source A {@code Settings} object with its {@code SecureSettings} open/available. + * @param settingsToCopy The list of settings to copy. + * @return A copy of the {@code SecureSettings} of the passed in {@code Settings} argument. + */ + private static SecureSettings extractSecureSettings(Settings source, List> settingsToCopy) + throws GeneralSecurityException { + final SecureSettings sourceSecureSettings = Settings.builder().put(source, true).getSecureSettings(); + final Map copiedSettings = new HashMap<>(); + + if (sourceSecureSettings != null && settingsToCopy != null) { + for (final String settingKey : sourceSecureSettings.getSettingNames()) { + for (final Setting secureSetting : settingsToCopy) { + if (secureSetting.match(settingKey)) { + copiedSettings.put( + settingKey, + new SecureSettingValue( + sourceSecureSettings.getString(settingKey), + sourceSecureSettings.getSHA256Digest(settingKey) + ) + ); + } + } + } + } + return new SecureSettings() { + @Override + public boolean isLoaded() { + return true; + } + + @Override + public SecureString getString(String setting) { + return copiedSettings.get(setting).value(); + } + + @Override + public Set getSettingNames() { + return copiedSettings.keySet(); + } + + @Override + public InputStream getFile(String setting) { + throw new UnsupportedOperationException("A cached SecureSetting cannot be a file"); + } + + @Override + public byte[] getSHA256Digest(String setting) { + return copiedSettings.get(setting).sha256Digest(); + } + + @Override + public void close() throws IOException {} + + @Override + public void writeTo(StreamOutput out) throws IOException { + throw new UnsupportedOperationException("A cached SecureSetting cannot be serialized"); + } + }; + } + + private record SecureSettingValue(SecureString value, byte[] sha256Digest) {} + +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java new file mode 100644 index 0000000000000..dc4a82679dedf --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java @@ -0,0 +1,106 @@ +/* + * 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.transport; + +import org.elasticsearch.common.settings.SecureSetting; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Setting; +import org.elasticsearch.common.ssl.SslConfigurationKeys; +import org.elasticsearch.common.util.CollectionUtils; +import org.elasticsearch.transport.RemoteClusterSettings; + +import java.util.List; + +import javax.net.ssl.KeyManagerFactory; + +public class CrossClusterApiKeySignerSettings { + static final String SETTINGS_PART_SIGNING = "signing"; + + static final String KEYSTORE_ALIAS_SUFFIX = "keystore.alias"; + + static final Setting.AffixSetting SIGNING_KEYSTORE_ALIAS = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + KEYSTORE_ALIAS_SUFFIX, + key -> Setting.simpleString(key, newKey -> { + + }, Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) + ); + + static final Setting.AffixSetting SIGNING_KEYSTORE_PATH = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_PATH, + key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) + ); + + static final Setting.AffixSetting SIGNING_KEYSTORE_SECURE_PASSWORD = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_SECURE_PASSWORD, + key -> SecureSetting.secureString(key, null) + ); + + static final Setting.AffixSetting SIGNING_KEYSTORE_ALGORITHM = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_ALGORITHM, + key -> Setting.simpleString( + key, + KeyManagerFactory.getDefaultAlgorithm(), + Setting.Property.NodeScope, + Setting.Property.Filtered, + Setting.Property.Dynamic + ) + ); + + static final Setting.AffixSetting SIGNING_KEYSTORE_TYPE = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_TYPE, + key -> Setting.simpleString(key, "", Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) + ); + + static final Setting.AffixSetting SIGNING_KEYSTORE_SECURE_KEY_PASSWORD = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_SECURE_KEY_PASSWORD, + key -> SecureSetting.secureString(key, null) + ); + + static final Setting.AffixSetting SIGNING_KEY_PATH = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEY, + key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) + ); + + static final Setting.AffixSetting SIGNING_KEY_SECURE_PASSPHRASE = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEY_SECURE_PASSPHRASE, + key -> SecureSetting.secureString(key, null) + ); + + static final Setting.AffixSetting SIGNING_CERT_PATH = Setting.affixKeySetting( + RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.CERTIFICATE, + key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) + ); + + public static List> getDynamicSettings() { + return List.of( + SIGNING_KEYSTORE_ALIAS, + SIGNING_KEYSTORE_PATH, + SIGNING_KEYSTORE_ALGORITHM, + SIGNING_KEYSTORE_TYPE, + SIGNING_KEY_PATH, + SIGNING_CERT_PATH + ); + } + + public static List> getSecureSettings() { + return List.of(SIGNING_KEYSTORE_SECURE_PASSWORD, SIGNING_KEYSTORE_SECURE_KEY_PASSWORD, SIGNING_KEY_SECURE_PASSPHRASE); + } + + public static List> getSettings() { + return CollectionUtils.concatLists(getDynamicSettings(), getSecureSettings()); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index fb56a9f46cc4b..133b3417c27d1 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -100,6 +100,7 @@ public class SecurityServerTransportInterceptor implements TransportInterceptor private final CrossClusterAccessAuthenticationService crossClusterAccessAuthcService; private final Function> remoteClusterCredentialsResolver; private final XPackLicenseState licenseState; + private final CrossClusterApiKeySigner crossClusterApiKeySigner; public SecurityServerTransportInterceptor( Settings settings, @@ -110,7 +111,8 @@ public SecurityServerTransportInterceptor( SecurityContext securityContext, DestructiveOperations destructiveOperations, CrossClusterAccessAuthenticationService crossClusterAccessAuthcService, - XPackLicenseState licenseState + XPackLicenseState licenseState, + CrossClusterApiKeySigner crossClusterApiKeySigner ) { this( settings, @@ -122,6 +124,7 @@ public SecurityServerTransportInterceptor( destructiveOperations, crossClusterAccessAuthcService, licenseState, + crossClusterApiKeySigner, RemoteConnectionManager::resolveRemoteClusterAliasWithCredentials ); } @@ -136,6 +139,7 @@ public SecurityServerTransportInterceptor( DestructiveOperations destructiveOperations, CrossClusterAccessAuthenticationService crossClusterAccessAuthcService, XPackLicenseState licenseState, + CrossClusterApiKeySigner crossClusterApiKeySigner, // Inject for simplified testing Function> remoteClusterCredentialsResolver ) { @@ -147,6 +151,7 @@ public SecurityServerTransportInterceptor( this.securityContext = securityContext; this.crossClusterAccessAuthcService = crossClusterAccessAuthcService; this.licenseState = licenseState; + this.crossClusterApiKeySigner = crossClusterApiKeySigner; this.remoteClusterCredentialsResolver = remoteClusterCredentialsResolver; this.profileFilters = initializeProfileFilters(destructiveOperations); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java new file mode 100644 index 0000000000000..91e99f9c3a204 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/X509CertificateSignature.java @@ -0,0 +1,169 @@ +/* + * 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.transport; + +import org.elasticsearch.TransportVersion; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.ssl.SslUtil; +import org.elasticsearch.core.CheckedConsumer; +import org.elasticsearch.core.CheckedFunction; +import org.elasticsearch.logging.LogManager; +import org.elasticsearch.logging.Logger; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Objects; + +public final class X509CertificateSignature implements Writeable { + + private static final Logger logger = LogManager.getLogger(X509CertificateSignature.class); + + private final X509Certificate certificate; + private final String algorithm; + private final BytesReference signature; + + public X509CertificateSignature(X509Certificate certificate, String algorithm, BytesReference signature) { + this.certificate = Objects.requireNonNull(certificate); + this.algorithm = Objects.requireNonNull(algorithm); + this.signature = Objects.requireNonNull(signature); + } + + public X509CertificateSignature(StreamInput in) throws IOException { + final byte[] certBytes = in.readByteArray(); + if (certBytes == null || certBytes.length == 0) { + throw new IOException("Certificate bytes cannot be empty"); + } + try (var bais = new ByteArrayInputStream(certBytes)) { + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + final Certificate cert = certFactory.generateCertificate(bais); + if (cert instanceof X509Certificate x509) { + this.certificate = x509; + } else { + throw new IOException("Input bytes are not an X509 certificate [" + cert.getClass() + "] [" + cert + "]"); + } + } catch (CertificateException e) { + throw new IOException("Cannot read certificate", e); + } + this.algorithm = in.readString(); + this.signature = in.readBytesReference(); + } + + public X509Certificate certificate() { + return certificate; + } + + public String algorithm() { + return algorithm; + } + + public BytesReference signature() { + return signature; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (X509CertificateSignature) obj; + return Objects.equals(this.certificate, that.certificate) + && Objects.equals(this.algorithm, that.algorithm) + && Objects.equals(this.signature, that.signature); + } + + @Override + public int hashCode() { + return Objects.hash(certificate, algorithm, signature); + } + + @Override + public String toString() { + return "X509CertificateSignature[" + + "certificate=(" + + certificate.getSubjectX500Principal() + + ";" + + certificate.getType() + + ";" + + fingerprint() + + "), " + + "algorithm=" + + algorithm + + ", " + + "signature=" + + signature + + ']'; + } + + private String fingerprint() { + try { + return "SHA1:" + SslUtil.calculateFingerprint(this.certificate, "SHA-1"); + } catch (CertificateEncodingException e) { + return ""; + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + try { + final byte[] encoded = certificate.getEncoded(); + out.writeByteArray(encoded); + } catch (CertificateEncodingException e) { + throw new IOException("Cannot convert certificate for " + certificate.getSubjectX500Principal() + " to bytes", e); + } + out.writeString(algorithm); + out.writeBytesReference(signature); + } + + public String encodeToString() throws IOException { + final String encoded = encode(this); + logger.trace("Encoding {} as [{}]", this, encoded); + return encoded; + } + + public static X509CertificateSignature decode(String encoded) throws IOException { + logger.trace("Decoding [{}]", encoded); + try { + return decode(encoded, X509CertificateSignature::new); + } catch (IOException e) { + logger.debug("Failed to decode signature", e); + throw e; + } + } + + public static String encode(Writeable writeable) throws IOException { + return encode(TransportVersion.current(), writeable::writeTo); + } + + public static String encode(TransportVersion transportVersion, CheckedConsumer body) throws IOException { + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.setTransportVersion(transportVersion); + TransportVersion.writeVersion(transportVersion, out); + body.accept(out); + out.flush(); + return Base64.getEncoder().encodeToString(BytesReference.toBytes(out.bytes())); + } + } + + public static T decode(String encoded, CheckedFunction body) throws IOException { + Objects.requireNonNull(encoded); + final byte[] bytes = Base64.getDecoder().decode(encoded); + final StreamInput in = StreamInput.wrap(bytes); + final TransportVersion transportVersion = TransportVersion.readVersion(in); + in.setTransportVersion(transportVersion); + return body.apply(in); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/signature/X509CertificateSignatureTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/signature/X509CertificateSignatureTests.java new file mode 100644 index 0000000000000..7f2a69416a9ef --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/signature/X509CertificateSignatureTests.java @@ -0,0 +1,40 @@ +/* + * 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.signature; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.ssl.PemUtils; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.security.transport.X509CertificateSignature; +import org.hamcrest.Matchers; + +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; + +public class X509CertificateSignatureTests extends ESTestCase { + + public void testEncodeDecode() throws Exception { + final List certificates = PemUtils.readCertificates(List.of(getResourceDataPath(getClass(), "signing_rsa.crt"))); + assertThat(certificates, hasSize(1)); + final BytesReference bytes = randomBytesReference(randomIntBetween(8, 50)); + final X509CertificateSignature original = new X509CertificateSignature( + (X509Certificate) certificates.get(0), + "SHA256withRSA", + bytes + ); + + final String encoded = original.encodeToString(); + final X509CertificateSignature decoded = X509CertificateSignature.decode(encoded); + + assertThat(decoded, Matchers.equalTo(original)); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloaderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloaderTests.java new file mode 100644 index 0000000000000..e1880f5d9a1ec --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloaderTests.java @@ -0,0 +1,157 @@ +/* + * 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.transport; + +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.junit.After; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CrossClusterApiKeySignerReloaderTests extends ESTestCase { + private CrossClusterApiKeySigner crossClusterApiKeySigner; + private ResourceWatcherService resourceWatcherService; + private ThreadPool threadPool; + + @Override + public void setUp() throws Exception { + super.setUp(); + crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); + Settings settings = Settings.builder().put("resource.reload.interval.high", TimeValue.timeValueMillis(100)).build(); + threadPool = new TestThreadPool(getTestName()); + resourceWatcherService = new ResourceWatcherService(settings, threadPool); + } + + public void testSimpleDynamicSettingsUpdate() throws IOException { + Settings settings = Settings.builder().put("cluster.remote.my_remote.signing.keystore.alias", "mykey").build(); + when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of()); + var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + + new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); + clusterSettings.applySettings(Settings.builder().put("cluster.remote.my_remote.signing.keystore.alias", "anotherkey").build()); + verify(crossClusterApiKeySigner).loadSigningConfig( + "my_remote", + Settings.builder() + .put("cluster.remote.my_remote.signing.keystore.alias", "anotherkey") + .build() + .getByPrefix("cluster.remote.my_remote."), + false + ); + verify(crossClusterApiKeySigner, times(2)).getDependentFilesToClusterAliases(); + } + + public void testDynamicSettingsUpdateWithAddedFile() throws Exception { + var fileToMonitor = createTempFile(); + Settings settings = Settings.builder().put("cluster.remote.my_remote.signing.keystore.alias", "mykey").build(); + when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of()) + .thenReturn(Map.of(fileToMonitor, Set.of("my_remote"))); + + var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); + + clusterSettings.applySettings( + Settings.builder() + .put("cluster.remote.my_remote.signing.keystore.alias", "mykey") + .put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor) + .build() + ); + + verify(crossClusterApiKeySigner).loadSigningConfig( + "my_remote", + Settings.builder() + .put("cluster.remote.my_remote.signing.keystore.alias", "mykey") + .put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor) + .build() + .getByPrefix("cluster.remote.my_remote."), + false + ); + verify(crossClusterApiKeySigner, times(2)).getDependentFilesToClusterAliases(); + verify(crossClusterApiKeySigner, times(0)).reloadSigningConfigs(Set.of("my_remote")); + Files.writeString(fileToMonitor, "some content"); + assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).reloadSigningConfigs(Set.of("my_remote"))); + } + + public void testSimpleSecureSettingsReload() { + when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of()); + var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + var reloader = new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); + + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secret"); + Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); + reloader.reload(settings); + + verify(crossClusterApiKeySigner).loadSigningConfig("my_remote", settings.getByPrefix("cluster.remote.my_remote."), true); + verify(crossClusterApiKeySigner, times(1)).getDependentFilesToClusterAliases(); + } + + public void testSecureSettingsReloadNoMatchingSecureSettings() { + when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of()); + var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + var reloader = new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); + + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("not.a.setting", "secret"); + Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); + reloader.reload(settings); + + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(any(), any(), anyBoolean()); + verify(crossClusterApiKeySigner, times(1)).getDependentFilesToClusterAliases(); + } + + public void testFileUpdatedReloaded() throws Exception { + var fileToMonitor = createTempFile(); + Settings settings = Settings.builder().put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build(); + when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of(fileToMonitor, Set.of("my_remote"))); + + var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); + + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any(), anyBoolean()); + verify(crossClusterApiKeySigner, times(1)).getDependentFilesToClusterAliases(); + Files.writeString(fileToMonitor, "some content"); + assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).reloadSigningConfigs(Set.of("my_remote"))); + } + + public void testFileDeletedReloaded() throws Exception { + var fileToMonitor = createTempFile(); + Settings settings = Settings.builder().put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build(); + when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of(fileToMonitor, Set.of("my_remote"))); + + var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); + + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any(), anyBoolean()); + verify(crossClusterApiKeySigner, times(1)).getDependentFilesToClusterAliases(); + Files.delete(fileToMonitor); + assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).reloadSigningConfigs(Set.of("my_remote"))); + } + + @After + public void tearDownThreadPool() { + terminate(threadPool); + } + +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java new file mode 100644 index 0000000000000..ccb546080af1a --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java @@ -0,0 +1,104 @@ +/* + * 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.transport; + +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.node.Node; +import org.elasticsearch.test.ESTestCase; + +import java.util.Map; +import java.util.Set; + +public class CrossClusterApiKeySignerTests extends ESTestCase { + + @Override + public void setUp() throws Exception { + super.setUp(); + } + + public void testLoadKeystore() { + var builder = Settings.builder() + .put("cluster.remote.my_remote.signing.keystore.alias", "wholelottakey") + .put("cluster.remote.my_remote.signing.keystore.path", getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks")) + .put("path.home", createTempDir()) + .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secretpassword"); + builder.setSecureSettings(secureSettings); + var signer = new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())); + + assertNotNull(signer.sign("my_remote", "a_header")); + } + + public void testLoadKeystoreMissingFile() { + var builder = Settings.builder() + .put("cluster.remote.my_remote.signing.keystore.alias", "wholelottakey") + .put("cluster.remote.my_remote.signing.keystore.path", "not_a_valid_path") + .put("path.home", createTempDir()) + .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secretpassword"); + builder.setSecureSettings(secureSettings); + var signer = new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())); + + assertNull(signer.sign("my_remote", "a_header")); + } + + public void testLoadSeveralAliasesWithoutAliasSettingKeystore() { + var builder = Settings.builder() + .put("cluster.remote.my_remote.signing.keystore.path", getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks")) + .put("path.home", createTempDir()) + .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secretpassword"); + builder.setSecureSettings(secureSettings); + var signer = new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())); + + assertNull(signer.sign("my_remote", "a_header")); + } + + public void testGetDependentFilesToClusterAliases() { + var builder = Settings.builder() + .put("cluster.remote.my_remote1.signing.keystore.alias", "wholelottakey") + .put("cluster.remote.my_remote1.signing.keystore.path", getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks")) + .put("cluster.remote.my_remote2.signing.keystore.alias", "wholelottakey") + .put("cluster.remote.my_remote2.signing.keystore.path", getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks")) + .put( + "cluster.remote.my_remote3.signing.certificate", + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt") + ) + .put("cluster.remote.my_remote3.signing.key", getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key")) + .put( + "cluster.remote.my_remote4.signing.certificate", + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt") + ) + .put("cluster.remote.my_remote4.signing.key", getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key")) + .put("path.home", createTempDir()) + .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("cluster.remote.my_remote1.signing.keystore.secure_password", "secretpassword"); + secureSettings.setString("cluster.remote.my_remote2.signing.keystore.secure_password", "secretpassword"); + builder.setSecureSettings(secureSettings); + var signer = new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())); + assertNotNull(signer.sign(randomFrom("my_remote1", "my_remote2", "my_remote3", "my_remote4"), "a_header")); + + assertEquals( + Map.of( + getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks"), + Set.of("my_remote1", "my_remote2"), + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt"), + Set.of("my_remote3", "my_remote4"), + getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key"), + Set.of("my_remote3", "my_remote4") + ), + signer.getDependentFilesToClusterAliases() + ); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java index 655e4eba4a179..095498f1adac5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java @@ -120,6 +120,7 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { private SecurityContext securityContext; private ClusterService clusterService; private MockLicenseState mockLicenseState; + private CrossClusterApiKeySigner crossClusterApiKeySigner; @Override public void setUp() throws Exception { @@ -130,6 +131,7 @@ public void setUp() throws Exception { threadContext = threadPool.getThreadContext(); securityContext = spy(new SecurityContext(settings, threadPool.getThreadContext())); mockLicenseState = MockLicenseState.createMock(); + crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); Mockito.when(mockLicenseState.isAllowed(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE)).thenReturn(true); } @@ -158,7 +160,8 @@ public void testSendAsync() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + mockLicenseState, + crossClusterApiKeySigner ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -209,7 +212,8 @@ public void testSendAsyncSwitchToSystem() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + mockLicenseState, + crossClusterApiKeySigner ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -253,7 +257,8 @@ public void testSendWithoutUser() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + mockLicenseState, + crossClusterApiKeySigner ) { @Override void assertNoAuthentication(String action) {} @@ -315,7 +320,8 @@ public void testSendToNewerVersionSetsCorrectVersion() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + mockLicenseState, + crossClusterApiKeySigner ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -383,7 +389,8 @@ public void testSendToOlderVersionSetsCorrectVersion() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + mockLicenseState, + crossClusterApiKeySigner ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -449,7 +456,8 @@ public void testSetUserBasedOnActionOrigin() { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + mockLicenseState, + crossClusterApiKeySigner ); final AtomicBoolean calledWrappedSender = new AtomicBoolean(false); @@ -617,6 +625,7 @@ public void testSendWithCrossClusterAccessHeadersWithUnsupportedLicense() throws ), mock(CrossClusterAccessAuthenticationService.class), unsupportedLicenseState, + crossClusterApiKeySigner, mockRemoteClusterCredentialsResolver(remoteClusterAlias) ); @@ -754,6 +763,7 @@ private void doTestSendWithCrossClusterAccessHeaders( ), mock(CrossClusterAccessAuthenticationService.class), mockLicenseState, + crossClusterApiKeySigner, ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) ); @@ -892,6 +902,7 @@ public void testSendWithUserIfCrossClusterAccessHeadersConditionNotMet() throws ), mock(CrossClusterAccessAuthenticationService.class), mockLicenseState, + crossClusterApiKeySigner, ignored -> notRemoteConnection ? Optional.empty() : (finalNoCredential @@ -951,6 +962,7 @@ public void testSendWithCrossClusterAccessHeadersThrowsOnOldConnection() throws ), mock(CrossClusterAccessAuthenticationService.class), mockLicenseState, + crossClusterApiKeySigner, ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) ); @@ -1050,6 +1062,7 @@ public void testSendRemoteRequestFailsIfUserHasNoRemoteIndicesPrivileges() throw ), mock(CrossClusterAccessAuthenticationService.class), mockLicenseState, + crossClusterApiKeySigner, ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) ); @@ -1159,7 +1172,8 @@ public void testProfileFiltersCreatedDifferentlyForDifferentTransportAndRemoteCl new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + mockLicenseState, + crossClusterApiKeySigner ); final Map profileFilters = securityServerTransportInterceptor.getProfileFilters(); @@ -1217,7 +1231,8 @@ public void testNoProfileFilterForRemoteClusterWhenTheFeatureIsDisabled() { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState + mockLicenseState, + crossClusterApiKeySigner ); final Map profileFilters = securityServerTransportInterceptor.getProfileFilters(); diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing.jks b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing.jks new file mode 100644 index 0000000000000000000000000000000000000000..a15da99f27dd27cc62771e9d7cc5046de8e8ef51 GIT binary patch literal 5237 zcmbW5Wl$VSmxgDCLBik+B)A0EnTxwiAV45^Ah;9UJp;jAhX6r>dw?LpB>@JP;2KM1vuq6c|s~LKQaVav4f%W zpbgbnj{Ce3qDq7Mqp5c{B=gE35HkS?NDITk!1}L=peN`67!}47<4{GQ1sWL01}0cg z)XSXl5MxztxvMK{Bt8Noz@r-6>*I_}Z$LRbzoTQ64YG^j4yGv9u?Z5%(hA%#eKPjz zuko5LXve>QRDoeoUd+~@1dNUB z#I3C?x_QrdlAnAVCtG_?JzwMHJ7e{U|1bJH&zA&W3_sTUF%aHvq=yyioqpfe#=GU1 zNy)((|HLF;?|OC?2|{^yYNep30Nmz3SDO-ruA=c={VAC%Y20{yCZ}w$U)blM#4h}O zx4kat>*uz&lqIl|ReEVJd@^(>4ku{$l2fQ6I;Hvj2&b`3d%?DeiN_eX^ercRySEMP z6F}SgQhu+t7to<4q6X|Q1pbKpeo4ggj=ODHqTC*Ydo;_!z^z)f46A`{1Q!#ZPiV|C znv0uQLYQ_PG2hY2PF%cD&L?;`O;BOQN3dBBH7;kdr63CnG^^1U4h>K0oX`Y6&B^7o zYFf88ZZZ3bC=Cp!J)>G8)Ef{IJc*K5p-D(VT?B=G_BLZyuq(&ClHxdd-Awm>_Z<-L z;%r3D2u|7%w-PUfi8h~jg71P~foEJgt`hvBa*Dk8H|UVG_NHR0_t>LFC%J%1WxhOJ z%o&%^vOW;&*#jZpgJW9rxL$~!gP2}ZN4ffJ*)BUJUmx}QP&`+M=N+J@wT|9rzqXSdfmwy`f1FqoF%;rRqdTf?TAtFr4KXY(r&OgkXp_o?@mTNt za1z7G;s+3iG>=;5(pcaMa4Nt4HTL?9+Ps(L*j-3PiIaSlh$OA928}6rur{QzbK6@J zdCVdgbW3F%kjPe$Gy*+#Np6VAD^ukQr-`pLAf5;uks8K}g;N)09*r|JnCwjn^*Lvc zQ!V&@p(6*uT>ChzKUf4I{7C{HglksbjD)_k`y>kRJxjN*)5gJw_lk1D=rN}#5N)|n zggYIdgvAUvWdQN!%JHJr5)aC!H{?^lI-Fb)po*9)NwJzaz#`b{Tz6c$CVNBW>>&DI zB%~5}}YYv%_=2l+i+@SdqLD_wi_@rToqXB%@lI^HnlI zJ%{~#0e|Z^-OR3bZ!!rD9uo>%3kgzA&X>ap-;51Pd(y~AMfmz!M0*S{yuR2OrH@C( z0T;}?wR~MX-Q-Defkc51Oo7;uw!eOF=h|;-ArtBQwJ+G|zu=)G|JZhsmjlJ;ys-K> zvI&6K4Bx9Oj0CY^MwnxH)eSk~QGWII)$D4Ji4+z#&~5)^G^Vd39P=q0hh|IYqtkQ2 ze3QnN6m0yP>>Jdx^!!5IP)0JdVA|c-$kwr~Nc*C$UZJC2XUjFGthF1#>E*;0-s5tPKT=EAX792 z?|q4taVk4nnc^BScP_n#8Zr36g0v%1Q+f@p@uGO+oLzZUb>02z(|5g!>BC{f9Au|; zMT!NVE17pzd+N8xDwpEE&ALV1;`r}1Z>#$l&U)qZSglARgY5m@&oopzbkZ2*2ZxFK zDG9e>JL-L^kA9mVZsqup;XWBv$2!GmRE6BHUgOJpb6t8Rqw0Tk`0W(keNnzC4X$e!!E5nTNE>&;sIv z#-OpRn#9k*{Su*t9E9QLmk#+VpD>G}x*#u-XDRcI!8asOm1iWY!Qup&*2;s`N;HAY zdftEVCDWFv;CRz=rV~BL9wQ3|DL^Lo!-H|*MVEh%3_vC zJYC$tw+fLFa02L}vlo`c2Nje)GKaNgy8aEW#yDCa&NC-G0l}rHZS@}^OXM_;8V9uA z)ay)`g?$Ge?5~QoQxP}8H=aNJGrgr*<>oGuB4$yQ{;MWs7hjk_g$`Ab-03Y2Cc`&}Iqh^IJ*c}qwp5gSX6}s$0U0ZF? z9rLrNbC&KAs4cb5iI%Tt+Zgwyq`&^vfuJY zy8hd=dvyGq3>lxPNWC<@T257SO!pPs*~VA=Z!H)C%k?o95EKG!(1rJ3Ui~@CKV^vjJgGqlUA!a|rni2e*qNYs7qK?i)^6Qs&KTdHTOF-VesBQ!|m%<;F_*k&LV{QGk^%hB-`@ z;^+ia47etCuL3Y!irHPR{&2DQ^bs7~(i{C;@xiYgoD>t`P*Q!`a67V^19`e(0UM$a zjy{>=-HCOv#~gc3si5A6(hyirb})=y8I0ES`!ul{=eo$fj3J!#!fB9Ji7)HvGhdg` z-24dLqHpJMa3_|%;Ds_sn)T>cf|WeHg3d_GMfsoPVzAhNcZ3PNnBksi$``4KLhOhy zerFJ?5tM7r+g#cB{+rs`flIGs462)L4p>`6L)z8rIP17&WxtHT5}&aHp{PtB6xdn{8=E zbW^=GEN%Dun^zd3Zlj|sY774LNfUmgo~QT@hDRjZ037}eXZuGy|4W%Z;@bVu`%hQ{T>nR0^9sQJ;(@~uSP6em#>B`$U?Co@ z01y!HczFI(0se0pTQ@ZRc<_K2zu7ba1w;OT^oTK~{9o|xju>i3jTD*mkD6w}#)XyUy7hxF^L(>(2rlVao^`3LDfKYMrQ*tEb+UcU@z zZ$>0{s@hE$5>H_kssHlbKA}h)(<16%CPczEfu|-BPuy!>ZsK#!^zcPry| zrwBq*lSYVniTg7D zc2-+)Dp^jVTk@o-+=RT0hICLMkYizYV9{k!lPcj4yxa2-2Kj|WQ|Gg`wSd{#PRZ>$ zSX(ahK`if<0>M-~RNMJPBmw&}Gp3R4x=GkN>ixKAs=A(tA9~j~bQ&G+ZgIoi)w$d2 z+mvS{(f)GN)y&&>s<=l#KJ^O3E5M6u*>#LEt2&%1=tnG4vE00@#5eDaA`elkSkVVV zX8qz~O(y550iH3|l=kqZMqkY7w??dVXoaCT( zi(w6cY>Ea%zEV;)3agt<74B54_;ytEQZ`?wruPX2+h7mx#|s|2s9Kj1eT&O2wq2|c z&A7Sc{*9_m;+RXDM?MeSDMXr2>jminV7Sq1uR zGQ8t&3~=911@vJxW^2nz<-RF_Z5%7$ZFR>@-cZBR6j|0{xqOyBcgLqon{@P4xFOC& z#s-|uN&6(PL<9C&DZtyqiIwH~h{5^q5K0!kL%PKcflV`LMQp#`1kW$QC4}w}gt52g z6^57UZ`?2t{4J9Q!*m?JjUopRIg|gK7{Yv}ZiGd;Vh$}?=J;-3;9%T&Rq|O$Yi)Ce zPTl4S?!qp8-%VMub^|bY@%2yeKGu(WmEOvPGiiBOy4bu33H>%b?mU{*`!CJ#vZt5S%r-SIpHg%ra$T+>~;;T zqZ=gQY!l!5w(r}0;G~Fuh2PT#4CXKLiTW#;9=Wyie1N8_qCSn&Qyv~5`F;~=EQk=< zM3UlepO&ZNwmN-HWKWVd!FUs@AL37s$)7@0(nI3>g9W=@Ngebyx!;Z7#*QoM$rx7A%aWi+3=d7Bq@{fjzyiS7MjlR^}}s6q|Q(yn4J(`vSiTohn4?69>K4 zRh||DxdV)lyV!5&bH<@r?Q{9uS6C6XrEXG32?zM3j2qzjugB82NL43KS7Y39inNRzD#)9+tubn(vtP_*i@(d}9-j{M&#mlnyZ!jvQF3!2 z_@Y(|qkl2`c66fe=(>D+5aeFYZ+mnH*G)QYNJ_^T44|d?dA@NT`smrsZ4#dj4VQ za=ijIYM6f)YFxp`6#TjnTFa1KxDZn3Ii6AK8f&Ibq2dW)u!|iY#*7aw;aV1TKK44e z1g0pbdaIk#q`|MyD(*SCRHXILLY)d|dsZt%xMw2=6ZtydVAcnb#HsA*AoJAVl0asb%dRUC)^p zQeuu~UTg+k*eAa#QE@Lp=YDt3!#V1!wj<>`SkgJaqt&1y0=7b3IbXVv4frr}(6WA@ z&mYp{>kAieP6(UC%9;%p)LuxKoJ7F1<&ZirXbck^#L~{Qoi2V^BJP6>?yfr9~Q*MWvb7Du7+3Hq=KAQt=+-ILuwXW`wE0;4s|3zv4hZbO4xkFzqKd{w*ewIfYF~ xF6><9@?`8Mh`;}2*V-=L+N&cU3~k=*oQf(jpch%h924gvYS!S@!v9wU`44mxrr-bo literal 0 HcmV?d00001 diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_ec.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_ec.crt new file mode 100644 index 0000000000000..102eb8278ec71 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_ec.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBjDCCATOgAwIBAgIULGYxJUdnvCzcwcsh4em8hSPD0lYwCgYIKoZIzj0EAwIw +GzEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNvbTAgFw0yNTA5MDQxMTUyMjhaGA8y +Mjk5MDYxOTExNTIyOFowGzEZMBcGA1UEAwwQdGVzdC5leGFtcGxlLmNvbTBZMBMG +ByqGSM49AgEGCCqGSM49AwEHA0IABKpa9uHCqE4bs4pCqtlUt0mH2fR2uoqYpWhb +BQXW6VbqZEZIgWg1N8fw8EhREHkF++gdoRv4hhms8A5Ph1hVHfqjUzBRMB0GA1Ud +DgQWBBSiXYpB4EIFnACHMJTSsAeSvcE90DAfBgNVHSMEGDAWgBSiXYpB4EIFnACH +MJTSsAeSvcE90DAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMCA0cAMEQCIDMI +8OAE9qbykxNMxLIAhNPJCFjJGE/U4/smQpIYB8MAAiBNgT9Nqv2MpUS3E+9Tg6u2 +/VSjqC+6Yy7sh4cd7GlG0Q== +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_ec.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_ec.key new file mode 100644 index 0000000000000..b10ab58b5dd3f --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_ec.key @@ -0,0 +1,8 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +MIH0MF8GCSqGSIb3DQEFDTBSMDEGCSqGSIb3DQEFDDAkBBBZ9OGpiId2dhQXH9Wj +E6UsAgIIADAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQUCYy3Ay5F7QODsco +vtaCxQSBkD6HuBgyX7GH9xW+RAxDr2hgOgasgwHbZiT/IT8xQDr5+tYY8Rstczm+ +d9ioa5MXHaMDhrQtUW+XQpwd56a0ygrdar5b5N/oA8/f/64hj/pTgAMxnIjwfyJg +0NBU/h3fPZ4zCS7HmPa6oqpnWhfnphKlURX9ANcvOvYOCzntWU3sfS2hL8vtBduA +NzizssOhjg== +-----END ENCRYPTED PRIVATE KEY----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_rsa.crt b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_rsa.crt new file mode 100644 index 0000000000000..702172f48d03a --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_rsa.crt @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDWzCCAkOgAwIBAgIUSgWZycmZNvUi8VhMjqAjSTaugckwDQYJKoZIhvcNAQEL +BQAwQDETMBEGCgmSJomT8ixkARkWA25ldDEXMBUGCgmSJomT8ixkARkWB2V4YW1w +bGUxEDAOBgNVBAMTB3NpZ25pbmcwHhcNMjUwNzAzMDcyNjE5WhcNMjgwNzAyMDcy +NjE5WjBAMRMwEQYKCZImiZPyLGQBGRYDbmV0MRcwFQYKCZImiZPyLGQBGRYHZXhh +bXBsZTEQMA4GA1UEAxMHc2lnbmluZzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBALD2UgeR5s6PIAdRSORhrHjfoNPunvNj9oMEANRYjqhWJ7ouyzFSB3GG +FtLbe4enrRHIUUCIQt60g6SvCAX+GCS3WemM19+70vc58rTh7JDbB4kMkXYeXT8H +Izm+TArgAV6scIvLwFNmgIuZp+YXWNFrV1eRv2OMSQ0c/aIu9WqS+GNem6TG1bXp +y1n+JtYNSyEJE0DOWUxFDfQz9/9HAGspiiyl/rBzTzYFwT1DuLKVjdLvXvhCHgaE +5pCWJ0HXFSMMHNWzdZdao7t7xpDf3ZMocTOAoC9o53YVMmOIkKgJDKIq4e9Ywsz8 +R+IiUWzEcfgac2wzWOlO+h7qC8kTCYMCAwEAAaNNMEswHQYDVR0OBBYEFNAC8Hgn +1NMUo/EmA17LvfphN8YtMB8GA1UdIwQYMBaAFNAC8Hgn1NMUo/EmA17LvfphN8Yt +MAkGA1UdEwQCMAAwDQYJKoZIhvcNAQELBQADggEBAFZ5yB9OV9YjOVrIyQiOtTru +xLl2qM/+UI/UIDbjMfK4grjhLfQBoSp/Qyd+YFoPPwJkcJpQSrj+JZ6l5DtU7put +PTklHmjl7uG1Pf7viMB0Cb14dFia+a7V+PEn8lNVrXTkBqjhX3j2RZ7dpWLF0OIh +tzcjwS4BR+axLAJbqKhhGg/zb+FeFkZggTY9FAp8IhgDRWOR9ky23zIA10U1ebds +SPsbCszxIfCEjp+KaHDSQJM5WRXnoLQQB+XdHq7WDu0fYdVvgjQXpGw4QFN0t/FU +wWicTQEYEWylcz3t/cdhnLw7HsPwYvXbGF2neuxVdmmiUDLFlnf64D6T86etTmE= +-----END CERTIFICATE----- diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_rsa.key b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_rsa.key new file mode 100644 index 0000000000000..36dfc424fddb0 --- /dev/null +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing_rsa.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAsPZSB5Hmzo8gB1FI5GGseN+g0+6e82P2gwQA1FiOqFYnui7L +MVIHcYYW0tt7h6etEchRQIhC3rSDpK8IBf4YJLdZ6YzX37vS9znytOHskNsHiQyR +dh5dPwcjOb5MCuABXqxwi8vAU2aAi5mn5hdY0WtXV5G/Y4xJDRz9oi71apL4Y16b +pMbVtenLWf4m1g1LIQkTQM5ZTEUN9DP3/0cAaymKLKX+sHNPNgXBPUO4spWN0u9e ++EIeBoTmkJYnQdcVIwwc1bN1l1qju3vGkN/dkyhxM4CgL2jndhUyY4iQqAkMoirh +71jCzPxH4iJRbMRx+BpzbDNY6U76HuoLyRMJgwIDAQABAoIBAAXtU+jOqkWmvpDT +u8E4RA4V7EoHk2YdbRD+39R1zjOf9406P+QfudcY83mG12mGQLP/fnGkKWNuvO5A +TlJS1sfYCkoS4fdys4mZguIr+I4rf+JHSlH1iSzGiAlXW6BUgoAf4h1bqtxcsey7 +EYD1O1cOFw6m940j2Y+iYKrUzCVlxwlA23Ms589mMliRwR0FnKd+52tB4O4FfwCC +g/IU/zcFgqoMX7q3Qdmv0nQ0yb3ixEYcS34NurdmbhNWgx5yQMEzUQjHDHcZ3z5q +QkSCHavypOeCwOF1qoPIoKQ9Z3SEuW/Z/4e4/mWt/UICMDztqfjiYQouMbK3D4wn +i84lnC0CgYEAw5RflFButojyOs6N/0N+788WAAbc/XjYAYMXznuJY+jmixAJH5Z9 +mn6M+g/3y0OokunPEWeg5lQ1/zJMat/dwOSHVHJ2Cha4Jnkt8JgsJAr+pUzQvrm/ +SERLNKRNVNFgeUVnMz6hqV7pYVZ7zwvQhOT4l2yCPP7BgP+DykPe/o0CgYEA56GR +6HPFtND5ddoJ5DBbWe1v7tOnOknHANFEEqNcA0emgiqVf4dyb5i23WW3GYKxWk/W +66FEtv3LGZ9IoGDAWM+eZMPtCv8kC9OPpK3Nk0Icsybj+s6uc6xLVNYn7criYBiz +jnTWjGPI/jiFn91sYtrJd0XGrE2R8hB62FcbbE8CgYAftriE8UHyoWQ9+u51nPlB +Y6AaowJEq8rC/AHpPoj5xXNUy2XfVGTLn4e2qM4yjKcSI42rMdWaY79ZwUs47VIl +DCmRnPndCvATdQTpBZPqyEmgfkM/GhmVW1WilJ3hig4NvB5O5fIK59QKL57l5PGM +CyDwVO0NfPXduBEjxDutpQKBgQCkGo0D8hnNHAzQ2RQO7c+aq6SUwLEGk8SAqMIg +rkn/LOEj8UWPX4fM1pYfzvNlCHncMRpkQBItzyr4USgkL8e2ZAmk/EZRdyezlUR8 +eIJf5QPuTQxR4eIoo5WPWlZZm1a8nGOB9vcV6ZA5xBOvijFC7By1+uJhqmdO5ywR +X81W8wKBgDLZ8XEfDX6/He6I2368c5JaNamlij6/RMssC+Kn5c0+IWHURxg0rcTl +CpPw4ZS96VNWWaU4OgA6txfGbS0CEZmlzWgErD2/yMvd+z9b2n0XZcZWVGvBTA0E +Ltm64LSnAaEjahZigl1wdtuAFjQM28ZsgIPr2uBwyZFrPh9cIeU4 +-----END RSA PRIVATE KEY----- From 4fea9e245565060ec47fa91823c5860921abb40f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Fred=C3=A9n?= <109296772+jfreden@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:44:56 +0200 Subject: [PATCH 02/24] Update docs/changelog/134137.yaml --- docs/changelog/134137.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 docs/changelog/134137.yaml diff --git a/docs/changelog/134137.yaml b/docs/changelog/134137.yaml new file mode 100644 index 0000000000000..edcf6db58c9d2 --- /dev/null +++ b/docs/changelog/134137.yaml @@ -0,0 +1,5 @@ +pr: 134137 +summary: Add signing configuration for cross cluster api keys +area: Security +type: enhancement +issues: [] From 93f2304bc11cf588ad77aa05c65219906525e09a Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Mon, 8 Sep 2025 08:40:41 +0200 Subject: [PATCH 03/24] fixup! CI --- .../CrossClusterSigningConfigurationReloaderIntegTests.java | 2 +- .../security/transport/CrossClusterApiKeySignerTests.java | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java index f0779ca972ee6..8fdecbcf2983d 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java @@ -234,7 +234,7 @@ private Set randomClusterAliases() { } private void writeSecureSettingsToKeyStoreAndReload(Map entries) { - char[] keyStorePassword = randomAlphaOfLengthBetween(1, randomInt(20)).toCharArray(); + char[] keyStorePassword = randomAlphaOfLengthBetween(1, randomIntBetween(5, 20)).toCharArray(); internalCluster().getInstances(Environment.class).forEach(environment -> { final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create(); entries.forEach(keyStoreWrapper::setString); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java index ccb546080af1a..63bfb9ccac6b9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java @@ -18,11 +18,6 @@ public class CrossClusterApiKeySignerTests extends ESTestCase { - @Override - public void setUp() throws Exception { - super.setUp(); - } - public void testLoadKeystore() { var builder = Settings.builder() .put("cluster.remote.my_remote.signing.keystore.alias", "wholelottakey") From 4035b14f62266212a8bf6f9806b30cb0cf8eb2f0 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Mon, 8 Sep 2025 10:14:48 +0200 Subject: [PATCH 04/24] fixup! fips --- .../CrossClusterApiKeySignerTests.java | 20 +++++++++++++----- .../xpack/security/signature/signing.bcfks | Bin 0 -> 5050 bytes 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing.bcfks diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java index 63bfb9ccac6b9..f80b332af29fb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java @@ -21,9 +21,9 @@ public class CrossClusterApiKeySignerTests extends ESTestCase { public void testLoadKeystore() { var builder = Settings.builder() .put("cluster.remote.my_remote.signing.keystore.alias", "wholelottakey") - .put("cluster.remote.my_remote.signing.keystore.path", getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks")) .put("path.home", createTempDir()) .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + addKeyStorePathToBuilder("my_remote", builder); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secretpassword"); builder.setSecureSettings(secureSettings); @@ -38,6 +38,7 @@ public void testLoadKeystoreMissingFile() { .put("cluster.remote.my_remote.signing.keystore.path", "not_a_valid_path") .put("path.home", createTempDir()) .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secretpassword"); builder.setSecureSettings(secureSettings); @@ -48,9 +49,10 @@ public void testLoadKeystoreMissingFile() { public void testLoadSeveralAliasesWithoutAliasSettingKeystore() { var builder = Settings.builder() - .put("cluster.remote.my_remote.signing.keystore.path", getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks")) .put("path.home", createTempDir()) .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + + addKeyStorePathToBuilder("my_remote", builder); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secretpassword"); builder.setSecureSettings(secureSettings); @@ -62,9 +64,7 @@ public void testLoadSeveralAliasesWithoutAliasSettingKeystore() { public void testGetDependentFilesToClusterAliases() { var builder = Settings.builder() .put("cluster.remote.my_remote1.signing.keystore.alias", "wholelottakey") - .put("cluster.remote.my_remote1.signing.keystore.path", getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks")) .put("cluster.remote.my_remote2.signing.keystore.alias", "wholelottakey") - .put("cluster.remote.my_remote2.signing.keystore.path", getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks")) .put( "cluster.remote.my_remote3.signing.certificate", getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt") @@ -77,6 +77,8 @@ public void testGetDependentFilesToClusterAliases() { .put("cluster.remote.my_remote4.signing.key", getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key")) .put("path.home", createTempDir()) .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + addKeyStorePathToBuilder("my_remote1", builder); + addKeyStorePathToBuilder("my_remote2", builder); MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("cluster.remote.my_remote1.signing.keystore.secure_password", "secretpassword"); secureSettings.setString("cluster.remote.my_remote2.signing.keystore.secure_password", "secretpassword"); @@ -86,7 +88,7 @@ public void testGetDependentFilesToClusterAliases() { assertEquals( Map.of( - getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks"), + getDataPath("/org/elasticsearch/xpack/security/signature/signing." + (inFipsJvm() ? "bcfks" : "jks")), Set.of("my_remote1", "my_remote2"), getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt"), Set.of("my_remote3", "my_remote4"), @@ -96,4 +98,12 @@ public void testGetDependentFilesToClusterAliases() { signer.getDependentFilesToClusterAliases() ); } + + private void addKeyStorePathToBuilder(String remoteCluster, Settings.Builder builder) { + builder.put("cluster.remote." + remoteCluster + ".signing.keystore.type", inFipsJvm() ? "BCFKS" : "PKCS12") + .put( + "cluster.remote." + remoteCluster + ".signing.keystore.path", + getDataPath("/org/elasticsearch/xpack/security/signature/signing." + (inFipsJvm() ? "bcfks" : "jks")) + ); + } } diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing.bcfks b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/signature/signing.bcfks new file mode 100644 index 0000000000000000000000000000000000000000..fe9c079bc9808cd4a72a515035ee83c6df2a2e76 GIT binary patch literal 5050 zcmZA5Wjq`JD=B9_!F~)SqbWe9Tr@P&mc1(9YFP<0w z&;NP(d-Z$&g#>~cAb}*ekbneiT#nGsq1OZeECNVCD8%Cb8XuyADMn9Qq-P|@NEt8R z!EYBwt{ca66hxP)Xu63{n~~FZp~~A zl@rj>#n8|JbP#-O;D0~pcvxr&bX&OX z_{)(v*3Vmq(9=ugr?Z9TVOG;%?^V%@`|Yo6s^;r32GF=7%QpSGc+OFM>p(OhBM7{~ z#0^SWAv4caW-+UAd`ML&R%?-&n%GC#<}xz!d+9*8BkglaSlDqnrQqvXwbNy(@7aG- zHDazeBIS(C7E3y)Xu{e2DY+Kk<$J`sOo;KlV;7!vu&b5I2c~L@02nfphRmRaZl6eS zeJz&Tl%Q4x+BT$)bp!+-pOY4J+6}5fe^n-EI&CE!k8FZAW)ndj#cX{4WDSPs!vD-T z7u7wf7Rpe8rp&v;rF?zU1RNYj;=Vri%!BI26xcV;`-8C9(y)#NMt9cxVC8$- z)VVYbcw(d3{L(lZhuXe5GORU#UvjSN5$TPB=W@6SDe>kFG4{-dY7uG=G;B5#fh>rn z|J@S>&(ew;P7mT3OTIoK?cA5yM3>0G$gVGEt-V(OD>TQ{0 zA+DeAf2hp3g35b#yRssN>Y9E5RvbP0m3{a;M<3_c=Ir0UA?D-TWclEkS7^%yM(>Sf z$P=`;Pw4u^iXTUau*6+#2On9%O6W~mLz(36%jM3C8#PMXuvpn+It{0EH9!=I4`CDU zx(!vfd3`$fE4JICcD3FnrYEJ@bUD8x4^S}#U}XX#dw1dNX=jt_jx)c8Ulvku2gS)|WIzTWayl>Qm@I8SYj9-Vs@HCbC|%5PUXW})%f z_hMb)NMcL35XN@7##r}3OC{yph#mzOquh>(h9+qf3+L!`ir*SNk8&FufFvazMvVv1 zduKYg5SMDp?Z9{u-)=wxVqAr8^E`6gx>6i<%JEWRY}XT9Oi*ahiVbV8PB;=k>jqzu z3k)M?9Rpp7r%^d1jp)bUI=sA!?@Fn^mqtS`@87Q%OTJD2b68+3MU+66L*Bp>rt)p} zyWWfIShTxb366(r?|KXUC@fP`=#R~lDt!aFP33<#?0{z_x{Et!AJ)er9&Us~{AM*!RW77?^80+!)AK%LFf9pwZw( zenJ87j?6(GckS1#1cBG2GD|u~yZ$zArBAa@Z7<9TpR<1-=AQ6wP&bu>?MVsAMS$zEiVzrM@y7Y)AXZv7<$ zr(BYv*~5r!PI1f?kJ6*Cm9XXchY)o?;Q75pxK2EtUeenJf#1YG*osmnhXPtn8C7_E z(@C_+1Uo;i3BQGoGamFTbUn%T{iu9Bp}s1K&bq^F{GBER^V}-2wu;eWY*6w?jT(m( zO?L7YAESoH&+ust>&lQ;>M z)ob`q=B2di6slbDvimewQe}z9HRG-o_~+`nZI0v5T|`!Rx|p-R=luY#$yANXtAzD|8bYR5qVrarLdbv_u?92dn&@v9Ez)FHIF) zq$63q#ci|4d}U|nGSuJ(Rs1nqpXro2YBG{l(0O6WGBBLAdsw(@z6HBlFZrYPu{Kmh zvK(9qsGDhb$Ar3iX33#>Y;DhKDj31(1+N2d;B;8FjA8znXr_XO2b3M*Uo(q39FNfD zBx3lZd{@4n52`{gyyhz8%TnHLkzS=KJhyUg82`?N+sa1+!(Z}HI)rypC2A7!u%LSj zpByd_NYvDyokMJx{V>-Q1lkDF8Ai;Qqi=PZ7LZ%wTak0BY~svuVf|hnn|v3nC;y~0 zw0f?Xn=V@Q?o4E8>`ckXpj(6Ai9+h37plFrNiL*bxI0?uIC-JQ-T$z1^9>#`>Y>SP zE60y+6$MR%0Uh;;5X7-Ra69%EJpKW8QCvw(5)|d^XuoX3G`IqiZ+y|IRulY_-CuT(x})tg>3Xhoo6tL;|5no?d=QPVD+kWXiKifGT2cqR~YkA zg5VW@mS9bRZU>08v2cW*=-8)r_bNIPR^l1zY4Qkc+~fop8uhWef)ByNc%P$#~ zTOYIO%7@=21E!;}s(i6T2&GX`I++Ln(k#u~rIk*zPh9SdIcD}!xf^UBEAwJbfn+$C zA~C^x`k!hJjjwg?3j9U;Sh;(+e7KZo&1SRKxw8H)hKf4zax@r1|>y1#=oJZQHqNhD40BxH(|#_|cZ zJ1Mx3Vb_W(U;J5rnzs_SzIv*=;vGqsey&8xqyYW7IWuwE6=-YS-hV9TVD*@Z(X1d4 zoBe)7Rrdkee}pLU>pnJS2`o5d!4#e3i;BAJ$pB_ll^o)=NMKJI3JuQXe zh^;+(uAq2=pKtj6{P(f~-B^OYf!OA|CF-E;=2DddVT1D%5q}T1q&aNIN?;6PE3WX` znw4(Bn^BiKhrF5F-a|ZD-bfYnQG6%XJfP19^T~vl`c;mtC*6iqc#W8r z0fy#gW$$f%5ZbBe9@Wdivqv4%g1VF3IJyWVf3e{F@Z5nDrCQ<%$!Ysdf_K^a55u zgQO^BpPT*L7l!Is_e|dSx7*J4TE~xtF#C((7NsjDQw zo5?C38YMlxCyX>aLUpNWmq6C!MvXrHTIYbqA);~VX(_|%*kws+bAVnGG@RqgvQR+V z9(G|vkF~w7$aheQc5z_>7V%=3D0E8dVmMa~j5pVFL zzWVckKJVmsmD7AnY!@mpmk8tjKsWR;I6Z%OSd;M;_d9Ixf(d2)U@(~N>ViV5cJgFY zMxT5@!fq>*rha)dJT%bnpzB=!U{N-8k&Y!M!XKEhP7 z9mn68BbY$ilsCGb8YORTwS%xM1B}U4Ruv=+ji_P|5$pevPl#%pvXVnq*^y)WC6K24 z`_jDigbkc4ax+|@w}`jL%{A01{}@)KEm;H2Do@FwHfDo5Fk%MA944&aPjN<;1p9pL zBM29-C}#0<5wBL-cqe;wJXItWlF0U2j&Kex?erY{y;`V#S>mtmh`u3iS-PM<|C;Vl z`k7)Db6C8R_9>jTcWF!g6?s%51SiYn^$_pKUjT#Sg{+Z&%z&~VSAB{_+QVph9Wpvc z(;%)B2*{{;%D0oJG$Egw>HY>U74yYDM}1N1>iemBh|)r3ub&Ap9qvZ7Fg&Zwz~BXJ z%*KcnrUtkXR=eHKB`2=soX`Z4hAu$Nk05*AW{cT10$b}3c$;}B_JtzP=dqh`V66wH zOgd4r6|3rMr;HXc`p58Hk+b3v2j+OHCnD%O3Bzm)yL6E6%wP9m^Ds$iUvx9sud|6F zMbNcBUgM+zM)=-C63~~Dx+pqgpH`Kv6aU1cpkFa)@k>QEQPM;3xGxjFw8CtJlpNv{ zDK-qeR)Dp?;7-eH$yeJwUgA#X-2TL8tsR@{-_SgDjdIiW6v?pX^?2ckcfeCXygPAp z!-2j1tw4D*s;5G6WNO*@rwxz1VAR#aR!iLZlNzn1QFB*kSs4z=(J{WLW2Qa3`DJa# zeK|jMLW6v_kqKf@ul1KqV#2egAI9Qq+PlKq(3L>l?-fd}-PdPllk*+fBXoo~Ce*&k zdCsmuX&#`KP*#3iOHHGzpBdiTW~;Stv5BnfdM&6?*9+vBsf@{kgF>t|IL_5b=uEK| zb;n&1r6Np)VmOB(cUe6A8z8g-u6C!+M{}NXS+ldk#_4yPLmX5kl;*U!lUlOCP;% zO5d!rP#t=+hqOUQTXY#;GxFWKWH6?NS- zcNuv>-%NPuWEIMu>)r|*tk`4P8wz1D7aR4AEOk<(Jl@OCz!XVh-0Q3#8>?|!IOEj| z6qY|H@;um@VHlNaDB$BEaf?q|h6nAvq~*7bn+(mTvE9D!#ft_oc~hxhl}2g#HJAxq zsitM4sHG1lr#>nAZbRgRrQefKRMVl>F^dv;GRmX4 z?02U;e5+DHR4g2QLdn@&vp{;%ZIh>6s#8z(AF*G%MviloJsSCyg~RD(3b*;4)AKAG z5UT1f%sBRytt}@QAgpYAmwlfW(!%K|EKdGiD*hKAym|hTfFI0Y7MvlH*6KwTR&%Ap zcGYC36QDR+Mq;Dae)+D^EZzVT(C|Mb^}q1Nzu{bUG-^-w-F0g6*(7)}l`B7Se9yd| zdI?YTiL~%O+)#3Qx}iXdry2hXjS^k60)aK2sO-T~k@Ni9HOrm(|H7Bp|G!|0o&K0C zAdl@kAAj>PEdayHRBhEIFb*?g&%kwN{<-=tk&wds8iA^PGUGM648I-tNiw{2$YwzH|Tp literal 0 HcmV?d00001 From ab97104bf4a6ee390d7c2b9bef10b6c7189b5279 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Mon, 8 Sep 2025 10:36:43 +0200 Subject: [PATCH 05/24] fixup! Update forbidden patterns --- x-pack/plugin/security/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index a14bb5f3146c9..8744d126f3b05 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -184,6 +184,7 @@ tasks.named("dependencyLicenses").configure { tasks.named("forbiddenPatterns").configure { exclude '**/*.key' + exclude '**/*.bcfks' exclude '**/*.p12' exclude '**/*.der' exclude '**/*.zip' From 181b715ad8a0cae0f33eedef2a909981cdb8aafe Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Mon, 8 Sep 2025 10:44:18 +0200 Subject: [PATCH 06/24] fixup! Test issue --- .../CrossClusterSigningConfigurationReloaderIntegTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java index 8fdecbcf2983d..012f2b46253f0 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java @@ -230,7 +230,7 @@ private void addAndRemoveClusterConfigsRuntime( } private Set randomClusterAliases() { - return randomUnique(() -> randomAlphaOfLengthBetween(1, randomInt(20)), randomInt(5)); + return randomUnique(() -> randomAlphaOfLengthBetween(1, randomIntBetween(5, 20)), randomInt(5)); } private void writeSecureSettingsToKeyStoreAndReload(Map entries) { From 75e6ac2c329fbbab4f0c8b2ce652378f1d007d50 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Mon, 8 Sep 2025 13:36:23 +0200 Subject: [PATCH 07/24] fixup! Update tests for fips compliance --- .../CrossClusterApiKeySignerIntegTests.java | 11 +++++++++-- ...usterSigningConfigurationReloaderIntegTests.java | 13 +++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java index 6e0ed12b75c97..f6099cb39bfff 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java @@ -16,7 +16,9 @@ import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_ALIAS; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_PATH; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_SECURE_PASSWORD; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_TYPE; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_PATH; +import static org.hamcrest.Matchers.equalToIgnoringCase; public class CrossClusterApiKeySignerIntegTests extends SecurityIntegTestCase { @@ -40,7 +42,7 @@ public void testSignWithPemKeyConfig() { getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt").getParent() ); - assertEquals(signature.algorithm(), keyConfig.getKeys().getFirst().v2().getSigAlgName()); + assertThat(signature.algorithm(), equalToIgnoringCase(keyConfig.getKeys().getFirst().v2().getSigAlgName())); assertEquals(signature.certificate(), keyConfig.getKeys().getFirst().v2()); } @@ -65,9 +67,13 @@ public void testSeveralKeyStoreAliases() { // Create a new config without an alias. Since there are several aliases in the keystore, no signature should be generated updateClusterSettings( Settings.builder() + .put( + SIGNING_KEYSTORE_TYPE.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), + inFipsJvm() ? "BCFKS" : "PKCS12" + ) .put( SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), - getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks") + getDataPath("/org/elasticsearch/xpack/security/signature/signing." + (inFipsJvm() ? "bcfks" : "jks")) ) ); @@ -100,6 +106,7 @@ public void testSeveralKeyStoreAliases() { Settings.builder() .putNull(SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey()) .putNull(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey()) + .putNull(SIGNING_KEYSTORE_TYPE.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey()) .setSecureSettings(new MockSecureSettings()) ); } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java index 012f2b46253f0..492ab6ec88d00 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java @@ -72,16 +72,16 @@ public void testAddSecureSettingsConfigRuntime() throws Exception { updateClusterSettings( Settings.builder() - .put( - SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(clusterAlias).getKey(), - getDataPath("/org/elasticsearch/xpack/security/signature/signing.jks") - ) - .put(SIGNING_KEYSTORE_TYPE.getConcreteSettingForNamespace(clusterAlias).getKey(), "jks") .put( SIGNING_KEYSTORE_ALGORITHM.getConcreteSettingForNamespace(clusterAlias).getKey(), KeyManagerFactory.getDefaultAlgorithm() ) .put(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(clusterAlias).getKey(), "wholelottakey") + .put(SIGNING_KEYSTORE_TYPE.getConcreteSettingForNamespace(clusterAlias).getKey(), inFipsJvm() ? "BCFKS" : "PKCS12") + .put( + SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(clusterAlias).getKey(), + getDataPath("/org/elasticsearch/xpack/security/signature/signing." + (inFipsJvm() ? "bcfks" : "jks")) + ) ); }, clusterAlias -> { updateClusterSettings( @@ -96,6 +96,7 @@ public void testAddSecureSettingsConfigRuntime() throws Exception { } public void testDependentKeyConfigFilesUpdated() throws Exception { + assumeFalse("Test credentials uses key encryption not supported in Fips JVM", inFipsJvm()); final CrossClusterApiKeySigner signer = internalCluster().getInstance( CrossClusterApiKeySigner.class, internalCluster().getRandomNodeName() @@ -234,7 +235,7 @@ private Set randomClusterAliases() { } private void writeSecureSettingsToKeyStoreAndReload(Map entries) { - char[] keyStorePassword = randomAlphaOfLengthBetween(1, randomIntBetween(5, 20)).toCharArray(); + char[] keyStorePassword = randomAlphaOfLengthBetween(15, randomIntBetween(15, 20)).toCharArray(); internalCluster().getInstances(Environment.class).forEach(environment -> { final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create(); entries.forEach(keyStoreWrapper::setString); From 1fe644441e49a31d9d334227e12f0f5b38f3882c Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 9 Sep 2025 08:38:19 +0200 Subject: [PATCH 08/24] fixup! Make buildEffectiveSettings clearer --- .../security/transport/CrossClusterApiKeySigner.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index d6fed2c720a07..a4566fb2f6549 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -134,6 +134,7 @@ private void loadSigningConfigs() { * Build the effective remote cluster settings by merging the currently configured (if any) and new/updated settings *

* - If newSettings is null - use existing settings, used to refresh the dependent files + * - If newSettings is empty - return empty settings, used for resetting signing config * - If updateSecureSettings is true - merge secure settings from newSettings with current settings, used by secure settings refresh * - If updateSecureSettings is false - merge new settings with existing secure settings, used for regular settings update */ @@ -142,14 +143,14 @@ private Settings buildEffectiveSettings( @Nullable Settings newSettings, boolean updateSecureSettings ) { - if (currentSettings == null && newSettings == null) { - return Settings.EMPTY; + if (currentSettings == null) { + return newSettings == null ? Settings.EMPTY : newSettings; } if (newSettings == null) { return currentSettings; } - if (currentSettings == null || newSettings.isEmpty()) { - return newSettings; + if (newSettings.isEmpty()) { + return Settings.EMPTY; } Settings secureSettingsSource = updateSecureSettings ? newSettings : currentSettings; From b87acdaa29c5aaffa720c8bc65b4408c5e3dcfbe Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 9 Sep 2025 10:13:11 +0200 Subject: [PATCH 09/24] fixup! Code review comments --- .../transport/RemoteClusterSettings.java | 16 +++++----- .../xpack/security/Security.java | 22 ++++++------- .../transport/CrossClusterApiKeySigner.java | 15 +++++---- .../CrossClusterApiKeySignerReloader.java | 18 +++++------ .../CrossClusterApiKeySignerSettings.java | 19 ++++++------ .../SecurityServerTransportInterceptor.java | 7 +---- ...curityServerTransportInterceptorTests.java | 31 +++++-------------- 7 files changed, 51 insertions(+), 77 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/transport/RemoteClusterSettings.java b/server/src/main/java/org/elasticsearch/transport/RemoteClusterSettings.java index f2bc9a24ffddb..f5b72bf6e40b8 100644 --- a/server/src/main/java/org/elasticsearch/transport/RemoteClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/transport/RemoteClusterSettings.java @@ -42,14 +42,12 @@ public class RemoteClusterSettings { - public static final String REMOTE_CLUSTER_SETTINGS_PREFIX = "cluster.remote."; - public static final TimeValue DEFAULT_INITIAL_CONNECTION_TIMEOUT = TimeValue.timeValueSeconds(30); /** * The initial connect timeout for remote cluster connections */ public static final Setting REMOTE_INITIAL_CONNECTION_TIMEOUT_SETTING = Setting.positiveTimeSetting( - REMOTE_CLUSTER_SETTINGS_PREFIX + "initial_connect_timeout", + "cluster.remote.initial_connect_timeout", DEFAULT_INITIAL_CONNECTION_TIMEOUT, Setting.Property.NodeScope ); @@ -61,13 +59,13 @@ public class RemoteClusterSettings { * The value of the setting is expected to be a boolean, {@code true} for nodes that can become gateways, {@code false} otherwise. */ public static final Setting REMOTE_NODE_ATTRIBUTE = Setting.simpleString( - REMOTE_CLUSTER_SETTINGS_PREFIX + "node.attr", + "cluster.remote.node.attr", Setting.Property.NodeScope ); public static final boolean DEFAULT_SKIP_UNAVAILABLE = true; public static final Setting.AffixSetting REMOTE_CLUSTER_SKIP_UNAVAILABLE = Setting.affixKeySetting( - REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", "skip_unavailable", (ns, key) -> boolSetting( key, @@ -79,7 +77,7 @@ public class RemoteClusterSettings { ); public static final Setting.AffixSetting REMOTE_CLUSTER_PING_SCHEDULE = Setting.affixKeySetting( - REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", "transport.ping_schedule", (ns, key) -> timeSetting( key, @@ -91,7 +89,7 @@ public class RemoteClusterSettings { ); public static final Setting.AffixSetting REMOTE_CLUSTER_COMPRESS = Setting.affixKeySetting( - REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", "transport.compress", (ns, key) -> enumSetting( Compression.Enabled.class, @@ -104,7 +102,7 @@ public class RemoteClusterSettings { ); public static final Setting.AffixSetting REMOTE_CLUSTER_COMPRESSION_SCHEME = Setting.affixKeySetting( - REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", "transport.compression_scheme", (ns, key) -> enumSetting( Compression.Scheme.class, @@ -117,7 +115,7 @@ public class RemoteClusterSettings { ); public static final Setting.AffixSetting REMOTE_CLUSTER_CREDENTIALS = Setting.affixKeySetting( - REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", "credentials", key -> SecureSetting.secureString(key, null) ); 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 80a5c373dca30..295a6f21043a7 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 @@ -220,6 +220,7 @@ import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.AnonymousUser; +import org.elasticsearch.xpack.core.ssl.SSLConfigurationReloader; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.SslProfile; @@ -606,8 +607,6 @@ public class Security extends Plugin private final SetOnce tokenService = new SetOnce<>(); private final SetOnce securityActionFilter = new SetOnce<>(); private final SetOnce crossClusterAccessAuthcService = new SetOnce<>(); - private final SetOnce crossClusterApiKeySigner = new SetOnce<>(); - private final SetOnce crossClusterApiKeySignerReloader = new SetOnce<>(); private final SetOnce sharedGroupFactory = new SetOnce<>(); private final SetOnce dlsBitsetCache = new SetOnce<>(); private final SetOnce> bootstrapChecks = new SetOnce<>(); @@ -1169,16 +1168,14 @@ Collection createComponents( DestructiveOperations destructiveOperations = new DestructiveOperations(settings, clusterService.getClusterSettings()); crossClusterAccessAuthcService.set(new CrossClusterAccessAuthenticationService(clusterService, apiKeyService, authcService.get())); components.add(crossClusterAccessAuthcService.get()); - crossClusterApiKeySigner.set(new CrossClusterApiKeySigner(environment)); - components.add(crossClusterApiKeySigner.get()); - crossClusterApiKeySignerReloader.set( - new CrossClusterApiKeySignerReloader( - resourceWatcherService, - clusterService.getClusterSettings(), - crossClusterApiKeySigner.get() - ) + var crossClusterApiKeySigner = new CrossClusterApiKeySigner(environment); + components.add(crossClusterApiKeySigner); + var crossClusterApiKeySignerReloader = new CrossClusterApiKeySignerReloader( + resourceWatcherService, + clusterService.getClusterSettings(), + crossClusterApiKeySigner ); - components.add(crossClusterApiKeySignerReloader.get()); + components.add(crossClusterApiKeySignerReloader); securityInterceptor.set( new SecurityServerTransportInterceptor( settings, @@ -1189,8 +1186,7 @@ Collection createComponents( securityContext.get(), destructiveOperations, crossClusterAccessAuthcService.get(), - getLicenseState(), - crossClusterApiKeySigner.get() + getLicenseState() ) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index a4566fb2f6549..b993564912f61 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -18,7 +18,6 @@ import org.elasticsearch.env.Environment; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; -import org.elasticsearch.transport.RemoteClusterSettings; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import java.nio.charset.StandardCharsets; @@ -46,7 +45,6 @@ public class CrossClusterApiKeySigner { private final Logger logger = LogManager.getLogger(getClass()); private final Environment environment; private static final Map SIGNATURE_ALGORITHM_BY_TYPE = Map.of("RSA", "SHA256withRSA", "EC", "SHA256withECDSA"); - private final Map signingConfigByClusterAlias = new ConcurrentHashMap<>(); public CrossClusterApiKeySigner(Environment environment) { @@ -125,9 +123,9 @@ public X509CertificateSignature sign(String clusterAlias, String... headers) { } private void loadSigningConfigs() { - this.environment.settings().getGroups(RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, true).forEach((alias, settings) -> { - loadSigningConfig(alias, settings, false); - }); + this.environment.settings() + .getGroups("cluster.remote.", true) + .forEach((alias, settings) -> { loadSigningConfig(alias, settings, false); }); } /** @@ -157,7 +155,12 @@ private Settings buildEffectiveSettings( Settings settingsSource = updateSecureSettings ? currentSettings : newSettings; SecureSettings secureSettings = Settings.builder().put(secureSettingsSource, true).getSecureSettings(); - return Settings.builder().put(settingsSource, false).setSecureSettings(secureSettings).build(); + + var builder = Settings.builder().put(settingsSource, false); + if (secureSettings != null) { + builder.setSecureSettings(secureSettings); + } + return builder.build(); } void reloadSigningConfigs(Set clusterAliases) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java index adcd4deb671d0..f8276ebcbd7e5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java @@ -15,7 +15,6 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.core.Strings; -import org.elasticsearch.transport.RemoteClusterSettings; import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; @@ -54,7 +53,7 @@ public CrossClusterApiKeySignerReloader( ) { this.apiKeySigner = apiKeySigner; clusterSettings.addAffixGroupUpdateConsumer(getDynamicSettings(), (key, val) -> { - apiKeySigner.loadSigningConfig(key, val.getByPrefix(RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX + key + "."), false); + apiKeySigner.loadSigningConfig(key, val.getByPrefix("cluster.remote." + key + "."), false); logger.info("Updated signing configuration for [{}] due to updated cluster settings", key); watchDependentFilesForClusterAliases( apiKeySigner::reloadSigningConfigs, @@ -131,14 +130,13 @@ public void reload(Settings settings) { // closed. Since the secure settings will potentially be used later when the signing config is used to sign headers, the // settings need to be retrieved from the keystore and cached Settings cachedSettings = Settings.builder().setSecureSettings(extractSecureSettings(settings, getSecureSettings())).build(); - cachedSettings.getGroups(RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, true) - .forEach((clusterAlias, settingsForCluster) -> { - // Only update signing config if settings were found, since empty config means config deletion - if (settingsForCluster.isEmpty() == false) { - apiKeySigner.loadSigningConfig(clusterAlias, settingsForCluster, true); - logger.info("Updated signing configuration for [{}] due to reload of secure settings", clusterAlias); - } - }); + cachedSettings.getGroups("cluster.remote.", true).forEach((clusterAlias, settingsForCluster) -> { + // Only update signing config if settings were found, since empty config means config deletion + if (settingsForCluster.isEmpty() == false) { + apiKeySigner.loadSigningConfig(clusterAlias, settingsForCluster, true); + logger.info("Updated signing configuration for [{}] due to reload of secure settings", clusterAlias); + } + }); } catch (GeneralSecurityException e) { logger.error("Keystore exception while reloading signing configuration after reload of secure settings", e); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java index dc4a82679dedf..ae8cf013a48fc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java @@ -12,7 +12,6 @@ import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.ssl.SslConfigurationKeys; import org.elasticsearch.common.util.CollectionUtils; -import org.elasticsearch.transport.RemoteClusterSettings; import java.util.List; @@ -24,7 +23,7 @@ public class CrossClusterApiKeySignerSettings { static final String KEYSTORE_ALIAS_SUFFIX = "keystore.alias"; static final Setting.AffixSetting SIGNING_KEYSTORE_ALIAS = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + KEYSTORE_ALIAS_SUFFIX, key -> Setting.simpleString(key, newKey -> { @@ -32,19 +31,19 @@ public class CrossClusterApiKeySignerSettings { ); static final Setting.AffixSetting SIGNING_KEYSTORE_PATH = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_PATH, key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) ); static final Setting.AffixSetting SIGNING_KEYSTORE_SECURE_PASSWORD = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_SECURE_PASSWORD, key -> SecureSetting.secureString(key, null) ); static final Setting.AffixSetting SIGNING_KEYSTORE_ALGORITHM = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_ALGORITHM, key -> Setting.simpleString( key, @@ -56,31 +55,31 @@ public class CrossClusterApiKeySignerSettings { ); static final Setting.AffixSetting SIGNING_KEYSTORE_TYPE = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_TYPE, key -> Setting.simpleString(key, "", Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) ); static final Setting.AffixSetting SIGNING_KEYSTORE_SECURE_KEY_PASSWORD = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEYSTORE_SECURE_KEY_PASSWORD, key -> SecureSetting.secureString(key, null) ); static final Setting.AffixSetting SIGNING_KEY_PATH = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEY, key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) ); static final Setting.AffixSetting SIGNING_KEY_SECURE_PASSPHRASE = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.KEY_SECURE_PASSPHRASE, key -> SecureSetting.secureString(key, null) ); static final Setting.AffixSetting SIGNING_CERT_PATH = Setting.affixKeySetting( - RemoteClusterSettings.REMOTE_CLUSTER_SETTINGS_PREFIX, + "cluster.remote.", SETTINGS_PART_SIGNING + "." + SslConfigurationKeys.CERTIFICATE, key -> Setting.simpleString(key, Setting.Property.NodeScope, Setting.Property.Filtered, Setting.Property.Dynamic) ); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java index 133b3417c27d1..fb56a9f46cc4b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptor.java @@ -100,7 +100,6 @@ public class SecurityServerTransportInterceptor implements TransportInterceptor private final CrossClusterAccessAuthenticationService crossClusterAccessAuthcService; private final Function> remoteClusterCredentialsResolver; private final XPackLicenseState licenseState; - private final CrossClusterApiKeySigner crossClusterApiKeySigner; public SecurityServerTransportInterceptor( Settings settings, @@ -111,8 +110,7 @@ public SecurityServerTransportInterceptor( SecurityContext securityContext, DestructiveOperations destructiveOperations, CrossClusterAccessAuthenticationService crossClusterAccessAuthcService, - XPackLicenseState licenseState, - CrossClusterApiKeySigner crossClusterApiKeySigner + XPackLicenseState licenseState ) { this( settings, @@ -124,7 +122,6 @@ public SecurityServerTransportInterceptor( destructiveOperations, crossClusterAccessAuthcService, licenseState, - crossClusterApiKeySigner, RemoteConnectionManager::resolveRemoteClusterAliasWithCredentials ); } @@ -139,7 +136,6 @@ public SecurityServerTransportInterceptor( DestructiveOperations destructiveOperations, CrossClusterAccessAuthenticationService crossClusterAccessAuthcService, XPackLicenseState licenseState, - CrossClusterApiKeySigner crossClusterApiKeySigner, // Inject for simplified testing Function> remoteClusterCredentialsResolver ) { @@ -151,7 +147,6 @@ public SecurityServerTransportInterceptor( this.securityContext = securityContext; this.crossClusterAccessAuthcService = crossClusterAccessAuthcService; this.licenseState = licenseState; - this.crossClusterApiKeySigner = crossClusterApiKeySigner; this.remoteClusterCredentialsResolver = remoteClusterCredentialsResolver; this.profileFilters = initializeProfileFilters(destructiveOperations); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java index 095498f1adac5..655e4eba4a179 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/SecurityServerTransportInterceptorTests.java @@ -120,7 +120,6 @@ public class SecurityServerTransportInterceptorTests extends ESTestCase { private SecurityContext securityContext; private ClusterService clusterService; private MockLicenseState mockLicenseState; - private CrossClusterApiKeySigner crossClusterApiKeySigner; @Override public void setUp() throws Exception { @@ -131,7 +130,6 @@ public void setUp() throws Exception { threadContext = threadPool.getThreadContext(); securityContext = spy(new SecurityContext(settings, threadPool.getThreadContext())); mockLicenseState = MockLicenseState.createMock(); - crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); Mockito.when(mockLicenseState.isAllowed(Security.ADVANCED_REMOTE_CLUSTER_SECURITY_FEATURE)).thenReturn(true); } @@ -160,8 +158,7 @@ public void testSendAsync() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - crossClusterApiKeySigner + mockLicenseState ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -212,8 +209,7 @@ public void testSendAsyncSwitchToSystem() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - crossClusterApiKeySigner + mockLicenseState ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -257,8 +253,7 @@ public void testSendWithoutUser() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - crossClusterApiKeySigner + mockLicenseState ) { @Override void assertNoAuthentication(String action) {} @@ -320,8 +315,7 @@ public void testSendToNewerVersionSetsCorrectVersion() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - crossClusterApiKeySigner + mockLicenseState ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -389,8 +383,7 @@ public void testSendToOlderVersionSetsCorrectVersion() throws Exception { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - crossClusterApiKeySigner + mockLicenseState ); ClusterServiceUtils.setState(clusterService, clusterService.state()); // force state update to trigger listener @@ -456,8 +449,7 @@ public void testSetUserBasedOnActionOrigin() { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - crossClusterApiKeySigner + mockLicenseState ); final AtomicBoolean calledWrappedSender = new AtomicBoolean(false); @@ -625,7 +617,6 @@ public void testSendWithCrossClusterAccessHeadersWithUnsupportedLicense() throws ), mock(CrossClusterAccessAuthenticationService.class), unsupportedLicenseState, - crossClusterApiKeySigner, mockRemoteClusterCredentialsResolver(remoteClusterAlias) ); @@ -763,7 +754,6 @@ private void doTestSendWithCrossClusterAccessHeaders( ), mock(CrossClusterAccessAuthenticationService.class), mockLicenseState, - crossClusterApiKeySigner, ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) ); @@ -902,7 +892,6 @@ public void testSendWithUserIfCrossClusterAccessHeadersConditionNotMet() throws ), mock(CrossClusterAccessAuthenticationService.class), mockLicenseState, - crossClusterApiKeySigner, ignored -> notRemoteConnection ? Optional.empty() : (finalNoCredential @@ -962,7 +951,6 @@ public void testSendWithCrossClusterAccessHeadersThrowsOnOldConnection() throws ), mock(CrossClusterAccessAuthenticationService.class), mockLicenseState, - crossClusterApiKeySigner, ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) ); @@ -1062,7 +1050,6 @@ public void testSendRemoteRequestFailsIfUserHasNoRemoteIndicesPrivileges() throw ), mock(CrossClusterAccessAuthenticationService.class), mockLicenseState, - crossClusterApiKeySigner, ignored -> Optional.of(new RemoteClusterAliasWithCredentials(remoteClusterAlias, new SecureString(encodedApiKey.toCharArray()))) ); @@ -1172,8 +1159,7 @@ public void testProfileFiltersCreatedDifferentlyForDifferentTransportAndRemoteCl new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - crossClusterApiKeySigner + mockLicenseState ); final Map profileFilters = securityServerTransportInterceptor.getProfileFilters(); @@ -1231,8 +1217,7 @@ public void testNoProfileFilterForRemoteClusterWhenTheFeatureIsDisabled() { new ClusterSettings(Settings.EMPTY, Collections.singleton(DestructiveOperations.REQUIRES_NAME_SETTING)) ), mock(CrossClusterAccessAuthenticationService.class), - mockLicenseState, - crossClusterApiKeySigner + mockLicenseState ); final Map profileFilters = securityServerTransportInterceptor.getProfileFilters(); From 8e24ec6629c95dc7b21b9e73146fdbe061cece9c Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 9 Sep 2025 08:20:02 +0000 Subject: [PATCH 10/24] [CI] Auto commit changes from spotless --- .../src/main/java/org/elasticsearch/xpack/security/Security.java | 1 - 1 file changed, 1 deletion(-) 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 295a6f21043a7..703b1dda95aaa 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 @@ -220,7 +220,6 @@ import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.user.AnonymousUser; -import org.elasticsearch.xpack.core.ssl.SSLConfigurationReloader; import org.elasticsearch.xpack.core.ssl.SSLConfigurationSettings; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.SslProfile; From 3ce1bf6bd0fe3810c863cb8aff839bced664b73e Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 9 Sep 2025 15:48:15 +0200 Subject: [PATCH 11/24] fixup! address race condition when creating signer --- ...usterSigningConfigReloaderIntegTests.java} | 2 +- .../xpack/security/Security.java | 16 +- .../transport/CrossClusterApiKeySigner.java | 28 +-- ...ssClusterApiKeySigningConfigReloader.java} | 105 +++++++--- ...CrossClusterApiKeySignerReloaderTests.java | 157 -------------- .../CrossClusterApiKeySignerTests.java | 41 ---- ...usterApiKeySigningConfigReloaderTests.java | 195 ++++++++++++++++++ 7 files changed, 295 insertions(+), 249 deletions(-) rename x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/{CrossClusterSigningConfigurationReloaderIntegTests.java => CrossClusterSigningConfigReloaderIntegTests.java} (99%) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/{CrossClusterApiKeySignerReloader.java => CrossClusterApiKeySigningConfigReloader.java} (68%) delete mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloaderTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java similarity index 99% rename from x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java rename to x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java index 492ab6ec88d00..f597a69601f90 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigurationReloaderIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java @@ -37,7 +37,7 @@ import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_PATH; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_SECURE_PASSPHRASE; -public class CrossClusterSigningConfigurationReloaderIntegTests extends SecurityIntegTestCase { +public class CrossClusterSigningConfigReloaderIntegTests extends SecurityIntegTestCase { public void testAddAndRemoveClusterConfigsRuntime() throws Exception { addAndRemoveClusterConfigsRuntime(randomClusterAliases(), clusterAlias -> { 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 703b1dda95aaa..0198bdc0ca8fc 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 @@ -418,7 +418,7 @@ import org.elasticsearch.xpack.security.support.SecurityMigrations; import org.elasticsearch.xpack.security.support.SecuritySystemIndices; import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigner; -import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerReloader; +import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningConfigReloader; import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings; import org.elasticsearch.xpack.security.transport.SecurityHttpSettings; import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; @@ -1167,14 +1167,18 @@ Collection createComponents( DestructiveOperations destructiveOperations = new DestructiveOperations(settings, clusterService.getClusterSettings()); crossClusterAccessAuthcService.set(new CrossClusterAccessAuthenticationService(clusterService, apiKeyService, authcService.get())); components.add(crossClusterAccessAuthcService.get()); - var crossClusterApiKeySigner = new CrossClusterApiKeySigner(environment); - components.add(crossClusterApiKeySigner); - var crossClusterApiKeySignerReloader = new CrossClusterApiKeySignerReloader( + + var crossClusterApiKeySignerReloader = new CrossClusterApiKeySigningConfigReloader( + environment, resourceWatcherService, - clusterService.getClusterSettings(), - crossClusterApiKeySigner + clusterService.getClusterSettings() ); components.add(crossClusterApiKeySignerReloader); + + var crossClusterApiKeySigner = new CrossClusterApiKeySigner(environment); + crossClusterApiKeySignerReloader.setApiKeySigner(crossClusterApiKeySigner); + components.add(crossClusterApiKeySigner); + securityInterceptor.set( new SecurityServerTransportInterceptor( settings, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index b993564912f61..fc40c3bb5954a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -42,9 +42,10 @@ import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SETTINGS_PART_SIGNING; public class CrossClusterApiKeySigner { + private static final Map SIGNATURE_ALGORITHM_BY_TYPE = Map.of("RSA", "SHA256withRSA", "EC", "SHA256withECDSA"); + private final Logger logger = LogManager.getLogger(getClass()); private final Environment environment; - private static final Map SIGNATURE_ALGORITHM_BY_TYPE = Map.of("RSA", "SHA256withRSA", "EC", "SHA256withECDSA"); private final Map signingConfigByClusterAlias = new ConcurrentHashMap<>(); public CrossClusterApiKeySigner(Environment environment) { @@ -52,16 +53,8 @@ public CrossClusterApiKeySigner(Environment environment) { loadSigningConfigs(); } - public Map> getDependentFilesToClusterAliases() { - return signingConfigByClusterAlias.entrySet() - .stream() - .filter(entry -> entry.getValue().dependentFiles != null) - .flatMap(entry -> entry.getValue().dependentFiles.stream().map(path -> Map.entry(path, entry.getKey()))) - .collect(Collectors.groupingBy(Map.Entry::getKey, Collectors.mapping(Map.Entry::getValue, Collectors.toSet()))); - } - - public void loadSigningConfig(String clusterAlias, @Nullable Settings settings, boolean updateSecureSettings) { - signingConfigByClusterAlias.compute(clusterAlias, (key, currentSigningConfig) -> { + SigningConfig loadSigningConfig(String clusterAlias, @Nullable Settings settings, boolean updateSecureSettings) { + return signingConfigByClusterAlias.compute(clusterAlias, (key, currentSigningConfig) -> { var effectiveSettings = buildEffectiveSettings( currentSigningConfig != null ? currentSigningConfig.settings : null, settings, @@ -122,10 +115,15 @@ public X509CertificateSignature sign(String clusterAlias, String... headers) { } } + // visible for testing + Map getSigningConfigByClusterAlias() { + return signingConfigByClusterAlias; + } + private void loadSigningConfigs() { this.environment.settings() .getGroups("cluster.remote.", true) - .forEach((alias, settings) -> { loadSigningConfig(alias, settings, false); }); + .forEach((alias, settings) -> loadSigningConfig(alias, settings, false)); } /** @@ -163,10 +161,6 @@ private Settings buildEffectiveSettings( return builder.build(); } - void reloadSigningConfigs(Set clusterAliases) { - clusterAliases.forEach(alias -> loadSigningConfig(alias, null, true)); - } - private X509KeyPair buildKeyPair(SslKeyConfig keyConfig) { final X509KeyManager keyManager = keyConfig.createKeyManager(); if (keyManager == null) { @@ -251,6 +245,6 @@ private record X509KeyPair(X509Certificate certificate, PrivateKey privateKey, S } } - private record SigningConfig(@Nullable X509KeyPair keyPair, @Nullable Collection dependentFiles, Settings settings) {} + record SigningConfig(@Nullable X509KeyPair keyPair, @Nullable Collection dependentFiles, Settings settings) {} } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java similarity index 68% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java index f8276ebcbd7e5..782d61a5a544a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java @@ -8,17 +8,24 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.SecureSettings; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.ssl.SslKeyConfig; +import org.elasticsearch.common.util.set.Sets; +import org.elasticsearch.core.Nullable; import org.elasticsearch.core.Strings; +import org.elasticsearch.env.Environment; import org.elasticsearch.watcher.FileChangesListener; import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.watcher.ResourceWatcherService.Frequency; +import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.security.support.ReloadableSecurityComponent; import java.io.IOException; @@ -26,11 +33,15 @@ import java.nio.file.Path; import java.security.GeneralSecurityException; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.function.Consumer; +import java.util.stream.Collectors; +import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SETTINGS_PART_SIGNING; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.getDynamicSettings; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.getSecureSettings; @@ -40,37 +51,77 @@ * - Reloadable secure settings * - File changes in any of the files pointed to by the cluster settings */ -public final class CrossClusterApiKeySignerReloader implements ReloadableSecurityComponent { +public final class CrossClusterApiKeySigningConfigReloader implements ReloadableSecurityComponent { - private static final Logger logger = LogManager.getLogger(CrossClusterApiKeySignerReloader.class); - private final CrossClusterApiKeySigner apiKeySigner; + private static final Logger logger = LogManager.getLogger(CrossClusterApiKeySigningConfigReloader.class); private final Map monitoredPathToChangeListener = new HashMap<>(); + private final ResourceWatcherService resourceWatcherService; - public CrossClusterApiKeySignerReloader( + private final PlainActionFuture crossClusterApiKeySignerFuture = new PlainActionFuture<>() { + @Override + protected boolean blockingAllowed() { + return true; // waits on the scheduler thread, once, and not for long + } + }; + + public CrossClusterApiKeySigningConfigReloader( + Environment environment, ResourceWatcherService resourceWatcherService, - ClusterSettings clusterSettings, - CrossClusterApiKeySigner apiKeySigner + ClusterSettings clusterSettings ) { - this.apiKeySigner = apiKeySigner; + this.resourceWatcherService = resourceWatcherService; clusterSettings.addAffixGroupUpdateConsumer(getDynamicSettings(), (key, val) -> { - apiKeySigner.loadSigningConfig(key, val.getByPrefix("cluster.remote." + key + "."), false); + reloadConsumer(key, val.getByPrefix("cluster.remote." + key + "."), false); logger.info("Updated signing configuration for [{}] due to updated cluster settings", key); - watchDependentFilesForClusterAliases( - apiKeySigner::reloadSigningConfigs, - resourceWatcherService, - apiKeySigner.getDependentFilesToClusterAliases() - ); }); - watchDependentFilesForClusterAliases( - apiKeySigner::reloadSigningConfigs, - resourceWatcherService, - apiKeySigner.getDependentFilesToClusterAliases() - ); + watchDependentFilesForClusterAliases(resourceWatcherService, getInitialFilesToMonitor(environment)); + } + + private void reloadConsumer(String clusterAlias, @Nullable Settings settings, boolean updateSecureSettings) { + try { + var apiKeySigner = crossClusterApiKeySignerFuture.get(); + var signingConfig = apiKeySigner.loadSigningConfig(clusterAlias, settings, updateSecureSettings); + if (signingConfig.dependentFiles() != null) { + watchDependentFilesForClusterAliases( + resourceWatcherService, + signingConfig.dependentFiles().stream().collect(Collectors.toMap(file -> file, (file) -> Set.of(clusterAlias))) + ); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + throw new ElasticsearchException("Failed to obtain crossClusterApiKeySigner", e); + } + } + + public void setApiKeySigner(CrossClusterApiKeySigner apiKeySigner) { + assert crossClusterApiKeySignerFuture.isDone() == false : "apiKeySigner already set"; + crossClusterApiKeySignerFuture.onResponse(apiKeySigner); + } + + private Map> getInitialFilesToMonitor(Environment environment) { + Map> filesToMonitor = new HashMap<>(); + + var clusterGroups = environment.settings().getGroups("cluster.remote.", true); + + for (var entry : clusterGroups.entrySet()) { + String clusterAlias = entry.getKey(); + Settings settingsForCluster = entry.getValue(); + + SslKeyConfig keyConfig = CertParsingUtils.createKeyConfig(settingsForCluster, SETTINGS_PART_SIGNING + ".", environment, false); + + for (Path path : keyConfig.getDependentFiles()) { + filesToMonitor.compute( + path, + (p, aliases) -> aliases == null ? Set.of(clusterAlias) : Sets.addToCopy(aliases, clusterAlias) + ); + } + } + return filesToMonitor; } private void watchDependentFilesForClusterAliases( - Consumer> reloadConsumer, ResourceWatcherService resourceWatcherService, Map> dependentFilesToClusterAliases ) { @@ -83,8 +134,11 @@ private void watchDependentFilesForClusterAliases( } logger.trace("Adding listener for file [{}] for clusters {}", path, clusterAliases); - - ChangeListener changeListener = new ChangeListener(clusterAliases, path, reloadConsumer); + ChangeListener changeListener = new ChangeListener( + new HashSet<>(clusterAliases), + path, + (clusterAlias) -> this.reloadConsumer(clusterAlias, null, false) + ); FileWatcher fileWatcher = new FileWatcher(path); fileWatcher.addListener(changeListener); try { @@ -96,10 +150,7 @@ private void watchDependentFilesForClusterAliases( }); } - private record ChangeListener(Set clusterAliases, Path file, Consumer> reloadConsumer) - implements - FileChangesListener { - + private record ChangeListener(Set clusterAliases, Path file, Consumer reloadConsumer) implements FileChangesListener { public void addClusterAliases(Set clusterAliases) { this.clusterAliases.addAll(clusterAliases); } @@ -117,7 +168,7 @@ public void onFileDeleted(Path file) { @Override public void onFileChanged(Path file) { if (this.file.equals(file)) { - reloadConsumer.accept(this.clusterAliases); + this.clusterAliases.forEach(reloadConsumer); logger.info("Updated signing configuration for [{}] config(s) due to update of file [{}]", clusterAliases.size(), file); } } @@ -133,7 +184,7 @@ public void reload(Settings settings) { cachedSettings.getGroups("cluster.remote.", true).forEach((clusterAlias, settingsForCluster) -> { // Only update signing config if settings were found, since empty config means config deletion if (settingsForCluster.isEmpty() == false) { - apiKeySigner.loadSigningConfig(clusterAlias, settingsForCluster, true); + reloadConsumer(clusterAlias, settingsForCluster, true); logger.info("Updated signing configuration for [{}] due to reload of secure settings", clusterAlias); } }); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloaderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloaderTests.java deleted file mode 100644 index e1880f5d9a1ec..0000000000000 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerReloaderTests.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * 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.transport; - -import org.elasticsearch.common.settings.ClusterSettings; -import org.elasticsearch.common.settings.MockSecureSettings; -import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.core.TimeValue; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.threadpool.TestThreadPool; -import org.elasticsearch.threadpool.ThreadPool; -import org.elasticsearch.watcher.ResourceWatcherService; -import org.junit.After; - -import java.io.IOException; -import java.nio.file.Files; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -public class CrossClusterApiKeySignerReloaderTests extends ESTestCase { - private CrossClusterApiKeySigner crossClusterApiKeySigner; - private ResourceWatcherService resourceWatcherService; - private ThreadPool threadPool; - - @Override - public void setUp() throws Exception { - super.setUp(); - crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); - Settings settings = Settings.builder().put("resource.reload.interval.high", TimeValue.timeValueMillis(100)).build(); - threadPool = new TestThreadPool(getTestName()); - resourceWatcherService = new ResourceWatcherService(settings, threadPool); - } - - public void testSimpleDynamicSettingsUpdate() throws IOException { - Settings settings = Settings.builder().put("cluster.remote.my_remote.signing.keystore.alias", "mykey").build(); - when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of()); - var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); - - new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); - clusterSettings.applySettings(Settings.builder().put("cluster.remote.my_remote.signing.keystore.alias", "anotherkey").build()); - verify(crossClusterApiKeySigner).loadSigningConfig( - "my_remote", - Settings.builder() - .put("cluster.remote.my_remote.signing.keystore.alias", "anotherkey") - .build() - .getByPrefix("cluster.remote.my_remote."), - false - ); - verify(crossClusterApiKeySigner, times(2)).getDependentFilesToClusterAliases(); - } - - public void testDynamicSettingsUpdateWithAddedFile() throws Exception { - var fileToMonitor = createTempFile(); - Settings settings = Settings.builder().put("cluster.remote.my_remote.signing.keystore.alias", "mykey").build(); - when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of()) - .thenReturn(Map.of(fileToMonitor, Set.of("my_remote"))); - - var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); - new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); - - clusterSettings.applySettings( - Settings.builder() - .put("cluster.remote.my_remote.signing.keystore.alias", "mykey") - .put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor) - .build() - ); - - verify(crossClusterApiKeySigner).loadSigningConfig( - "my_remote", - Settings.builder() - .put("cluster.remote.my_remote.signing.keystore.alias", "mykey") - .put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor) - .build() - .getByPrefix("cluster.remote.my_remote."), - false - ); - verify(crossClusterApiKeySigner, times(2)).getDependentFilesToClusterAliases(); - verify(crossClusterApiKeySigner, times(0)).reloadSigningConfigs(Set.of("my_remote")); - Files.writeString(fileToMonitor, "some content"); - assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).reloadSigningConfigs(Set.of("my_remote"))); - } - - public void testSimpleSecureSettingsReload() { - when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of()); - var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); - var reloader = new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); - - MockSecureSettings secureSettings = new MockSecureSettings(); - secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secret"); - Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); - reloader.reload(settings); - - verify(crossClusterApiKeySigner).loadSigningConfig("my_remote", settings.getByPrefix("cluster.remote.my_remote."), true); - verify(crossClusterApiKeySigner, times(1)).getDependentFilesToClusterAliases(); - } - - public void testSecureSettingsReloadNoMatchingSecureSettings() { - when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of()); - var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); - var reloader = new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); - - MockSecureSettings secureSettings = new MockSecureSettings(); - secureSettings.setString("not.a.setting", "secret"); - Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); - reloader.reload(settings); - - verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(any(), any(), anyBoolean()); - verify(crossClusterApiKeySigner, times(1)).getDependentFilesToClusterAliases(); - } - - public void testFileUpdatedReloaded() throws Exception { - var fileToMonitor = createTempFile(); - Settings settings = Settings.builder().put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build(); - when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of(fileToMonitor, Set.of("my_remote"))); - - var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); - new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); - - verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any(), anyBoolean()); - verify(crossClusterApiKeySigner, times(1)).getDependentFilesToClusterAliases(); - Files.writeString(fileToMonitor, "some content"); - assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).reloadSigningConfigs(Set.of("my_remote"))); - } - - public void testFileDeletedReloaded() throws Exception { - var fileToMonitor = createTempFile(); - Settings settings = Settings.builder().put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build(); - when(crossClusterApiKeySigner.getDependentFilesToClusterAliases()).thenReturn(Map.of(fileToMonitor, Set.of("my_remote"))); - - var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); - new CrossClusterApiKeySignerReloader(resourceWatcherService, clusterSettings, crossClusterApiKeySigner); - - verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any(), anyBoolean()); - verify(crossClusterApiKeySigner, times(1)).getDependentFilesToClusterAliases(); - Files.delete(fileToMonitor); - assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).reloadSigningConfigs(Set.of("my_remote"))); - } - - @After - public void tearDownThreadPool() { - terminate(threadPool); - } - -} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java index f80b332af29fb..cedc0ead7eabc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java @@ -13,9 +13,6 @@ import org.elasticsearch.node.Node; import org.elasticsearch.test.ESTestCase; -import java.util.Map; -import java.util.Set; - public class CrossClusterApiKeySignerTests extends ESTestCase { public void testLoadKeystore() { @@ -61,44 +58,6 @@ public void testLoadSeveralAliasesWithoutAliasSettingKeystore() { assertNull(signer.sign("my_remote", "a_header")); } - public void testGetDependentFilesToClusterAliases() { - var builder = Settings.builder() - .put("cluster.remote.my_remote1.signing.keystore.alias", "wholelottakey") - .put("cluster.remote.my_remote2.signing.keystore.alias", "wholelottakey") - .put( - "cluster.remote.my_remote3.signing.certificate", - getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt") - ) - .put("cluster.remote.my_remote3.signing.key", getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key")) - .put( - "cluster.remote.my_remote4.signing.certificate", - getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt") - ) - .put("cluster.remote.my_remote4.signing.key", getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key")) - .put("path.home", createTempDir()) - .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); - addKeyStorePathToBuilder("my_remote1", builder); - addKeyStorePathToBuilder("my_remote2", builder); - MockSecureSettings secureSettings = new MockSecureSettings(); - secureSettings.setString("cluster.remote.my_remote1.signing.keystore.secure_password", "secretpassword"); - secureSettings.setString("cluster.remote.my_remote2.signing.keystore.secure_password", "secretpassword"); - builder.setSecureSettings(secureSettings); - var signer = new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())); - assertNotNull(signer.sign(randomFrom("my_remote1", "my_remote2", "my_remote3", "my_remote4"), "a_header")); - - assertEquals( - Map.of( - getDataPath("/org/elasticsearch/xpack/security/signature/signing." + (inFipsJvm() ? "bcfks" : "jks")), - Set.of("my_remote1", "my_remote2"), - getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt"), - Set.of("my_remote3", "my_remote4"), - getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key"), - Set.of("my_remote3", "my_remote4") - ), - signer.getDependentFilesToClusterAliases() - ); - } - private void addKeyStorePathToBuilder(String remoteCluster, Settings.Builder builder) { builder.put("cluster.remote." + remoteCluster + ".signing.keystore.type", inFipsJvm() ? "BCFKS" : "PKCS12") .put( diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java new file mode 100644 index 0000000000000..2c12e2e6924a2 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java @@ -0,0 +1,195 @@ +/* + * 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.transport; + +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.core.TimeValue; +import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.node.Node; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.threadpool.TestThreadPool; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.watcher.ResourceWatcherService; +import org.junit.After; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Set; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class CrossClusterApiKeySigningConfigReloaderTests extends ESTestCase { + private CrossClusterApiKeySigner crossClusterApiKeySigner; + private ResourceWatcherService resourceWatcherService; + private ThreadPool threadPool; + private Settings.Builder settingsBuilder; + + @Override + public void setUp() throws Exception { + super.setUp(); + crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); + when(crossClusterApiKeySigner.loadSigningConfig(any(), any(), anyBoolean())).thenReturn( + new CrossClusterApiKeySigner.SigningConfig(null, null, null) + ); + Settings settings = Settings.builder().put("resource.reload.interval.high", TimeValue.timeValueMillis(100)).build(); + threadPool = new TestThreadPool(getTestName()); + resourceWatcherService = new ResourceWatcherService(settings, threadPool); + settingsBuilder = Settings.builder() + .put("path.home", createTempDir()) + .put(Node.NODE_NAME_SETTING.getKey(), randomAlphaOfLengthBetween(3, 8)); + } + + public void testSimpleDynamicSettingsUpdate() throws IOException { + Settings settings = settingsBuilder.put("cluster.remote.my_remote.signing.keystore.alias", "mykey").build(); + + var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + + var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( + TestEnvironment.newEnvironment(settings), + resourceWatcherService, + clusterSettings + ); + crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); + clusterSettings.applySettings(Settings.builder().put("cluster.remote.my_remote.signing.keystore.alias", "anotherkey").build()); + verify(crossClusterApiKeySigner, times(1)).loadSigningConfig( + "my_remote", + Settings.builder() + .put("cluster.remote.my_remote.signing.keystore.alias", "anotherkey") + .build() + .getByPrefix("cluster.remote.my_remote."), + false + ); + } + + public void testDynamicSettingsUpdateWithAddedFiles() throws Exception { + var filesToMonitor = new Path[] { createTempFile(), createTempFile(), createTempFile() }; + Settings settings = settingsBuilder.build(); + + var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( + TestEnvironment.newEnvironment(settings), + resourceWatcherService, + clusterSettings + ); + var crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); + when(crossClusterApiKeySigner.loadSigningConfig(any(), any(), anyBoolean())).thenReturn( + new CrossClusterApiKeySigner.SigningConfig(null, Set.of(filesToMonitor), null) + ); + + crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); + clusterSettings.applySettings( + Settings.builder() + .put("cluster.remote.my_remote0.signing.keystore.alias", "mykey") + .put("cluster.remote.my_remote0.signing.keystore.path", filesToMonitor[0]) + .put("cluster.remote.my_remote1.signing.keystore.alias", "mykey") + .put("cluster.remote.my_remote1.signing.keystore.path", filesToMonitor[1]) + .put("cluster.remote.my_remote2.signing.keystore.alias", "mykey") + .put("cluster.remote.my_remote2.signing.keystore.path", filesToMonitor[2]) + .build() + ); + + for (int i = 0; i < 3; i++) { + final String clusterName = "my_remote" + i; + verify(crossClusterApiKeySigner, times(1)).loadSigningConfig( + clusterName, + Settings.builder() + .put("cluster.remote." + clusterName + ".signing.keystore.alias", "mykey") + .put("cluster.remote." + clusterName + ".signing.keystore.path", filesToMonitor[i]) + .build() + .getByPrefix("cluster.remote." + clusterName + "."), + false + ); + Files.writeString(filesToMonitor[i], "some content"); + assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).loadSigningConfig(clusterName, null, false)); + } + } + + public void testSimpleSecureSettingsReload() { + var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( + TestEnvironment.newEnvironment(settingsBuilder.build()), + resourceWatcherService, + clusterSettings + ); + + crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); + + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secret"); + Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); + crossClusterApiKeySigningConfigReloader.reload(settings); + + verify(crossClusterApiKeySigner).loadSigningConfig("my_remote", settings.getByPrefix("cluster.remote.my_remote."), true); + } + + public void testSecureSettingsReloadNoMatchingSecureSettings() { + var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( + TestEnvironment.newEnvironment(settingsBuilder.build()), + resourceWatcherService, + clusterSettings + ); + crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); + + MockSecureSettings secureSettings = new MockSecureSettings(); + secureSettings.setString("not.a.setting", "secret"); + Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); + crossClusterApiKeySigningConfigReloader.reload(settings); + + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(any(), any(), anyBoolean()); + } + + public void testFileUpdatedReloaded() throws Exception { + var fileToMonitor = createTempFile(); + var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + + var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( + TestEnvironment.newEnvironment(settingsBuilder.put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build()), + resourceWatcherService, + clusterSettings + ); + + crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); + + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any(), anyBoolean()); + Files.writeString(fileToMonitor, "some content"); + assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).loadSigningConfig("my_remote", null, false)); + } + + public void testFileDeletedReloaded() throws Exception { + var fileToMonitor = createTempFile(); + var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); + + var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( + TestEnvironment.newEnvironment(settingsBuilder.put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build()), + resourceWatcherService, + clusterSettings + ); + + crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); + + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any(), anyBoolean()); + Files.delete(fileToMonitor); + assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).loadSigningConfig("my_remote", null, false)); + } + + @After + public void tearDownThreadPool() { + terminate(threadPool); + } + +} From e500b0f84ebc9a0526c3a83d843a795eb439348c Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Tue, 9 Sep 2025 15:51:21 +0200 Subject: [PATCH 12/24] fixup! Do not include private key in exception --- .../xpack/security/transport/CrossClusterApiKeySigner.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index fc40c3bb5954a..f9786ce3824a0 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -237,7 +237,11 @@ private record X509KeyPair(X509Certificate certificate, PrivateKey privateKey, S Optional.ofNullable(SIGNATURE_ALGORITHM_BY_TYPE.get(privateKey.getAlgorithm())) .orElseThrow( () -> new IllegalArgumentException( - "Unsupported Key Type [" + privateKey.getAlgorithm() + "] for [" + privateKey + "]" + "Unsupported Key Type [" + + privateKey.getAlgorithm() + + "] in private key for [" + + certificate.getSubjectX500Principal() + + "]" ) ), calculateFingerprint(certificate) From c9c0b7307fdac656b20af574141cc28d32008e59 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Tue, 9 Sep 2025 13:59:39 +0000 Subject: [PATCH 13/24] [CI] Auto commit changes from spotless --- .../main/java/org/elasticsearch/xpack/security/Security.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0198bdc0ca8fc..391a97e4fb7eb 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 @@ -418,8 +418,8 @@ import org.elasticsearch.xpack.security.support.SecurityMigrations; import org.elasticsearch.xpack.security.support.SecuritySystemIndices; import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigner; -import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningConfigReloader; import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings; +import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningConfigReloader; import org.elasticsearch.xpack.security.transport.SecurityHttpSettings; import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; import org.elasticsearch.xpack.security.transport.filter.IPFilter; From effcde74f9c4b9b58c49e3f2141e64384337f461 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Wed, 10 Sep 2025 14:32:22 +0200 Subject: [PATCH 14/24] fixup! Strict validation on node startup --- .../CrossClusterApiKeySignerIntegTests.java | 4 +- .../transport/CrossClusterApiKeySigner.java | 83 +++----------- ...ossClusterApiKeySigningConfigReloader.java | 60 ++++++++-- .../CrossClusterApiKeySignerTests.java | 17 ++- ...usterApiKeySigningConfigReloaderTests.java | 105 +++++++++++------- 5 files changed, 142 insertions(+), 127 deletions(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java index f6099cb39bfff..94679ad236809 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java @@ -92,14 +92,14 @@ public void testSeveralKeyStoreAliases() { assertNotNull(signature); } - // Add an alias not in the keystore + // Add an alias not in the keystore, settings should silently fail to apply updateClusterSettings( Settings.builder() .put(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), "idonotexist") ); { X509CertificateSignature signature = signer.sign(DYNAMIC_TEST_CLUSTER_ALIAS, "test", "test"); - assertNull(signature); + assertNotNull(signature); } } finally { updateClusterSettings( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index f9786ce3824a0..07f2eb2fd9733 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -10,7 +10,6 @@ import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; -import org.elasticsearch.common.settings.SecureSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslKeyConfig; import org.elasticsearch.common.ssl.SslUtil; @@ -53,43 +52,29 @@ public CrossClusterApiKeySigner(Environment environment) { loadSigningConfigs(); } - SigningConfig loadSigningConfig(String clusterAlias, @Nullable Settings settings, boolean updateSecureSettings) { + SigningConfig loadSigningConfig(String clusterAlias, Settings settings) { return signingConfigByClusterAlias.compute(clusterAlias, (key, currentSigningConfig) -> { - var effectiveSettings = buildEffectiveSettings( - currentSigningConfig != null ? currentSigningConfig.settings : null, - settings, - updateSecureSettings - ); - assert effectiveSettings != null : "Signing config settings must not be null"; - logger.trace("Loading signing config for [{}] with settings [{}]", clusterAlias, effectiveSettings); - - SigningConfig signingConfig = new SigningConfig(null, null, effectiveSettings); - if (effectiveSettings.getByPrefix(SETTINGS_PART_SIGNING).isEmpty() == false) { + logger.trace("Loading signing config for [{}] with settings [{}]", clusterAlias, settings); + if (settings.getByPrefix(SETTINGS_PART_SIGNING).isEmpty() == false) { try { - SslKeyConfig keyConfig = CertParsingUtils.createKeyConfig( - effectiveSettings, - SETTINGS_PART_SIGNING + ".", - environment, - false - ); + SslKeyConfig keyConfig = CertParsingUtils.createKeyConfig(settings, SETTINGS_PART_SIGNING + ".", environment, false); if (keyConfig.hasKeyMaterial()) { - String alias = effectiveSettings.get(SETTINGS_PART_SIGNING + "." + KEYSTORE_ALIAS_SUFFIX); + String alias = settings.get(SETTINGS_PART_SIGNING + "." + KEYSTORE_ALIAS_SUFFIX); var keyPair = Strings.isNullOrEmpty(alias) ? buildKeyPair(keyConfig) : buildKeyPair(keyConfig, alias); if (keyPair != null) { logger.trace("Key pair [{}] found for [{}]", keyPair, clusterAlias); - signingConfig = new SigningConfig(keyPair, keyConfig.getDependentFiles(), effectiveSettings); + return new SigningConfig(keyPair, keyConfig.getDependentFiles()); } } else { logger.error(Strings.format("No signing credentials found in signing config for cluster [%s]", clusterAlias)); } } catch (Exception e) { - // Since this can be called by the settings applier we don't want to surface an error here - logger.error(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); + throw new IllegalStateException(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); } - } else { - logger.trace("No signing settings found for [{}]", clusterAlias); } - return signingConfig; + + logger.trace("No valid signing config settings found for [{}] with settings [{}]", clusterAlias, settings); + return SigningConfig.EMPTY; }); } @@ -121,44 +106,7 @@ Map getSigningConfigByClusterAlias() { } private void loadSigningConfigs() { - this.environment.settings() - .getGroups("cluster.remote.", true) - .forEach((alias, settings) -> loadSigningConfig(alias, settings, false)); - } - - /** - * Build the effective remote cluster settings by merging the currently configured (if any) and new/updated settings - *

- * - If newSettings is null - use existing settings, used to refresh the dependent files - * - If newSettings is empty - return empty settings, used for resetting signing config - * - If updateSecureSettings is true - merge secure settings from newSettings with current settings, used by secure settings refresh - * - If updateSecureSettings is false - merge new settings with existing secure settings, used for regular settings update - */ - private Settings buildEffectiveSettings( - @Nullable Settings currentSettings, - @Nullable Settings newSettings, - boolean updateSecureSettings - ) { - if (currentSettings == null) { - return newSettings == null ? Settings.EMPTY : newSettings; - } - if (newSettings == null) { - return currentSettings; - } - if (newSettings.isEmpty()) { - return Settings.EMPTY; - } - - Settings secureSettingsSource = updateSecureSettings ? newSettings : currentSettings; - Settings settingsSource = updateSecureSettings ? currentSettings : newSettings; - - SecureSettings secureSettings = Settings.builder().put(secureSettingsSource, true).getSecureSettings(); - - var builder = Settings.builder().put(settingsSource, false); - if (secureSettings != null) { - builder.setSecureSettings(secureSettings); - } - return builder.build(); + this.environment.settings().getGroups("cluster.remote.", true).forEach(this::loadSigningConfig); } private X509KeyPair buildKeyPair(SslKeyConfig keyConfig) { @@ -213,8 +161,11 @@ private X509KeyPair buildKeyPair(SslKeyConfig keyConfig, String alias) { final X509Certificate[] chain = keyManager.getCertificateChain(alias); logger.trace("KeyConfig [{}] has entry for alias: [{}] [{}]", keyConfig, alias, chain != null); + if (chain == null) { + throw new IllegalStateException("Key config missing certificate chain for alias [" + alias + "]"); + } - return chain != null ? new X509KeyPair(chain[0], keyManager.getPrivateKey(alias)) : null; + return new X509KeyPair(chain[0], keyManager.getPrivateKey(alias)); } private static byte[] getSignableBytes(final String... headers) { @@ -249,6 +200,8 @@ private record X509KeyPair(X509Certificate certificate, PrivateKey privateKey, S } } - record SigningConfig(@Nullable X509KeyPair keyPair, @Nullable Collection dependentFiles, Settings settings) {} + record SigningConfig(@Nullable X509KeyPair keyPair, @Nullable Collection dependentFiles) { + static SigningConfig EMPTY = new SigningConfig(null, null); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java index 782d61a5a544a..5c19a5e120a13 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java @@ -37,6 +37,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -56,6 +57,7 @@ public final class CrossClusterApiKeySigningConfigReloader implements Reloadable private static final Logger logger = LogManager.getLogger(CrossClusterApiKeySigningConfigReloader.class); private final Map monitoredPathToChangeListener = new HashMap<>(); private final ResourceWatcherService resourceWatcherService; + private final Map settingsByClusterAlias = new ConcurrentHashMap<>(); private final PlainActionFuture crossClusterApiKeySignerFuture = new PlainActionFuture<>() { @Override @@ -70,18 +72,22 @@ public CrossClusterApiKeySigningConfigReloader( ClusterSettings clusterSettings ) { this.resourceWatcherService = resourceWatcherService; + settingsByClusterAlias.putAll(environment.settings().getGroups("cluster.remote.", true)); + watchDependentFilesForClusterAliases(resourceWatcherService, getInitialFilesToMonitor(environment)); clusterSettings.addAffixGroupUpdateConsumer(getDynamicSettings(), (key, val) -> { reloadConsumer(key, val.getByPrefix("cluster.remote." + key + "."), false); logger.info("Updated signing configuration for [{}] due to updated cluster settings", key); }); - - watchDependentFilesForClusterAliases(resourceWatcherService, getInitialFilesToMonitor(environment)); } private void reloadConsumer(String clusterAlias, @Nullable Settings settings, boolean updateSecureSettings) { try { var apiKeySigner = crossClusterApiKeySignerFuture.get(); - var signingConfig = apiKeySigner.loadSigningConfig(clusterAlias, settings, updateSecureSettings); + Settings effectiveSettings = settingsByClusterAlias.compute( + clusterAlias, + (key, val) -> buildEffectiveSettings(val, settings, updateSecureSettings) + ); + var signingConfig = apiKeySigner.loadSigningConfig(clusterAlias, effectiveSettings); if (signingConfig.dependentFiles() != null) { watchDependentFilesForClusterAliases( resourceWatcherService, @@ -92,6 +98,8 @@ private void reloadConsumer(String clusterAlias, @Nullable Settings settings, bo Thread.currentThread().interrupt(); } catch (ExecutionException e) { throw new ElasticsearchException("Failed to obtain crossClusterApiKeySigner", e); + } catch (IllegalStateException e) { + logger.error(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); } } @@ -102,22 +110,15 @@ public void setApiKeySigner(CrossClusterApiKeySigner apiKeySigner) { private Map> getInitialFilesToMonitor(Environment environment) { Map> filesToMonitor = new HashMap<>(); - - var clusterGroups = environment.settings().getGroups("cluster.remote.", true); - - for (var entry : clusterGroups.entrySet()) { - String clusterAlias = entry.getKey(); - Settings settingsForCluster = entry.getValue(); - + this.settingsByClusterAlias.forEach((clusterAlias, settingsForCluster) -> { SslKeyConfig keyConfig = CertParsingUtils.createKeyConfig(settingsForCluster, SETTINGS_PART_SIGNING + ".", environment, false); - for (Path path : keyConfig.getDependentFiles()) { filesToMonitor.compute( path, (p, aliases) -> aliases == null ? Set.of(clusterAlias) : Sets.addToCopy(aliases, clusterAlias) ); } - } + }); return filesToMonitor; } @@ -174,6 +175,41 @@ public void onFileChanged(Path file) { } } + /** + * Build the effective remote cluster settings by merging the currently configured (if any) and new/updated settings + *

+ * - If newSettings is null - use existing settings, used to refresh the dependent files + * - If newSettings is empty - return empty settings, used for resetting signing config + * - If updateSecureSettings is true - merge secure settings from newSettings with current settings, used by secure settings refresh + * - If updateSecureSettings is false - merge new settings with existing secure settings, used for regular settings update + */ + private Settings buildEffectiveSettings( + @Nullable Settings currentSettings, + @Nullable Settings newSettings, + boolean updateSecureSettings + ) { + if (currentSettings == null) { + return newSettings == null ? Settings.EMPTY : newSettings; + } + if (newSettings == null) { + return currentSettings; + } + if (newSettings.isEmpty()) { + return Settings.EMPTY; + } + + Settings secureSettingsSource = updateSecureSettings ? newSettings : currentSettings; + Settings settingsSource = updateSecureSettings ? currentSettings : newSettings; + + SecureSettings secureSettings = Settings.builder().put(secureSettingsSource, true).getSecureSettings(); + + var builder = Settings.builder().put(settingsSource, false); + if (secureSettings != null) { + builder.setSecureSettings(secureSettings); + } + return builder.build(); + } + @Override public void reload(Settings settings) { try { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java index cedc0ead7eabc..abde62f49304b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerTests.java @@ -13,6 +13,8 @@ import org.elasticsearch.node.Node; import org.elasticsearch.test.ESTestCase; +import static org.hamcrest.Matchers.equalTo; + public class CrossClusterApiKeySignerTests extends ESTestCase { public void testLoadKeystore() { @@ -39,9 +41,12 @@ public void testLoadKeystoreMissingFile() { MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secretpassword"); builder.setSecureSettings(secureSettings); - var signer = new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())); + var exception = assertThrows( + IllegalStateException.class, + () -> new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())) + ); + assertThat(exception.getMessage(), equalTo("Failed to load signing config for cluster [my_remote]")); - assertNull(signer.sign("my_remote", "a_header")); } public void testLoadSeveralAliasesWithoutAliasSettingKeystore() { @@ -53,9 +58,11 @@ public void testLoadSeveralAliasesWithoutAliasSettingKeystore() { MockSecureSettings secureSettings = new MockSecureSettings(); secureSettings.setString("cluster.remote.my_remote.signing.keystore.secure_password", "secretpassword"); builder.setSecureSettings(secureSettings); - var signer = new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())); - - assertNull(signer.sign("my_remote", "a_header")); + var exception = assertThrows( + IllegalStateException.class, + () -> new CrossClusterApiKeySigner(TestEnvironment.newEnvironment(builder.build())) + ); + assertThat(exception.getMessage(), equalTo("Failed to load signing config for cluster [my_remote]")); } private void addKeyStorePathToBuilder(String remoteCluster, Settings.Builder builder) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java index 2c12e2e6924a2..99d19bf77cfa5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java @@ -25,7 +25,6 @@ import java.util.Set; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -42,9 +41,7 @@ public class CrossClusterApiKeySigningConfigReloaderTests extends ESTestCase { public void setUp() throws Exception { super.setUp(); crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); - when(crossClusterApiKeySigner.loadSigningConfig(any(), any(), anyBoolean())).thenReturn( - new CrossClusterApiKeySigner.SigningConfig(null, null, null) - ); + when(crossClusterApiKeySigner.loadSigningConfig(any(), any())).thenReturn(new CrossClusterApiKeySigner.SigningConfig(null, null)); Settings settings = Settings.builder().put("resource.reload.interval.high", TimeValue.timeValueMillis(100)).build(); threadPool = new TestThreadPool(getTestName()); resourceWatcherService = new ResourceWatcherService(settings, threadPool); @@ -70,51 +67,63 @@ public void testSimpleDynamicSettingsUpdate() throws IOException { Settings.builder() .put("cluster.remote.my_remote.signing.keystore.alias", "anotherkey") .build() - .getByPrefix("cluster.remote.my_remote."), - false + .getByPrefix("cluster.remote.my_remote.") ); } public void testDynamicSettingsUpdateWithAddedFiles() throws Exception { + var clusterNames = new String[] { "my_remote0", "my_remote1", "my_remote2" }; var filesToMonitor = new Path[] { createTempFile(), createTempFile(), createTempFile() }; - Settings settings = settingsBuilder.build(); + var remoteClusterSettings = new Settings[filesToMonitor.length]; + var dynamicSettingsUpdate = Settings.builder() + .put("cluster.remote.my_remote0.signing.keystore.alias", "mykey") + .put("cluster.remote.my_remote0.signing.keystore.path", filesToMonitor[0]) + .put("cluster.remote.my_remote1.signing.keystore.alias", "mykey") + .put("cluster.remote.my_remote1.signing.keystore.path", filesToMonitor[1]) + .put("cluster.remote.my_remote2.signing.keystore.alias", "mykey") + .put("cluster.remote.my_remote2.signing.keystore.path", filesToMonitor[2]) + .build(); + + var clusterSettings = new ClusterSettings( + settingsBuilder.build(), + new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings()) + ); - var clusterSettings = new ClusterSettings(settings, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( - TestEnvironment.newEnvironment(settings), + TestEnvironment.newEnvironment(settingsBuilder.build()), resourceWatcherService, clusterSettings ); var crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); - when(crossClusterApiKeySigner.loadSigningConfig(any(), any(), anyBoolean())).thenReturn( - new CrossClusterApiKeySigner.SigningConfig(null, Set.of(filesToMonitor), null) - ); + + for (int i = 0; i < clusterNames.length; i++) { + remoteClusterSettings[i] = Settings.builder() + .put("cluster.remote." + clusterNames[i] + ".signing.keystore.alias", "mykey") + .put("cluster.remote." + clusterNames[i] + ".signing.keystore.path", filesToMonitor[i]) + .build(); + when( + crossClusterApiKeySigner.loadSigningConfig( + clusterNames[i], + remoteClusterSettings[i].getByPrefix("cluster.remote." + clusterNames[i] + ".") + ) + ).thenReturn(new CrossClusterApiKeySigner.SigningConfig(null, Set.of(filesToMonitor[i]))); + when( + crossClusterApiKeySigner.loadSigningConfig( + clusterNames[i], + dynamicSettingsUpdate.getByPrefix("cluster.remote." + clusterNames[i] + ".") + ) + ).thenReturn(new CrossClusterApiKeySigner.SigningConfig(null, Set.of(filesToMonitor[i]))); + } crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); - clusterSettings.applySettings( - Settings.builder() - .put("cluster.remote.my_remote0.signing.keystore.alias", "mykey") - .put("cluster.remote.my_remote0.signing.keystore.path", filesToMonitor[0]) - .put("cluster.remote.my_remote1.signing.keystore.alias", "mykey") - .put("cluster.remote.my_remote1.signing.keystore.path", filesToMonitor[1]) - .put("cluster.remote.my_remote2.signing.keystore.alias", "mykey") - .put("cluster.remote.my_remote2.signing.keystore.path", filesToMonitor[2]) - .build() - ); + clusterSettings.applySettings(dynamicSettingsUpdate); - for (int i = 0; i < 3; i++) { + for (int i = 0; i < clusterNames.length; i++) { final String clusterName = "my_remote" + i; - verify(crossClusterApiKeySigner, times(1)).loadSigningConfig( - clusterName, - Settings.builder() - .put("cluster.remote." + clusterName + ".signing.keystore.alias", "mykey") - .put("cluster.remote." + clusterName + ".signing.keystore.path", filesToMonitor[i]) - .build() - .getByPrefix("cluster.remote." + clusterName + "."), - false - ); + var remoteClusterSetting = remoteClusterSettings[i].getByPrefix("cluster.remote." + clusterName + "."); + verify(crossClusterApiKeySigner, times(1)).loadSigningConfig(clusterName, remoteClusterSetting); Files.writeString(filesToMonitor[i], "some content"); - assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).loadSigningConfig(clusterName, null, false)); + assertBusy(() -> verify(crossClusterApiKeySigner, times(2)).loadSigningConfig(clusterName, remoteClusterSetting)); } } @@ -133,7 +142,7 @@ public void testSimpleSecureSettingsReload() { Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); crossClusterApiKeySigningConfigReloader.reload(settings); - verify(crossClusterApiKeySigner).loadSigningConfig("my_remote", settings.getByPrefix("cluster.remote.my_remote."), true); + verify(crossClusterApiKeySigner).loadSigningConfig("my_remote", settings.getByPrefix("cluster.remote.my_remote.")); } public void testSecureSettingsReloadNoMatchingSecureSettings() { @@ -150,41 +159,51 @@ public void testSecureSettingsReloadNoMatchingSecureSettings() { Settings settings = Settings.builder().setSecureSettings(secureSettings).build(); crossClusterApiKeySigningConfigReloader.reload(settings); - verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(any(), any(), anyBoolean()); + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(any(), any()); } public void testFileUpdatedReloaded() throws Exception { var fileToMonitor = createTempFile(); var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); - + var initialSettings = settingsBuilder.put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build(); var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( - TestEnvironment.newEnvironment(settingsBuilder.put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build()), + TestEnvironment.newEnvironment(initialSettings), resourceWatcherService, clusterSettings ); crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); - verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any(), anyBoolean()); + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any()); Files.writeString(fileToMonitor, "some content"); - assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).loadSigningConfig("my_remote", null, false)); + assertBusy( + () -> verify(crossClusterApiKeySigner, times(1)).loadSigningConfig( + "my_remote", + initialSettings.getByPrefix("cluster.remote.my_remote.") + ) + ); } public void testFileDeletedReloaded() throws Exception { var fileToMonitor = createTempFile(); var clusterSettings = new ClusterSettings(Settings.EMPTY, new HashSet<>(CrossClusterApiKeySignerSettings.getDynamicSettings())); - + var initialSettings = settingsBuilder.put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build(); var crossClusterApiKeySigningConfigReloader = new CrossClusterApiKeySigningConfigReloader( - TestEnvironment.newEnvironment(settingsBuilder.put("cluster.remote.my_remote.signing.keystore.path", fileToMonitor).build()), + TestEnvironment.newEnvironment(initialSettings), resourceWatcherService, clusterSettings ); crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); - verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any(), anyBoolean()); + verify(crossClusterApiKeySigner, times(0)).loadSigningConfig(anyString(), any()); Files.delete(fileToMonitor); - assertBusy(() -> verify(crossClusterApiKeySigner, times(1)).loadSigningConfig("my_remote", null, false)); + assertBusy( + () -> verify(crossClusterApiKeySigner, times(1)).loadSigningConfig( + "my_remote", + initialSettings.getByPrefix("cluster.remote.my_remote.") + ) + ); } @After From 8f3f4c6b5038a8f93f432efbdca6891567a46bc0 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Wed, 10 Sep 2025 12:43:52 +0000 Subject: [PATCH 15/24] [CI] Auto commit changes from spotless --- .../main/java/org/elasticsearch/xpack/security/Security.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fbac6a986e16c..cb1174ce0a5e7 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 @@ -418,10 +418,10 @@ import org.elasticsearch.xpack.security.support.ReloadableSecurityComponent; import org.elasticsearch.xpack.security.support.SecurityMigrations; import org.elasticsearch.xpack.security.support.SecuritySystemIndices; +import org.elasticsearch.xpack.security.transport.CrossClusterAccessTransportInterceptor; import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigner; import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings; import org.elasticsearch.xpack.security.transport.CrossClusterApiKeySigningConfigReloader; -import org.elasticsearch.xpack.security.transport.CrossClusterAccessTransportInterceptor; import org.elasticsearch.xpack.security.transport.RemoteClusterTransportInterceptor; import org.elasticsearch.xpack.security.transport.SecurityHttpSettings; import org.elasticsearch.xpack.security.transport.SecurityServerTransportInterceptor; From 558ad026a6f1482252c48e3bb629039299a49132 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 11 Sep 2025 09:43:11 +0200 Subject: [PATCH 16/24] fixup! handle race condition and concurrency bug --- ...ossClusterApiKeySigningConfigReloader.java | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java index 5c19a5e120a13..4691314b89acc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java @@ -55,7 +55,7 @@ public final class CrossClusterApiKeySigningConfigReloader implements ReloadableSecurityComponent { private static final Logger logger = LogManager.getLogger(CrossClusterApiKeySigningConfigReloader.class); - private final Map monitoredPathToChangeListener = new HashMap<>(); + private final Map monitoredPathToChangeListener = new ConcurrentHashMap<>(); private final ResourceWatcherService resourceWatcherService; private final Map settingsByClusterAlias = new ConcurrentHashMap<>(); @@ -127,27 +127,29 @@ private void watchDependentFilesForClusterAliases( Map> dependentFilesToClusterAliases ) { dependentFilesToClusterAliases.forEach((path, clusterAliases) -> { - var existingChangeListener = monitoredPathToChangeListener.get(path); - if (existingChangeListener != null) { - logger.trace("Found existing listener for file [{}], adding clusterAliases {}", path, clusterAliases); - existingChangeListener.addClusterAliases(clusterAliases); - return; - } + monitoredPathToChangeListener.compute(path, (monitoredPath, existingChangeListener) -> { + if (existingChangeListener != null) { + logger.trace("Found existing listener for file [{}], adding clusterAliases {}", path, clusterAliases); + existingChangeListener.addClusterAliases(clusterAliases); + return existingChangeListener; + } - logger.trace("Adding listener for file [{}] for clusters {}", path, clusterAliases); - ChangeListener changeListener = new ChangeListener( - new HashSet<>(clusterAliases), - path, - (clusterAlias) -> this.reloadConsumer(clusterAlias, null, false) - ); - FileWatcher fileWatcher = new FileWatcher(path); - fileWatcher.addListener(changeListener); - try { - resourceWatcherService.add(fileWatcher, Frequency.HIGH); - monitoredPathToChangeListener.put(path, changeListener); - } catch (IOException | SecurityException e) { - logger.error(Strings.format("failed to start watching file [%s]", path), e); - } + logger.trace("Adding listener for file [{}] for clusters {}", path, clusterAliases); + ChangeListener changeListener = new ChangeListener( + new HashSet<>(clusterAliases), + path, + (clusterAlias) -> this.reloadConsumer(clusterAlias, null, false) + ); + FileWatcher fileWatcher = new FileWatcher(path); + fileWatcher.addListener(changeListener); + try { + resourceWatcherService.add(fileWatcher, Frequency.HIGH); + return changeListener; + } catch (IOException | SecurityException e) { + logger.error(Strings.format("failed to start watching file [%s]", path), e); + } + return changeListener; + }); }); } From 04257b718469718c18b2695f6790166edef2bab5 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 11 Sep 2025 11:00:12 +0200 Subject: [PATCH 17/24] fixup! Simplify after refactor --- ...lusterSigningConfigReloaderIntegTests.java | 24 +++++++- .../transport/CrossClusterApiKeySigner.java | 56 +++++++++---------- ...ossClusterApiKeySigningConfigReloader.java | 28 +++++----- ...usterApiKeySigningConfigReloaderTests.java | 23 +++++++- 4 files changed, 85 insertions(+), 46 deletions(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java index f597a69601f90..cd62af3f699c3 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java @@ -69,7 +69,6 @@ public void testAddSecureSettingsConfigRuntime() throws Exception { "secretpassword".toCharArray() ) ); - updateClusterSettings( Settings.builder() .put( @@ -92,6 +91,9 @@ public void testAddSecureSettingsConfigRuntime() throws Exception { .putNull(SIGNING_KEYSTORE_ALGORITHM.getConcreteSettingForNamespace(clusterAlias).getKey()) .setSecureSettings(new MockSecureSettings()) ); + removeSecureSettingsFromKeyStoreAndReload( + Set.of(SIGNING_KEYSTORE_SECURE_PASSWORD.getConcreteSettingForNamespace(clusterAlias).getKey()) + ); }); } @@ -148,6 +150,9 @@ public void testDependentKeyConfigFilesUpdated() throws Exception { .putNull(SIGNING_KEY_PATH.getConcreteSettingForNamespace(testClusterAlias).getKey()) .setSecureSettings(new MockSecureSettings()) ); + removeSecureSettingsFromKeyStoreAndReload( + Set.of(SIGNING_KEYSTORE_SECURE_PASSWORD.getConcreteSettingForNamespace(testClusterAlias).getKey()) + ); } } @@ -251,6 +256,23 @@ private void writeSecureSettingsToKeyStoreAndReload(Map entries) future.actionGet(); } + private void removeSecureSettingsFromKeyStoreAndReload(Set settingsToRemove) { + char[] keyStorePassword = randomAlphaOfLengthBetween(15, randomIntBetween(15, 20)).toCharArray(); + internalCluster().getInstances(Environment.class).forEach(environment -> { + final KeyStoreWrapper keyStoreWrapper = KeyStoreWrapper.create(); + settingsToRemove.forEach(keyStoreWrapper::remove); + try { + keyStoreWrapper.save(environment.configDir(), keyStorePassword, false); + logger.info(keyStoreWrapper.toString()); + } catch (Exception e) { + fail(e.getMessage()); + } + }); + PlainActionFuture future = new PlainActionFuture<>(); + reloadSecureSettings(keyStorePassword, future); + future.actionGet(); + } + private static void reloadSecureSettings(char[] password, ActionListener listener) { final var request = new NodesReloadSecureSettingsRequest(new String[0]); try { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index 07f2eb2fd9733..cddbba3bf3d58 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslKeyConfig; import org.elasticsearch.common.ssl.SslUtil; -import org.elasticsearch.core.Nullable; import org.elasticsearch.env.Environment; import org.elasticsearch.logging.LogManager; import org.elasticsearch.logging.Logger; @@ -53,29 +52,29 @@ public CrossClusterApiKeySigner(Environment environment) { } SigningConfig loadSigningConfig(String clusterAlias, Settings settings) { - return signingConfigByClusterAlias.compute(clusterAlias, (key, currentSigningConfig) -> { - logger.trace("Loading signing config for [{}] with settings [{}]", clusterAlias, settings); - if (settings.getByPrefix(SETTINGS_PART_SIGNING).isEmpty() == false) { - try { - SslKeyConfig keyConfig = CertParsingUtils.createKeyConfig(settings, SETTINGS_PART_SIGNING + ".", environment, false); - if (keyConfig.hasKeyMaterial()) { - String alias = settings.get(SETTINGS_PART_SIGNING + "." + KEYSTORE_ALIAS_SUFFIX); - var keyPair = Strings.isNullOrEmpty(alias) ? buildKeyPair(keyConfig) : buildKeyPair(keyConfig, alias); - if (keyPair != null) { - logger.trace("Key pair [{}] found for [{}]", keyPair, clusterAlias); - return new SigningConfig(keyPair, keyConfig.getDependentFiles()); - } - } else { - logger.error(Strings.format("No signing credentials found in signing config for cluster [%s]", clusterAlias)); + logger.trace("Loading signing config for [{}] with settings [{}]", clusterAlias, settings); + if (settings.getByPrefix(SETTINGS_PART_SIGNING).isEmpty() == false) { + try { + SslKeyConfig keyConfig = CertParsingUtils.createKeyConfig(settings, SETTINGS_PART_SIGNING + ".", environment, false); + if (keyConfig.hasKeyMaterial()) { + String alias = settings.get(SETTINGS_PART_SIGNING + "." + KEYSTORE_ALIAS_SUFFIX); + var keyPair = Strings.isNullOrEmpty(alias) ? buildKeyPair(keyConfig) : buildKeyPair(keyConfig, alias); + if (keyPair != null) { + logger.trace("Key pair [{}] found for [{}]", keyPair, clusterAlias); + var signingConfig = new SigningConfig(keyPair, keyConfig.getDependentFiles()); + signingConfigByClusterAlias.put(clusterAlias, signingConfig); + return signingConfig; } - } catch (Exception e) { - throw new IllegalStateException(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); + } else { + logger.error(Strings.format("No signing credentials found in signing config for cluster [%s]", clusterAlias)); } + } catch (Exception e) { + throw new IllegalStateException(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); } - - logger.trace("No valid signing config settings found for [{}] with settings [{}]", clusterAlias, settings); - return SigningConfig.EMPTY; - }); + } + logger.trace("No valid signing config settings found for [{}] with settings [{}]", clusterAlias, settings); + signingConfigByClusterAlias.remove(clusterAlias); + return null; } public X509CertificateSignature sign(String clusterAlias, String... headers) { @@ -100,11 +99,6 @@ public X509CertificateSignature sign(String clusterAlias, String... headers) { } } - // visible for testing - Map getSigningConfigByClusterAlias() { - return signingConfigByClusterAlias; - } - private void loadSigningConfigs() { this.environment.settings().getGroups("cluster.remote.", true).forEach(this::loadSigningConfig); } @@ -180,7 +174,8 @@ private static String calculateFingerprint(X509Certificate certificate) { } } - private record X509KeyPair(X509Certificate certificate, PrivateKey privateKey, String signatureAlgorithm, String fingerprint) { + // visible for testing + record X509KeyPair(X509Certificate certificate, PrivateKey privateKey, String signatureAlgorithm, String fingerprint) { X509KeyPair(X509Certificate certificate, PrivateKey privateKey) { this( Objects.requireNonNull(certificate), @@ -200,8 +195,11 @@ private record X509KeyPair(X509Certificate certificate, PrivateKey privateKey, S } } - record SigningConfig(@Nullable X509KeyPair keyPair, @Nullable Collection dependentFiles) { - static SigningConfig EMPTY = new SigningConfig(null, null); + record SigningConfig(X509KeyPair keyPair, Collection dependentFiles) { + public SigningConfig { + Objects.requireNonNull(keyPair); + Objects.requireNonNull(dependentFiles); + } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java index 4691314b89acc..c84ae2bbb6d9b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java @@ -83,23 +83,25 @@ public CrossClusterApiKeySigningConfigReloader( private void reloadConsumer(String clusterAlias, @Nullable Settings settings, boolean updateSecureSettings) { try { var apiKeySigner = crossClusterApiKeySignerFuture.get(); - Settings effectiveSettings = settingsByClusterAlias.compute( - clusterAlias, - (key, val) -> buildEffectiveSettings(val, settings, updateSecureSettings) - ); - var signingConfig = apiKeySigner.loadSigningConfig(clusterAlias, effectiveSettings); - if (signingConfig.dependentFiles() != null) { - watchDependentFilesForClusterAliases( - resourceWatcherService, - signingConfig.dependentFiles().stream().collect(Collectors.toMap(file -> file, (file) -> Set.of(clusterAlias))) - ); - } + settingsByClusterAlias.compute(clusterAlias, (key, val) -> { + var effectiveSettings = buildEffectiveSettings(val, settings, updateSecureSettings); + try { + var signingConfig = apiKeySigner.loadSigningConfig(clusterAlias, effectiveSettings); + if (signingConfig != null) { + watchDependentFilesForClusterAliases( + resourceWatcherService, + signingConfig.dependentFiles().stream().collect(Collectors.toMap(file -> file, (file) -> Set.of(clusterAlias))) + ); + } + } catch (IllegalStateException e) { + logger.error(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); + } + return effectiveSettings; + }); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } catch (ExecutionException e) { throw new ElasticsearchException("Failed to obtain crossClusterApiKeySigner", e); - } catch (IllegalStateException e) { - logger.error(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java index 99d19bf77cfa5..6c79dbf348226 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java @@ -21,7 +21,11 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.security.PrivateKey; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; import java.util.HashSet; +import java.util.List; import java.util.Set; import static org.mockito.ArgumentMatchers.any; @@ -36,12 +40,16 @@ public class CrossClusterApiKeySigningConfigReloaderTests extends ESTestCase { private ResourceWatcherService resourceWatcherService; private ThreadPool threadPool; private Settings.Builder settingsBuilder; + private CrossClusterApiKeySigner.X509KeyPair testKeyPair; @Override public void setUp() throws Exception { super.setUp(); crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); - when(crossClusterApiKeySigner.loadSigningConfig(any(), any())).thenReturn(new CrossClusterApiKeySigner.SigningConfig(null, null)); + testKeyPair = createTestKeyPair(); + when(crossClusterApiKeySigner.loadSigningConfig(any(), any())).thenReturn( + new CrossClusterApiKeySigner.SigningConfig(testKeyPair, List.of()) + ); Settings settings = Settings.builder().put("resource.reload.interval.high", TimeValue.timeValueMillis(100)).build(); threadPool = new TestThreadPool(getTestName()); resourceWatcherService = new ResourceWatcherService(settings, threadPool); @@ -106,13 +114,13 @@ public void testDynamicSettingsUpdateWithAddedFiles() throws Exception { clusterNames[i], remoteClusterSettings[i].getByPrefix("cluster.remote." + clusterNames[i] + ".") ) - ).thenReturn(new CrossClusterApiKeySigner.SigningConfig(null, Set.of(filesToMonitor[i]))); + ).thenReturn(new CrossClusterApiKeySigner.SigningConfig(testKeyPair, Set.of(filesToMonitor[i]))); when( crossClusterApiKeySigner.loadSigningConfig( clusterNames[i], dynamicSettingsUpdate.getByPrefix("cluster.remote." + clusterNames[i] + ".") ) - ).thenReturn(new CrossClusterApiKeySigner.SigningConfig(null, Set.of(filesToMonitor[i]))); + ).thenReturn(new CrossClusterApiKeySigner.SigningConfig(testKeyPair, Set.of(filesToMonitor[i]))); } crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); @@ -206,6 +214,15 @@ public void testFileDeletedReloaded() throws Exception { ); } + private CrossClusterApiKeySigner.X509KeyPair createTestKeyPair() throws CertificateEncodingException { + var certMock = mock(X509Certificate.class); + when(certMock.getEncoded()).thenReturn(new byte[0]); + var privateKeyMock = mock(PrivateKey.class); + when(privateKeyMock.getAlgorithm()).thenReturn(randomFrom("RSA", "EC")); + + return new CrossClusterApiKeySigner.X509KeyPair(certMock, privateKeyMock); + } + @After public void tearDownThreadPool() { terminate(threadPool); From fad4c4198f850f3be8999b1b1b819e1bc5e74250 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 11 Sep 2025 11:20:01 +0200 Subject: [PATCH 18/24] Simplify further --- .../transport/CrossClusterApiKeySigner.java | 38 +++++++++---------- ...ossClusterApiKeySigningConfigReloader.java | 10 ++--- ...usterApiKeySigningConfigReloaderTests.java | 7 ++-- 3 files changed, 26 insertions(+), 29 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index cddbba3bf3d58..8ae95ea873ac5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -51,20 +51,26 @@ public CrossClusterApiKeySigner(Environment environment) { loadSigningConfigs(); } - SigningConfig loadSigningConfig(String clusterAlias, Settings settings) { + Optional loadSigningConfig(String clusterAlias, Settings settings) { logger.trace("Loading signing config for [{}] with settings [{}]", clusterAlias, settings); if (settings.getByPrefix(SETTINGS_PART_SIGNING).isEmpty() == false) { try { SslKeyConfig keyConfig = CertParsingUtils.createKeyConfig(settings, SETTINGS_PART_SIGNING + ".", environment, false); if (keyConfig.hasKeyMaterial()) { String alias = settings.get(SETTINGS_PART_SIGNING + "." + KEYSTORE_ALIAS_SUFFIX); - var keyPair = Strings.isNullOrEmpty(alias) ? buildKeyPair(keyConfig) : buildKeyPair(keyConfig, alias); - if (keyPair != null) { - logger.trace("Key pair [{}] found for [{}]", keyPair, clusterAlias); - var signingConfig = new SigningConfig(keyPair, keyConfig.getDependentFiles()); - signingConfigByClusterAlias.put(clusterAlias, signingConfig); - return signingConfig; + X509KeyManager keyManager = keyConfig.createKeyManager(); + if (keyManager == null) { + throw new IllegalStateException("Cannot create key manager for key config [" + keyConfig + "]"); } + + var keyPair = Strings.isNullOrEmpty(alias) + ? buildKeyPair(keyManager, keyConfig) + : buildKeyPair(keyManager, keyConfig, alias); + + logger.trace("Key pair [{}] found for [{}]", keyPair, clusterAlias); + var signingConfig = new SigningConfig(keyPair, keyConfig.getDependentFiles()); + signingConfigByClusterAlias.put(clusterAlias, signingConfig); + return Optional.of(signingConfig); } else { logger.error(Strings.format("No signing credentials found in signing config for cluster [%s]", clusterAlias)); } @@ -74,12 +80,12 @@ SigningConfig loadSigningConfig(String clusterAlias, Settings settings) { } logger.trace("No valid signing config settings found for [{}] with settings [{}]", clusterAlias, settings); signingConfigByClusterAlias.remove(clusterAlias); - return null; + return Optional.empty(); } public X509CertificateSignature sign(String clusterAlias, String... headers) { SigningConfig signingConfig = signingConfigByClusterAlias.get(clusterAlias); - if (signingConfig == null || signingConfig.keyPair() == null) { + if (signingConfig == null) { logger.trace("No signing config found for [{}] returning empty signature", clusterAlias); return null; } @@ -103,12 +109,7 @@ private void loadSigningConfigs() { this.environment.settings().getGroups("cluster.remote.", true).forEach(this::loadSigningConfig); } - private X509KeyPair buildKeyPair(SslKeyConfig keyConfig) { - final X509KeyManager keyManager = keyConfig.createKeyManager(); - if (keyManager == null) { - return null; - } - + private X509KeyPair buildKeyPair(X509KeyManager keyManager, SslKeyConfig keyConfig) { final Set aliases = SIGNATURE_ALGORITHM_BY_TYPE.keySet() .stream() .map(keyType -> keyManager.getServerAliases(keyType, null)) @@ -133,14 +134,9 @@ private X509KeyPair buildKeyPair(SslKeyConfig keyConfig) { }; } - private X509KeyPair buildKeyPair(SslKeyConfig keyConfig, String alias) { + private X509KeyPair buildKeyPair(X509KeyManager keyManager, SslKeyConfig keyConfig, String alias) { assert alias != null; - final X509KeyManager keyManager = keyConfig.createKeyManager(); - if (keyManager == null) { - return null; - } - final String keyType = keyManager.getPrivateKey(alias).getAlgorithm(); if (SIGNATURE_ALGORITHM_BY_TYPE.containsKey(keyType) == false) { throw new IllegalStateException( diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java index c84ae2bbb6d9b..993a274e46c7b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java @@ -87,12 +87,12 @@ private void reloadConsumer(String clusterAlias, @Nullable Settings settings, bo var effectiveSettings = buildEffectiveSettings(val, settings, updateSecureSettings); try { var signingConfig = apiKeySigner.loadSigningConfig(clusterAlias, effectiveSettings); - if (signingConfig != null) { - watchDependentFilesForClusterAliases( + signingConfig.ifPresent( + config -> watchDependentFilesForClusterAliases( resourceWatcherService, - signingConfig.dependentFiles().stream().collect(Collectors.toMap(file -> file, (file) -> Set.of(clusterAlias))) - ); - } + config.dependentFiles().stream().collect(Collectors.toMap(file -> file, (file) -> Set.of(clusterAlias))) + ) + ); } catch (IllegalStateException e) { logger.error(Strings.format("Failed to load signing config for cluster [%s]", clusterAlias), e); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java index 6c79dbf348226..be0cfbb62f519 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloaderTests.java @@ -26,6 +26,7 @@ import java.security.cert.X509Certificate; import java.util.HashSet; import java.util.List; +import java.util.Optional; import java.util.Set; import static org.mockito.ArgumentMatchers.any; @@ -48,7 +49,7 @@ public void setUp() throws Exception { crossClusterApiKeySigner = mock(CrossClusterApiKeySigner.class); testKeyPair = createTestKeyPair(); when(crossClusterApiKeySigner.loadSigningConfig(any(), any())).thenReturn( - new CrossClusterApiKeySigner.SigningConfig(testKeyPair, List.of()) + Optional.of(new CrossClusterApiKeySigner.SigningConfig(testKeyPair, List.of())) ); Settings settings = Settings.builder().put("resource.reload.interval.high", TimeValue.timeValueMillis(100)).build(); threadPool = new TestThreadPool(getTestName()); @@ -114,13 +115,13 @@ public void testDynamicSettingsUpdateWithAddedFiles() throws Exception { clusterNames[i], remoteClusterSettings[i].getByPrefix("cluster.remote." + clusterNames[i] + ".") ) - ).thenReturn(new CrossClusterApiKeySigner.SigningConfig(testKeyPair, Set.of(filesToMonitor[i]))); + ).thenReturn(Optional.of(new CrossClusterApiKeySigner.SigningConfig(testKeyPair, Set.of(filesToMonitor[i])))); when( crossClusterApiKeySigner.loadSigningConfig( clusterNames[i], dynamicSettingsUpdate.getByPrefix("cluster.remote." + clusterNames[i] + ".") ) - ).thenReturn(new CrossClusterApiKeySigner.SigningConfig(testKeyPair, Set.of(filesToMonitor[i]))); + ).thenReturn(Optional.of(new CrossClusterApiKeySigner.SigningConfig(testKeyPair, Set.of(filesToMonitor[i])))); } crossClusterApiKeySigningConfigReloader.setApiKeySigner(crossClusterApiKeySigner); From 8077f019833e00a98b1c8eb81b234de8fed33eee Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 11 Sep 2025 12:59:18 +0200 Subject: [PATCH 19/24] fixup! Use new secure settings util --- .../CrossClusterApiKeySignerSettings.java | 10 ++- ...ossClusterApiKeySigningConfigReloader.java | 79 +------------------ 2 files changed, 10 insertions(+), 79 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java index ae8cf013a48fc..eac190ff7b209 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerSettings.java @@ -11,8 +11,8 @@ import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.ssl.SslConfigurationKeys; -import org.elasticsearch.common.util.CollectionUtils; +import java.util.ArrayList; import java.util.List; import javax.net.ssl.KeyManagerFactory; @@ -95,11 +95,13 @@ public static List> getDynamicSettings() { ); } - public static List> getSecureSettings() { + public static List> getSecureSettings() { return List.of(SIGNING_KEYSTORE_SECURE_PASSWORD, SIGNING_KEYSTORE_SECURE_KEY_PASSWORD, SIGNING_KEY_SECURE_PASSPHRASE); } - public static List> getSettings() { - return CollectionUtils.concatLists(getDynamicSettings(), getSecureSettings()); + public static List> getSettings() { + List> settings = new ArrayList<>(getSecureSettings()); + settings.addAll(getDynamicSettings()); + return settings; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java index 993a274e46c7b..0c56f9217c0d2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java @@ -10,11 +10,9 @@ import org.apache.logging.log4j.Logger; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.PlainActionFuture; -import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.InMemoryClonedSecureSettings; import org.elasticsearch.common.settings.SecureSettings; -import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.ssl.SslKeyConfig; import org.elasticsearch.common.util.set.Sets; @@ -29,12 +27,10 @@ import org.elasticsearch.xpack.security.support.ReloadableSecurityComponent; import java.io.IOException; -import java.io.InputStream; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.util.HashMap; import java.util.HashSet; -import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -220,7 +216,9 @@ public void reload(Settings settings) { // The secure settings provided to reload are only available in the scope of this method call since after that the keystore is // closed. Since the secure settings will potentially be used later when the signing config is used to sign headers, the // settings need to be retrieved from the keystore and cached - Settings cachedSettings = Settings.builder().setSecureSettings(extractSecureSettings(settings, getSecureSettings())).build(); + Settings cachedSettings = Settings.builder() + .setSecureSettings(InMemoryClonedSecureSettings.cloneSecureSettings(settings, getSecureSettings())) + .build(); cachedSettings.getGroups("cluster.remote.", true).forEach((clusterAlias, settingsForCluster) -> { // Only update signing config if settings were found, since empty config means config deletion if (settingsForCluster.isEmpty() == false) { @@ -232,73 +230,4 @@ public void reload(Settings settings) { logger.error("Keystore exception while reloading signing configuration after reload of secure settings", e); } } - - /** - * Extracts the {@link SecureSettings}` out of the passed in {@link Settings} object. The {@code Setting} argument has to have the - * {@code SecureSettings} open/available. Normally {@code SecureSettings} are available only under specific callstacks (eg. during node - * initialization or during a `reload` call). The returned copy can be reused freely as it will never be closed (this is a bit of - * cheating, but it is necessary in this specific circumstance). Only works for secure settings of type string (not file). - * - * @param source A {@code Settings} object with its {@code SecureSettings} open/available. - * @param settingsToCopy The list of settings to copy. - * @return A copy of the {@code SecureSettings} of the passed in {@code Settings} argument. - */ - private static SecureSettings extractSecureSettings(Settings source, List> settingsToCopy) - throws GeneralSecurityException { - final SecureSettings sourceSecureSettings = Settings.builder().put(source, true).getSecureSettings(); - final Map copiedSettings = new HashMap<>(); - - if (sourceSecureSettings != null && settingsToCopy != null) { - for (final String settingKey : sourceSecureSettings.getSettingNames()) { - for (final Setting secureSetting : settingsToCopy) { - if (secureSetting.match(settingKey)) { - copiedSettings.put( - settingKey, - new SecureSettingValue( - sourceSecureSettings.getString(settingKey), - sourceSecureSettings.getSHA256Digest(settingKey) - ) - ); - } - } - } - } - return new SecureSettings() { - @Override - public boolean isLoaded() { - return true; - } - - @Override - public SecureString getString(String setting) { - return copiedSettings.get(setting).value(); - } - - @Override - public Set getSettingNames() { - return copiedSettings.keySet(); - } - - @Override - public InputStream getFile(String setting) { - throw new UnsupportedOperationException("A cached SecureSetting cannot be a file"); - } - - @Override - public byte[] getSHA256Digest(String setting) { - return copiedSettings.get(setting).sha256Digest(); - } - - @Override - public void close() throws IOException {} - - @Override - public void writeTo(StreamOutput out) throws IOException { - throw new UnsupportedOperationException("A cached SecureSetting cannot be serialized"); - } - }; - } - - private record SecureSettingValue(SecureString value, byte[] sha256Digest) {} - } From 134776131ade8506a2260591cf79dd4a37c88ddb Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Thu, 11 Sep 2025 13:32:01 +0200 Subject: [PATCH 20/24] fixup! Config corner case --- .../transport/CrossClusterApiKeySigningConfigReloader.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java index 0c56f9217c0d2..a641eb239ed25 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java @@ -220,8 +220,8 @@ public void reload(Settings settings) { .setSecureSettings(InMemoryClonedSecureSettings.cloneSecureSettings(settings, getSecureSettings())) .build(); cachedSettings.getGroups("cluster.remote.", true).forEach((clusterAlias, settingsForCluster) -> { - // Only update signing config if settings were found, since empty config means config deletion - if (settingsForCluster.isEmpty() == false) { + // Only update signing config if settings were found, since empty signing config settings means config deletion + if (settingsForCluster.getByPrefix(SETTINGS_PART_SIGNING).isEmpty() == false) { reloadConsumer(clusterAlias, settingsForCluster, true); logger.info("Updated signing configuration for [{}] due to reload of secure settings", clusterAlias); } From 727bc38a2de959301a2cd67f126202877b8a47fe Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Mon, 15 Sep 2025 09:46:44 +0200 Subject: [PATCH 21/24] Add support for validator in addAffixGroupUpdateConsumer --- .../settings/AbstractScopedSettings.java | 29 +++++++++++++++---- .../common/settings/ScopedSettingsTests.java | 23 ++++++++++++++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java index 604f281a82310..e90db928a8323 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java @@ -305,14 +305,18 @@ public void apply(Map> values, Settings current, Settings pr } /** - * Adds a affix settings consumer that accepts the settings for a group of settings. The consumer is only - * notified if at least one of the settings change. + * Adds an affix settings consumer and validator that accepts the settings for a group of settings. The consumer and + * the validator are only notified if at least one of the settings change. *

* Note: Only settings registered in {@link SettingsModule} can be changed dynamically. *

*/ @SuppressWarnings("rawtypes") - public synchronized void addAffixGroupUpdateConsumer(List> settings, BiConsumer consumer) { + public synchronized void addAffixGroupUpdateConsumer( + List> settings, + BiConsumer consumer, + BiConsumer validator + ) { List affixUpdaters = new ArrayList<>(settings.size()); for (Setting.AffixSetting setting : settings) { ensureSettingIsRegistered(setting); @@ -330,8 +334,8 @@ public boolean hasChanged(Settings current, Settings previous) { public Map getValue(Settings current, Settings previous) { Set namespaces = new HashSet<>(); for (Setting.AffixSetting setting : settings) { - SettingUpdater affixUpdaterA = setting.newAffixUpdater((k, v) -> namespaces.add(k), logger, (a, b) -> {}); - affixUpdaterA.apply(current, previous); + SettingUpdater affixUpdater = setting.newAffixUpdater((k, v) -> namespaces.add(k), logger, (a, b) -> {}); + affixUpdater.apply(current, previous); } Map namespaceToSettings = Maps.newMapWithExpectedSize(namespaces.size()); for (String namespace : namespaces) { @@ -339,7 +343,9 @@ public Map getValue(Settings current, Settings previous) { for (Setting.AffixSetting setting : settings) { concreteSettings.add(setting.getConcreteSettingForNamespace(namespace).getKey()); } - namespaceToSettings.put(namespace, current.filter(concreteSettings::contains)); + var subset = current.filter(concreteSettings::contains); + validator.accept(namespace, subset); + namespaceToSettings.put(namespace, subset); } return namespaceToSettings; } @@ -353,6 +359,17 @@ public void apply(Map values, Settings current, Settings previ }); } + /** + * Adds an affix settings consumer that accepts the settings for a group of settings. The consumer is only + * notified if at least one of the settings change. + *

+ * Note: Only settings registered in {@link SettingsModule} can be changed dynamically. + *

+ */ + public synchronized void addAffixGroupUpdateConsumer(List> settings, BiConsumer consumer) { + addAffixGroupUpdateConsumer(settings, consumer, (a, b) -> {}); + } + private void ensureSettingIsRegistered(Setting.AffixSetting setting) { final Setting registeredSetting = this.complexMatchers.get(setting.getKey()); if (setting != registeredSetting) { diff --git a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java index 253abcf93dace..fd04040839fee 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java @@ -467,8 +467,14 @@ public void testAffixGroupUpdateConsumer() { String group2 = randomAlphaOfLength(4); String group3 = randomAlphaOfLength(5); BiConsumer listConsumer = results::put; + BiConsumer validator = (group, settings) -> { + var val = intSetting.getConcreteSettingForNamespace(group).get(settings); + if (val > 10) { + throw new IllegalArgumentException("int too large"); + } + }; - service.addAffixGroupUpdateConsumer(Arrays.asList(intSetting, listSetting), listConsumer); + service.addAffixGroupUpdateConsumer(Arrays.asList(intSetting, listSetting), listConsumer, validator); assertEquals(0, results.size()); service.applySettings( Settings.builder() @@ -541,6 +547,21 @@ public void testAffixGroupUpdateConsumer() { assertEquals(Arrays.asList(16, 17), listSetting.getConcreteSettingForNamespace(group1).get(groupOneSettings)); assertEquals(1, results.size()); assertEquals(2, groupOneSettings.size()); + + var exception = assertThrows( + IllegalArgumentException.class, + () -> service.applySettings( + Settings.builder() + .put(intBuilder.apply(group1), 2) + .put(intBuilder.apply(group2), 11) // fails validation + .putList(listBuilder.apply(group1), "16", "17") + .putList(listBuilder.apply(group3), "5", "6") + .build() + ) + ); + + assertThat(exception.getMessage(), containsString("int too large")); + results.clear(); } From 2e178bac554684d2827b406e6c6d865d36ffbbe4 Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Mon, 15 Sep 2025 10:35:34 +0200 Subject: [PATCH 22/24] Use validator --- ...ClusterSigningConfigReloaderIntegTests.java | 13 +++++++++++++ .../transport/CrossClusterApiKeySigner.java | 14 ++++++++++++++ ...rossClusterApiKeySigningConfigReloader.java | 18 +++++++++++++++++- 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java index cd62af3f699c3..f3d57d0817553 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java @@ -36,6 +36,7 @@ import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_TYPE; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_PATH; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_SECURE_PASSPHRASE; +import static org.hamcrest.Matchers.equalTo; public class CrossClusterSigningConfigReloaderIntegTests extends SecurityIntegTestCase { @@ -198,6 +199,18 @@ public void testRemoveFileWithConfig() throws Exception { } } + public void testValidationFailsWhenUpdateWithInvalidPath() throws Exception { + var exception = assertThrows( + IllegalArgumentException.class, + () -> updateClusterSettings( + Settings.builder() + .put(SIGNING_CERT_PATH.getConcreteSettingForNamespace("test").getKey(), "/unknown/path") + .put(SIGNING_KEY_PATH.getConcreteSettingForNamespace("test").getKey(), "/unknown/path") + ) + ); + assertThat(exception.getMessage(), equalTo("File [/unknown/path] configured for remote cluster [test] does no exist")); + } + private void addAndRemoveClusterConfigsRuntime( Set clusterAliases, Consumer clusterCreator, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index 8ae95ea873ac5..dcccc17823f1e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.security.GeneralSecurityException; import java.security.PrivateKey; @@ -83,6 +84,19 @@ Optional loadSigningConfig(String clusterAlias, Settings settings return Optional.empty(); } + public void validateSigningConfigUpdate(String clusterAlias, Settings settings) { + if (settings.getByPrefix(SETTINGS_PART_SIGNING).isEmpty() == false) { + var keyConfig = CertParsingUtils.createKeyConfig(settings, SETTINGS_PART_SIGNING + ".", environment, false); + keyConfig.getDependentFiles().stream().forEach(file -> { + if (Files.exists(file) == false) { + throw new IllegalArgumentException( + String.format("File [%s] configured for remote cluster [%s] does no exist", file, clusterAlias) + ); + } + }); + } + } + public X509CertificateSignature sign(String clusterAlias, String... headers) { SigningConfig signingConfig = signingConfigByClusterAlias.get(clusterAlias); if (signingConfig == null) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java index a641eb239ed25..e67085ef333a8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigningConfigReloader.java @@ -73,7 +73,23 @@ public CrossClusterApiKeySigningConfigReloader( clusterSettings.addAffixGroupUpdateConsumer(getDynamicSettings(), (key, val) -> { reloadConsumer(key, val.getByPrefix("cluster.remote." + key + "."), false); logger.info("Updated signing configuration for [{}] due to updated cluster settings", key); - }); + }, this::validateUpdate); + } + + private void validateUpdate(String clusterAlias, Settings settings) { + try { + var apiKeySigner = crossClusterApiKeySignerFuture.get(); + apiKeySigner.validateSigningConfigUpdate(clusterAlias, settings.getByPrefix("cluster.remote." + clusterAlias + ".")); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (ExecutionException e) { + throw new ElasticsearchException("Failed to obtain crossClusterApiKeySigner", e); + } catch (Exception e) { + logger.debug( + Strings.format("Failed to update cluster [%s] with settings [%s] due validation error [%s]", clusterAlias, settings, e) + ); + throw e; + } } private void reloadConsumer(String clusterAlias, @Nullable Settings settings, boolean updateSecureSettings) { From 00eb36f8c8fa8b16ae10071c75225a0dca42ab7c Mon Sep 17 00:00:00 2001 From: Johannes Freden Jansson Date: Mon, 15 Sep 2025 10:47:18 +0200 Subject: [PATCH 23/24] fixup! Wrong api... --- .../xpack/security/transport/CrossClusterApiKeySigner.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index dcccc17823f1e..343f98d631b84 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -18,6 +18,7 @@ import org.elasticsearch.logging.Logger; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; +import javax.net.ssl.X509KeyManager; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -35,8 +36,6 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; -import javax.net.ssl.X509KeyManager; - import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.KEYSTORE_ALIAS_SUFFIX; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SETTINGS_PART_SIGNING; @@ -90,7 +89,7 @@ public void validateSigningConfigUpdate(String clusterAlias, Settings settings) keyConfig.getDependentFiles().stream().forEach(file -> { if (Files.exists(file) == false) { throw new IllegalArgumentException( - String.format("File [%s] configured for remote cluster [%s] does no exist", file, clusterAlias) + Strings.format("File [%s] configured for remote cluster [%s] does no exist", file, clusterAlias) ); } }); From d6a582be7f579a77b159268c1009afdfb4afaee4 Mon Sep 17 00:00:00 2001 From: elasticsearchmachine Date: Mon, 15 Sep 2025 08:56:05 +0000 Subject: [PATCH 24/24] [CI] Auto commit changes from spotless --- .../xpack/security/transport/CrossClusterApiKeySigner.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java index 343f98d631b84..a895d848b43bc 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySigner.java @@ -18,7 +18,6 @@ import org.elasticsearch.logging.Logger; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; -import javax.net.ssl.X509KeyManager; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; @@ -36,6 +35,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import javax.net.ssl.X509KeyManager; + import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.KEYSTORE_ALIAS_SUFFIX; import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SETTINGS_PART_SIGNING;