Skip to content

Commit 60ff53c

Browse files
Add remote cluster security SPI extension (#134785)
This PR defines a new remote cluster security SPI extension point. It allows providing custom `RemoteClusterSecurityExtension` via SPI by registering a `RemoteClusterSecurityExtension.Provider`. Currently, it's possible to register only a single extension provider which is defined "internally". If a custom extension provider is not detected, it will fallback to the `CrossClusterAccessSecurityExtension.Provider` by default.
1 parent f4120df commit 60ff53c

23 files changed

+784
-80
lines changed

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/LocalStateCompositeXPackPlugin.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
import org.elasticsearch.plugins.ClusterPlugin;
6767
import org.elasticsearch.plugins.DiscoveryPlugin;
6868
import org.elasticsearch.plugins.EnginePlugin;
69+
import org.elasticsearch.plugins.ExtensiblePlugin;
6970
import org.elasticsearch.plugins.FieldPredicate;
7071
import org.elasticsearch.plugins.IndexStorePlugin;
7172
import org.elasticsearch.plugins.IngestPlugin;
@@ -211,6 +212,12 @@ public List<Setting<?>> getSettings() {
211212
return settings;
212213
}
213214

215+
@Override
216+
public void loadExtensions(ExtensionLoader loader) {
217+
super.loadExtensions(loader);
218+
filterPlugins(ExtensiblePlugin.class).forEach(p -> p.loadExtensions(loader));
219+
}
220+
214221
@Override
215222
public List<String> getSettingsFilter() {
216223
List<String> filters = new ArrayList<>(super.getSettingsFilter());
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
apply plugin: 'elasticsearch.base-internal-es-plugin'
2+
apply plugin: 'elasticsearch.internal-java-rest-test'
3+
4+
esplugin {
5+
name = 'test-rcs-extension-plugin'
6+
description = 'A plugin for testing RemoteClusterSecurityExtension'
7+
classname = 'org.elasticsearch.xpack.security.rcs.extension.RemoteClusterSecurityExtensionTestPlugin'
8+
extendedPlugins = ['x-pack-core', 'x-pack-security']
9+
}
10+
11+
dependencies {
12+
compileOnly project(':x-pack:plugin:core')
13+
compileOnly project(':x-pack:plugin:security')
14+
clusterPlugins project(':x-pack:plugin:security:qa:rcs-extension')
15+
16+
javaRestTestImplementation project(':x-pack:plugin:core')
17+
javaRestTestImplementation project(':x-pack:plugin:security')
18+
javaRestTestImplementation project(':test:framework')
19+
}
20+
21+
tasks.named("javadoc").configure { enabled = false }
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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.rcs.extension;
9+
10+
import org.elasticsearch.client.Request;
11+
import org.elasticsearch.common.settings.SecureString;
12+
import org.elasticsearch.common.settings.Settings;
13+
import org.elasticsearch.common.util.concurrent.ThreadContext;
14+
import org.elasticsearch.test.cluster.ElasticsearchCluster;
15+
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
16+
import org.elasticsearch.test.rest.ESRestTestCase;
17+
import org.junit.ClassRule;
18+
19+
import static org.hamcrest.Matchers.equalTo;
20+
21+
public class RemoteClusterSecurityExtensionIT extends ESRestTestCase {
22+
23+
@ClassRule
24+
public static ElasticsearchCluster cluster = ElasticsearchCluster.local()
25+
.distribution(DistributionType.INTEG_TEST)
26+
.name("test-rcs-extension-cluster")
27+
.plugin("test-rcs-extension-plugin")
28+
.setting("xpack.security.enabled", "true")
29+
.user("test-admin", "x-pack-test-password")
30+
.build();
31+
32+
@Override
33+
protected String getTestRestCluster() {
34+
return cluster.getHttpAddresses();
35+
}
36+
37+
@Override
38+
protected Settings restClientSettings() {
39+
return Settings.builder()
40+
.put(
41+
ThreadContext.PREFIX + ".Authorization",
42+
basicAuthHeaderValue("test-admin", new SecureString("x-pack-test-password".toCharArray()))
43+
)
44+
.build();
45+
}
46+
47+
/**
48+
* Simple test which checks if rcs extension is loaded
49+
* by asserting that RCS extension's setting is returned
50+
* by {@code /_cluster/settings} API.
51+
*/
52+
public void testExtensionIsLoaded() throws Exception {
53+
var settings = assertOKAndCreateObjectPath(client().performRequest(new Request("GET", "/_cluster/settings?include_defaults=true")));
54+
assertThat(settings.evaluate("defaults.xpack.test.rcs.extension.setting"), equalTo("default-rcs-extension-setting-value"));
55+
}
56+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import org.elasticsearch.xpack.security.rcs.extension.TestRemoteClusterSecurityExtension;
2+
import org.elasticsearch.xpack.security.transport.extension.RemoteClusterSecurityExtension;
3+
4+
module org.elasticsearch.internal.security {
5+
requires org.elasticsearch.base;
6+
requires org.elasticsearch.server;
7+
requires org.elasticsearch.xcore;
8+
requires org.elasticsearch.security;
9+
requires org.elasticsearch.sslconfig;
10+
11+
provides RemoteClusterSecurityExtension.Provider with TestRemoteClusterSecurityExtension.Provider;
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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.rcs.extension;
9+
10+
import org.elasticsearch.plugins.Plugin;
11+
12+
public class RemoteClusterSecurityExtensionTestPlugin extends Plugin {}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
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.rcs.extension;
9+
10+
import org.elasticsearch.action.ActionListener;
11+
import org.elasticsearch.action.support.DestructiveOperations;
12+
import org.elasticsearch.common.settings.Setting;
13+
import org.elasticsearch.common.settings.Settings;
14+
import org.elasticsearch.common.ssl.SslConfiguration;
15+
import org.elasticsearch.common.util.Maps;
16+
import org.elasticsearch.transport.Transport;
17+
import org.elasticsearch.transport.TransportInterceptor;
18+
import org.elasticsearch.transport.TransportRequest;
19+
import org.elasticsearch.xpack.core.XPackSettings;
20+
import org.elasticsearch.xpack.core.security.SecurityContext;
21+
import org.elasticsearch.xpack.core.security.authc.Authentication;
22+
import org.elasticsearch.xpack.core.ssl.SSLService;
23+
import org.elasticsearch.xpack.core.ssl.SslProfile;
24+
import org.elasticsearch.xpack.security.authc.RemoteClusterAuthenticationService;
25+
import org.elasticsearch.xpack.security.transport.RemoteClusterTransportInterceptor;
26+
import org.elasticsearch.xpack.security.transport.ServerTransportFilter;
27+
import org.elasticsearch.xpack.security.transport.extension.RemoteClusterSecurityExtension;
28+
29+
import java.util.Collections;
30+
import java.util.List;
31+
import java.util.Map;
32+
33+
public class TestRemoteClusterSecurityExtension implements RemoteClusterSecurityExtension {
34+
35+
public static final Setting<String> DUMMY_EXTENSION_SETTING = Setting.simpleString(
36+
"xpack.test.rcs.extension.setting",
37+
"default-rcs-extension-setting-value",
38+
Setting.Property.NodeScope
39+
);
40+
41+
private final Components components;
42+
43+
public TestRemoteClusterSecurityExtension(Components components) {
44+
this.components = components;
45+
}
46+
47+
@Override
48+
public RemoteClusterTransportInterceptor getTransportInterceptor() {
49+
return new RemoteClusterTransportInterceptor() {
50+
51+
@Override
52+
public TransportInterceptor.AsyncSender interceptSender(TransportInterceptor.AsyncSender sender) {
53+
return sender;
54+
}
55+
56+
@Override
57+
public boolean isRemoteClusterConnection(Transport.Connection connection) {
58+
return false;
59+
}
60+
61+
public boolean hasRemoteClusterAccessHeadersInContext(SecurityContext securityContext) {
62+
return false;
63+
}
64+
65+
@Override
66+
public Map<String, ServerTransportFilter> getProfileTransportFilters(
67+
Map<String, SslProfile> profileConfigurations,
68+
DestructiveOperations destructiveOperations
69+
) {
70+
Map<String, ServerTransportFilter> profileFilters = Maps.newMapWithExpectedSize(profileConfigurations.size() + 1);
71+
Settings settings = components.settings();
72+
final boolean transportSSLEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings);
73+
74+
for (Map.Entry<String, SslProfile> entry : profileConfigurations.entrySet()) {
75+
final String profileName = entry.getKey();
76+
final SslProfile sslProfile = entry.getValue();
77+
final SslConfiguration profileConfiguration = sslProfile.configuration();
78+
profileFilters.put(
79+
profileName,
80+
new ServerTransportFilter(
81+
components.authenticationService(),
82+
components.authorizationService(),
83+
components.threadPool().getThreadContext(),
84+
transportSSLEnabled && SSLService.isSSLClientAuthEnabled(profileConfiguration),
85+
destructiveOperations,
86+
components.securityContext()
87+
)
88+
);
89+
}
90+
// We need to register here the default security
91+
// server transport filter which ensures that all
92+
// incoming transport requests are properly
93+
// authenticated and authorized.
94+
return Collections.unmodifiableMap(profileFilters);
95+
}
96+
97+
};
98+
}
99+
100+
@Override
101+
public RemoteClusterAuthenticationService getAuthenticationService() {
102+
return new RemoteClusterAuthenticationService() {
103+
104+
@Override
105+
public void authenticate(String action, TransportRequest request, ActionListener<Authentication> listener) {
106+
listener.onFailure(new IllegalStateException("not relevant for this test"));
107+
}
108+
109+
@Override
110+
public void authenticateHeaders(Map<String, String> headers, ActionListener<Void> listener) {
111+
listener.onResponse(null);
112+
}
113+
};
114+
}
115+
116+
public static class Provider implements RemoteClusterSecurityExtension.Provider {
117+
118+
@Override
119+
public RemoteClusterSecurityExtension getExtension(Components components) {
120+
return new TestRemoteClusterSecurityExtension(components);
121+
}
122+
123+
@Override
124+
public List<Setting<?>> getSettings() {
125+
return List.of(DUMMY_EXTENSION_SETTING);
126+
}
127+
}
128+
129+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
org.elasticsearch.xpack.security.rcs.extension.TestRemoteClusterSecurityExtension$Provider

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignerIntegTests.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,7 @@ public class CrossClusterApiKeySignerIntegTests extends SecurityIntegTestCase {
2626
private static final String STATIC_TEST_CLUSTER_ALIAS = "static_test_cluster";
2727

2828
public void testSignWithPemKeyConfig() {
29-
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
30-
CrossClusterApiKeySigner.class,
31-
internalCluster().getRandomNodeName()
32-
);
29+
final CrossClusterApiKeySigner signer = getCrossClusterApiKeySignerInstance();
3330
final String[] testHeaders = randomArray(5, String[]::new, () -> randomAlphanumericOfLength(randomInt(20)));
3431

3532
X509CertificateSignature signature = signer.sign(STATIC_TEST_CLUSTER_ALIAS, testHeaders);
@@ -47,21 +44,15 @@ public void testSignWithPemKeyConfig() {
4744
}
4845

4946
public void testSignUnknownClusterAlias() {
50-
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
51-
CrossClusterApiKeySigner.class,
52-
internalCluster().getRandomNodeName()
53-
);
47+
final CrossClusterApiKeySigner signer = getCrossClusterApiKeySignerInstance();
5448
final String[] testHeaders = randomArray(5, String[]::new, () -> randomAlphanumericOfLength(randomInt(20)));
5549

5650
X509CertificateSignature signature = signer.sign("unknowncluster", testHeaders);
5751
assertNull(signature);
5852
}
5953

6054
public void testSeveralKeyStoreAliases() {
61-
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
62-
CrossClusterApiKeySigner.class,
63-
internalCluster().getRandomNodeName()
64-
);
55+
final CrossClusterApiKeySigner signer = getCrossClusterApiKeySignerInstance();
6556

6657
try {
6758
// Create a new config without an alias. Since there are several aliases in the keystore, no signature should be generated
@@ -131,4 +122,9 @@ protected Settings nodeSettings(int nodeOrdinal, Settings otherSettings) {
131122
);
132123
return builder.build();
133124
}
125+
126+
private static CrossClusterApiKeySigner getCrossClusterApiKeySignerInstance() {
127+
return CrossClusterTestHelper.getCrossClusterApiKeySigner(internalCluster());
128+
}
129+
134130
}

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterSigningConfigReloaderIntegTests.java

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,7 @@ public void testAddSecureSettingsConfigRuntime() throws Exception {
100100

101101
public void testDependentKeyConfigFilesUpdated() throws Exception {
102102
assumeFalse("Test credentials uses key encryption not supported in Fips JVM", inFipsJvm());
103-
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
104-
CrossClusterApiKeySigner.class,
105-
internalCluster().getRandomNodeName()
106-
);
103+
final CrossClusterApiKeySigner signer = getCrossClusterApiKeySignerInstance();
107104

108105
String testClusterAlias = "test_cluster";
109106

@@ -159,10 +156,7 @@ public void testDependentKeyConfigFilesUpdated() throws Exception {
159156

160157
public void testRemoveFileWithConfig() throws Exception {
161158
try {
162-
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
163-
CrossClusterApiKeySigner.class,
164-
internalCluster().getRandomNodeName()
165-
);
159+
final CrossClusterApiKeySigner signer = getCrossClusterApiKeySignerInstance();
166160

167161
assertNull(signer.sign("test_cluster", "a_header"));
168162
Path tempDir = createTempDir();
@@ -217,10 +211,7 @@ private void addAndRemoveClusterConfigsRuntime(
217211
Consumer<String> clusterCreator,
218212
Consumer<String> clusterRemover
219213
) throws Exception {
220-
final CrossClusterApiKeySigner signer = internalCluster().getInstance(
221-
CrossClusterApiKeySigner.class,
222-
internalCluster().getRandomNodeName()
223-
);
214+
final CrossClusterApiKeySigner signer = getCrossClusterApiKeySignerInstance();
224215
final String[] testHeaders = randomArray(5, String[]::new, () -> randomAlphanumericOfLength(randomInt(20)));
225216

226217
try {
@@ -302,4 +293,9 @@ public boolean transportSSLEnabled() {
302293
// Needs to be enabled to allow updates to secure settings
303294
return true;
304295
}
296+
297+
private static CrossClusterApiKeySigner getCrossClusterApiKeySignerInstance() {
298+
return CrossClusterTestHelper.getCrossClusterApiKeySigner(internalCluster());
299+
}
300+
305301
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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.test.InternalTestCluster;
11+
12+
final class CrossClusterTestHelper {
13+
14+
/**
15+
* Returns {@link CrossClusterApiKeySigner} instance from a random node of the given cluster.
16+
*/
17+
static CrossClusterApiKeySigner getCrossClusterApiKeySigner(InternalTestCluster cluster) {
18+
RemoteClusterTransportInterceptor interceptor = cluster.getInstance(
19+
RemoteClusterTransportInterceptor.class,
20+
cluster.getRandomNodeName()
21+
);
22+
assert interceptor instanceof CrossClusterAccessTransportInterceptor
23+
: "expected cross-cluster interceptor but got " + interceptor.getClass();
24+
return ((CrossClusterAccessTransportInterceptor) interceptor).getCrossClusterApiKeySigner();
25+
}
26+
27+
private CrossClusterTestHelper() {
28+
throw new IllegalAccessError("not allowed!");
29+
}
30+
}

0 commit comments

Comments
 (0)