Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
20ca640
Add signing configuration for cross cluster api keys
jfreden Aug 28, 2025
4fea9e2
Update docs/changelog/134137.yaml
jfreden Sep 5, 2025
93f2304
fixup! CI
jfreden Sep 8, 2025
4035b14
fixup! fips
jfreden Sep 8, 2025
ab97104
fixup! Update forbidden patterns
jfreden Sep 8, 2025
181b715
fixup! Test issue
jfreden Sep 8, 2025
75e6ac2
fixup! Update tests for fips compliance
jfreden Sep 8, 2025
648892e
Merge remote-tracking branch 'upstream/main' into rcs2.1/conf_signing…
jfreden Sep 8, 2025
1fe6444
fixup! Make buildEffectiveSettings clearer
jfreden Sep 9, 2025
b87acda
fixup! Code review comments
jfreden Sep 9, 2025
8e24ec6
[CI] Auto commit changes from spotless
Sep 9, 2025
3ce1bf6
fixup! address race condition when creating signer
jfreden Sep 9, 2025
e500b0f
fixup! Do not include private key in exception
jfreden Sep 9, 2025
c9c0b73
[CI] Auto commit changes from spotless
Sep 9, 2025
effcde7
fixup! Strict validation on node startup
jfreden Sep 10, 2025
2b48116
Merge remote-tracking branch 'upstream/main' into rcs2.1/conf_signing…
jfreden Sep 10, 2025
8f3f4c6
[CI] Auto commit changes from spotless
Sep 10, 2025
57641b5
Merge branch 'main' into rcs2.1/conf_signing_cert
jfreden Sep 10, 2025
558ad02
fixup! handle race condition and concurrency bug
jfreden Sep 11, 2025
04257b7
fixup! Simplify after refactor
jfreden Sep 11, 2025
fad4c41
Simplify further
jfreden Sep 11, 2025
67a6287
Merge remote-tracking branch 'upstream/main' into rcs2.1/conf_signing…
jfreden Sep 11, 2025
8077f01
fixup! Use new secure settings util
jfreden Sep 11, 2025
1347761
fixup! Config corner case
jfreden Sep 11, 2025
727bc38
Add support for validator in addAffixGroupUpdateConsumer
jfreden Sep 15, 2025
2e178ba
Use validator
jfreden Sep 15, 2025
00eb36f
fixup! Wrong api...
jfreden Sep 15, 2025
5a9af92
Merge remote-tracking branch 'upstream/main' into rcs2.1/conf_signing…
jfreden Sep 15, 2025
d6a582b
[CI] Auto commit changes from spotless
Sep 15, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/changelog/134137.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 134137
summary: Add signing configuration for cross cluster api keys
area: Security
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -305,14 +305,18 @@ public void apply(Map<String, Tuple<A, B>> 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.
* <p>
* Note: Only settings registered in {@link SettingsModule} can be changed dynamically.
* </p>
*/
@SuppressWarnings("rawtypes")
public synchronized void addAffixGroupUpdateConsumer(List<Setting.AffixSetting<?>> settings, BiConsumer<String, Settings> consumer) {
public synchronized void addAffixGroupUpdateConsumer(
List<Setting.AffixSetting<?>> settings,
BiConsumer<String, Settings> consumer,
BiConsumer<String, Settings> validator
) {
List<SettingUpdater> affixUpdaters = new ArrayList<>(settings.size());
for (Setting.AffixSetting<?> setting : settings) {
ensureSettingIsRegistered(setting);
Expand All @@ -330,16 +334,18 @@ public boolean hasChanged(Settings current, Settings previous) {
public Map<String, Settings> getValue(Settings current, Settings previous) {
Set<String> 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<String, Settings> namespaceToSettings = Maps.newMapWithExpectedSize(namespaces.size());
for (String namespace : namespaces) {
Set<String> concreteSettings = Sets.newHashSetWithExpectedSize(settings.size());
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;
}
Expand All @@ -353,6 +359,17 @@ public void apply(Map<String, Settings> 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.
* <p>
* Note: Only settings registered in {@link SettingsModule} can be changed dynamically.
* </p>
*/
public synchronized void addAffixGroupUpdateConsumer(List<Setting.AffixSetting<?>> settings, BiConsumer<String, Settings> consumer) {
addAffixGroupUpdateConsumer(settings, consumer, (a, b) -> {});
}

private void ensureSettingIsRegistered(Setting.AffixSetting<?> setting) {
final Setting<?> registeredSetting = this.complexMatchers.get(setting.getKey());
if (setting != registeredSetting) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -467,8 +467,14 @@ public void testAffixGroupUpdateConsumer() {
String group2 = randomAlphaOfLength(4);
String group3 = randomAlphaOfLength(5);
BiConsumer<String, Settings> listConsumer = results::put;
BiConsumer<String, Settings> 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()
Expand Down Expand Up @@ -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();
}

Expand Down
1 change: 1 addition & 0 deletions x-pack/plugin/security/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ tasks.named("dependencyLicenses").configure {

tasks.named("forbiddenPatterns").configure {
exclude '**/*.key'
exclude '**/*.bcfks'
exclude '**/*.p12'
exclude '**/*.der'
exclude '**/*.zip'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* 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_KEYSTORE_TYPE;
import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_PATH;
import static org.hamcrest.Matchers.equalToIgnoringCase;

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()
);

assertThat(signature.algorithm(), equalToIgnoringCase(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_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." + (inFipsJvm() ? "bcfks" : "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, 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");
assertNotNull(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())
.putNull(SIGNING_KEYSTORE_TYPE.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();
}
}
Loading