Skip to content

Commit d235a74

Browse files
jfredenelasticsearchmachine
andauthored
Add signing configuration for cross cluster api keys (#134137)
* Add signing configuration for cross cluster api keys * Update docs/changelog/134137.yaml * Add support for validator in addAffixGroupUpdateConsumer * Use validator --------- Co-authored-by: elasticsearchmachine <[email protected]>
1 parent 1bbc0db commit d235a74

File tree

20 files changed

+1658
-7
lines changed

20 files changed

+1658
-7
lines changed

docs/changelog/134137.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 134137
2+
summary: Add signing configuration for cross cluster api keys
3+
area: Security
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/common/settings/AbstractScopedSettings.java

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -305,14 +305,18 @@ public void apply(Map<String, Tuple<A, B>> values, Settings current, Settings pr
305305
}
306306

307307
/**
308-
* Adds a affix settings consumer that accepts the settings for a group of settings. The consumer is only
309-
* notified if at least one of the settings change.
308+
* Adds an affix settings consumer and validator that accepts the settings for a group of settings. The consumer and
309+
* the validator are only notified if at least one of the settings change.
310310
* <p>
311311
* Note: Only settings registered in {@link SettingsModule} can be changed dynamically.
312312
* </p>
313313
*/
314314
@SuppressWarnings("rawtypes")
315-
public synchronized void addAffixGroupUpdateConsumer(List<Setting.AffixSetting<?>> settings, BiConsumer<String, Settings> consumer) {
315+
public synchronized void addAffixGroupUpdateConsumer(
316+
List<Setting.AffixSetting<?>> settings,
317+
BiConsumer<String, Settings> consumer,
318+
BiConsumer<String, Settings> validator
319+
) {
316320
List<SettingUpdater> affixUpdaters = new ArrayList<>(settings.size());
317321
for (Setting.AffixSetting<?> setting : settings) {
318322
ensureSettingIsRegistered(setting);
@@ -330,16 +334,18 @@ public boolean hasChanged(Settings current, Settings previous) {
330334
public Map<String, Settings> getValue(Settings current, Settings previous) {
331335
Set<String> namespaces = new HashSet<>();
332336
for (Setting.AffixSetting<?> setting : settings) {
333-
SettingUpdater affixUpdaterA = setting.newAffixUpdater((k, v) -> namespaces.add(k), logger, (a, b) -> {});
334-
affixUpdaterA.apply(current, previous);
337+
SettingUpdater affixUpdater = setting.newAffixUpdater((k, v) -> namespaces.add(k), logger, (a, b) -> {});
338+
affixUpdater.apply(current, previous);
335339
}
336340
Map<String, Settings> namespaceToSettings = Maps.newMapWithExpectedSize(namespaces.size());
337341
for (String namespace : namespaces) {
338342
Set<String> concreteSettings = Sets.newHashSetWithExpectedSize(settings.size());
339343
for (Setting.AffixSetting<?> setting : settings) {
340344
concreteSettings.add(setting.getConcreteSettingForNamespace(namespace).getKey());
341345
}
342-
namespaceToSettings.put(namespace, current.filter(concreteSettings::contains));
346+
var subset = current.filter(concreteSettings::contains);
347+
validator.accept(namespace, subset);
348+
namespaceToSettings.put(namespace, subset);
343349
}
344350
return namespaceToSettings;
345351
}
@@ -353,6 +359,17 @@ public void apply(Map<String, Settings> values, Settings current, Settings previ
353359
});
354360
}
355361

362+
/**
363+
* Adds an affix settings consumer that accepts the settings for a group of settings. The consumer is only
364+
* notified if at least one of the settings change.
365+
* <p>
366+
* Note: Only settings registered in {@link SettingsModule} can be changed dynamically.
367+
* </p>
368+
*/
369+
public synchronized void addAffixGroupUpdateConsumer(List<Setting.AffixSetting<?>> settings, BiConsumer<String, Settings> consumer) {
370+
addAffixGroupUpdateConsumer(settings, consumer, (a, b) -> {});
371+
}
372+
356373
private void ensureSettingIsRegistered(Setting.AffixSetting<?> setting) {
357374
final Setting<?> registeredSetting = this.complexMatchers.get(setting.getKey());
358375
if (setting != registeredSetting) {

server/src/test/java/org/elasticsearch/common/settings/ScopedSettingsTests.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -467,8 +467,14 @@ public void testAffixGroupUpdateConsumer() {
467467
String group2 = randomAlphaOfLength(4);
468468
String group3 = randomAlphaOfLength(5);
469469
BiConsumer<String, Settings> listConsumer = results::put;
470+
BiConsumer<String, Settings> validator = (group, settings) -> {
471+
var val = intSetting.getConcreteSettingForNamespace(group).get(settings);
472+
if (val > 10) {
473+
throw new IllegalArgumentException("int too large");
474+
}
475+
};
470476

471-
service.addAffixGroupUpdateConsumer(Arrays.asList(intSetting, listSetting), listConsumer);
477+
service.addAffixGroupUpdateConsumer(Arrays.asList(intSetting, listSetting), listConsumer, validator);
472478
assertEquals(0, results.size());
473479
service.applySettings(
474480
Settings.builder()
@@ -541,6 +547,21 @@ public void testAffixGroupUpdateConsumer() {
541547
assertEquals(Arrays.asList(16, 17), listSetting.getConcreteSettingForNamespace(group1).get(groupOneSettings));
542548
assertEquals(1, results.size());
543549
assertEquals(2, groupOneSettings.size());
550+
551+
var exception = assertThrows(
552+
IllegalArgumentException.class,
553+
() -> service.applySettings(
554+
Settings.builder()
555+
.put(intBuilder.apply(group1), 2)
556+
.put(intBuilder.apply(group2), 11) // fails validation
557+
.putList(listBuilder.apply(group1), "16", "17")
558+
.putList(listBuilder.apply(group3), "5", "6")
559+
.build()
560+
)
561+
);
562+
563+
assertThat(exception.getMessage(), containsString("int too large"));
564+
544565
results.clear();
545566
}
546567

x-pack/plugin/security/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ tasks.named("dependencyLicenses").configure {
184184

185185
tasks.named("forbiddenPatterns").configure {
186186
exclude '**/*.key'
187+
exclude '**/*.bcfks'
187188
exclude '**/*.p12'
188189
exclude '**/*.der'
189190
exclude '**/*.zip'
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.security.transport;
9+
10+
import org.elasticsearch.common.settings.MockSecureSettings;
11+
import org.elasticsearch.common.settings.Settings;
12+
import org.elasticsearch.common.ssl.PemKeyConfig;
13+
import org.elasticsearch.test.SecurityIntegTestCase;
14+
15+
import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_CERT_PATH;
16+
import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_ALIAS;
17+
import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_PATH;
18+
import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_SECURE_PASSWORD;
19+
import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEYSTORE_TYPE;
20+
import static org.elasticsearch.xpack.security.transport.CrossClusterApiKeySignerSettings.SIGNING_KEY_PATH;
21+
import static org.hamcrest.Matchers.equalToIgnoringCase;
22+
23+
public class CrossClusterApiKeySignerIntegTests extends SecurityIntegTestCase {
24+
25+
private static final String DYNAMIC_TEST_CLUSTER_ALIAS = "dynamic_test_cluster";
26+
private static final String STATIC_TEST_CLUSTER_ALIAS = "static_test_cluster";
27+
28+
public void testSignWithPemKeyConfig() {
29+
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
30+
CrossClusterApiKeySigner.class,
31+
internalCluster().getRandomNodeName()
32+
);
33+
final String[] testHeaders = randomArray(5, String[]::new, () -> randomAlphanumericOfLength(randomInt(20)));
34+
35+
X509CertificateSignature signature = signer.sign(STATIC_TEST_CLUSTER_ALIAS, testHeaders);
36+
signature.certificate().getPublicKey();
37+
38+
var keyConfig = new PemKeyConfig(
39+
"signing_rsa.crt",
40+
"signing_rsa.key",
41+
new char[0],
42+
getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt").getParent()
43+
);
44+
45+
assertThat(signature.algorithm(), equalToIgnoringCase(keyConfig.getKeys().getFirst().v2().getSigAlgName()));
46+
assertEquals(signature.certificate(), keyConfig.getKeys().getFirst().v2());
47+
}
48+
49+
public void testSignUnknownClusterAlias() {
50+
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
51+
CrossClusterApiKeySigner.class,
52+
internalCluster().getRandomNodeName()
53+
);
54+
final String[] testHeaders = randomArray(5, String[]::new, () -> randomAlphanumericOfLength(randomInt(20)));
55+
56+
X509CertificateSignature signature = signer.sign("unknowncluster", testHeaders);
57+
assertNull(signature);
58+
}
59+
60+
public void testSeveralKeyStoreAliases() {
61+
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
62+
CrossClusterApiKeySigner.class,
63+
internalCluster().getRandomNodeName()
64+
);
65+
66+
try {
67+
// Create a new config without an alias. Since there are several aliases in the keystore, no signature should be generated
68+
updateClusterSettings(
69+
Settings.builder()
70+
.put(
71+
SIGNING_KEYSTORE_TYPE.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(),
72+
inFipsJvm() ? "BCFKS" : "PKCS12"
73+
)
74+
.put(
75+
SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(),
76+
getDataPath("/org/elasticsearch/xpack/security/signature/signing." + (inFipsJvm() ? "bcfks" : "jks"))
77+
)
78+
);
79+
80+
{
81+
X509CertificateSignature signature = signer.sign(DYNAMIC_TEST_CLUSTER_ALIAS, "test", "test");
82+
assertNull(signature);
83+
}
84+
85+
// Add an alias from the keystore
86+
updateClusterSettings(
87+
Settings.builder()
88+
.put(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), "wholelottakey")
89+
);
90+
{
91+
X509CertificateSignature signature = signer.sign(DYNAMIC_TEST_CLUSTER_ALIAS, "test", "test");
92+
assertNotNull(signature);
93+
}
94+
95+
// Add an alias not in the keystore, settings should silently fail to apply
96+
updateClusterSettings(
97+
Settings.builder()
98+
.put(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(), "idonotexist")
99+
);
100+
{
101+
X509CertificateSignature signature = signer.sign(DYNAMIC_TEST_CLUSTER_ALIAS, "test", "test");
102+
assertNotNull(signature);
103+
}
104+
} finally {
105+
updateClusterSettings(
106+
Settings.builder()
107+
.putNull(SIGNING_KEYSTORE_PATH.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey())
108+
.putNull(SIGNING_KEYSTORE_ALIAS.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey())
109+
.putNull(SIGNING_KEYSTORE_TYPE.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey())
110+
.setSecureSettings(new MockSecureSettings())
111+
);
112+
}
113+
}
114+
115+
@Override
116+
protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
117+
var builder = Settings.builder();
118+
MockSecureSettings secureSettings = (MockSecureSettings) builder.put(super.nodeSettings(nodeOrdinal, otherSettings))
119+
.put(
120+
SIGNING_CERT_PATH.getConcreteSettingForNamespace(STATIC_TEST_CLUSTER_ALIAS).getKey(),
121+
getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.crt")
122+
)
123+
.put(
124+
SIGNING_KEY_PATH.getConcreteSettingForNamespace(STATIC_TEST_CLUSTER_ALIAS).getKey(),
125+
getDataPath("/org/elasticsearch/xpack/security/signature/signing_rsa.key")
126+
)
127+
.getSecureSettings();
128+
secureSettings.setString(
129+
SIGNING_KEYSTORE_SECURE_PASSWORD.getConcreteSettingForNamespace(DYNAMIC_TEST_CLUSTER_ALIAS).getKey(),
130+
"secretpassword"
131+
);
132+
return builder.build();
133+
}
134+
}

0 commit comments

Comments
 (0)