Skip to content

Commit

Permalink
Use SecureSetting to store RCS remote access key (#93763)
Browse files Browse the repository at this point in the history
The remote access key is a secret. As such it should be stored in the ES
keystore instead of a filtered setting in cluster state.
  • Loading branch information
ywangd committed Mar 23, 2023
1 parent 7527d6f commit 5dc2541
Show file tree
Hide file tree
Showing 21 changed files with 404 additions and 365 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
import org.apache.lucene.tests.util.TimeUnits;
import org.elasticsearch.Version;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestClientBuilder;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
Expand All @@ -38,9 +40,11 @@
import org.junit.rules.TestRule;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import static java.util.Collections.unmodifiableList;
import static org.elasticsearch.test.rest.yaml.CcsCommonYamlTestSuiteIT.CCS_APIS;
Expand All @@ -65,6 +69,7 @@ public class RcsCcsCommonYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
private static TestCandidateAwareClient searchYamlTestClient;
// the remote cluster is the one we write index operations etc... to
private static final String REMOTE_CLUSTER_NAME = "remote_cluster";
private static final AtomicReference<Map<String, Object>> API_KEY_MAP_REF = new AtomicReference<>();

private static LocalClusterConfigProvider commonClusterConfig = cluster -> cluster.module("x-pack-async-search")
.module("aggregations")
Expand Down Expand Up @@ -98,10 +103,58 @@ public class RcsCcsCommonYamlTestSuiteIT extends ESClientYamlSuiteTestCase {
.setting("node.roles", "[data,ingest,master,remote_cluster_client]")
.setting("cluster.remote.connections_per_cluster", "1")
.apply(commonClusterConfig)
.keystore("cluster.remote." + REMOTE_CLUSTER_NAME + ".credentials", () -> {
if (API_KEY_MAP_REF.get() == null) {
try {
API_KEY_MAP_REF.set(createCrossClusterAccessApiKey());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return (String) API_KEY_MAP_REF.get().get("encoded");
})
.rolesFile(Resource.fromClasspath("roles.yml"))
.user("remote_search_user", "x-pack-test-password", "remote_search_role")
.build();

private static Map<String, Object> createCrossClusterAccessApiKey() throws IOException {
assert fulfillingCluster != null;
final var createApiKeyRequest = new Request("POST", "/_security/api_key");
createApiKeyRequest.setJsonEntity("""
{
"name": "cross_cluster_access_key",
"role_descriptors": {
"role": {
"cluster": ["cross_cluster_access"],
"index": [
{
"names": ["*"],
"privileges": ["read", "read_cross_cluster"],
"allow_restricted_indices": true
}
]
}
}
}""");
createApiKeyRequest.setOptions(
RequestOptions.DEFAULT.toBuilder()
.addHeader("Authorization", basicAuthHeaderValue("test_admin", new SecureString("x-pack-test-password".toCharArray())))
);

final int numberOfFcNodes = fulfillingCluster.getHttpAddresses().split(",").length;
final String url = fulfillingCluster.getHttpAddress(randomIntBetween(0, numberOfFcNodes - 1));
final int portSeparator = url.lastIndexOf(':');
final var httpHost = new HttpHost(url.substring(0, portSeparator), Integer.parseInt(url.substring(portSeparator + 1)), "http");
RestClientBuilder builder = RestClient.builder(httpHost);
configureClient(builder, Settings.EMPTY);
builder.setStrictDeprecationMode(true);
try (RestClient fulfillingClusterClient = builder.build()) {
final Response createApiKeyResponse = fulfillingClusterClient.performRequest(createApiKeyRequest);
assertOK(createApiKeyResponse);
return responseAsMap(createApiKeyResponse);
}
}

@ClassRule
// Use a RuleChain to ensure that remote cluster is started before local cluster
public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster);
Expand Down Expand Up @@ -206,30 +259,7 @@ public void initSearchClient() throws IOException {
}

private static void configureRemoteCluster() throws IOException {
final var createApiKeyRequest = new Request("POST", "/_security/api_key");
createApiKeyRequest.setJsonEntity("""
{
"name": "cross_cluster_access_key",
"role_descriptors": {
"role": {
"cluster": ["cross_cluster_access"],
"index": [
{
"names": ["*"],
"privileges": ["read", "read_cross_cluster"],
"allow_restricted_indices": true
}
]
}
}
}""");
final Response createApiKeyResponse = adminClient().performRequest(createApiKeyRequest);
assertOK(createApiKeyResponse);
final Map<String, Object> apiKeyMap = responseAsMap(createApiKeyResponse);
final String encodedCrossClusterAccessApiKey = (String) apiKeyMap.get("encoded");

final Settings.Builder builder = Settings.builder()
.put("cluster.remote." + REMOTE_CLUSTER_NAME + ".credentials", encodedCrossClusterAccessApiKey);
final Settings.Builder builder = Settings.builder();
if (randomBoolean()) {
builder.put("cluster.remote." + REMOTE_CLUSTER_NAME + ".mode", "proxy")
.put("cluster.remote." + REMOTE_CLUSTER_NAME + ".proxy_address", fulfillingCluster.getRemoteClusterServerEndpoint(0));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ public void listenForUpdates(ClusterSettings clusterSettings) {
List<Setting.AffixSetting<?>> remoteClusterSettings = Stream.of(
RemoteClusterService.REMOTE_CLUSTER_COMPRESS,
RemoteClusterService.REMOTE_CLUSTER_PING_SCHEDULE,
TcpTransport.isUntrustedRemoteClusterEnabled() ? RemoteClusterService.REMOTE_CLUSTER_CREDENTIALS : null,
RemoteConnectionStrategy.REMOTE_CONNECTION_MODE,
SniffConnectionStrategy.REMOTE_CLUSTERS_PROXY,
SniffConnectionStrategy.REMOTE_CLUSTER_SEEDS,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,14 @@ final class RemoteClusterConnection implements Closeable {
* @param settings the nodes settings object
* @param clusterAlias the configured alias of the cluster to connect to
* @param transportService the local nodes transport service
* @param credentialsProtected Whether the remote cluster is protected by a credentials, i.e. it has a credentials configured
* via secure setting. This means the remote cluster uses the new configurable access RCS model
* (as opposed to the basic model).
*/
RemoteClusterConnection(Settings settings, String clusterAlias, TransportService transportService) {
RemoteClusterConnection(Settings settings, String clusterAlias, TransportService transportService, boolean credentialsProtected) {
this.transportService = transportService;
this.clusterAlias = clusterAlias;
ConnectionProfile profile = RemoteConnectionStrategy.buildConnectionProfile(clusterAlias, settings);
ConnectionProfile profile = RemoteConnectionStrategy.buildConnectionProfile(clusterAlias, settings, credentialsProtected);
this.remoteConnectionManager = new RemoteConnectionManager(clusterAlias, createConnectionManager(profile, transportService));
this.connectionStrategy = RemoteConnectionStrategy.buildStrategy(clusterAlias, transportService, remoteConnectionManager, settings);
// we register the transport service here as a listener to make sure we notify handlers on disconnect etc.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.elasticsearch.cluster.node.DiscoveryNodeRole;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.SecureSetting;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
Expand Down Expand Up @@ -122,10 +124,10 @@ public final class RemoteClusterService extends RemoteClusterAware implements Cl
)
);

public static final Setting.AffixSetting<String> REMOTE_CLUSTER_CREDENTIALS = Setting.affixKeySetting(
public static final Setting.AffixSetting<SecureString> REMOTE_CLUSTER_CREDENTIALS = Setting.affixKeySetting(
"cluster.remote.",
"credentials",
key -> Setting.simpleString(key, v -> {}, Setting.Property.Dynamic, Setting.Property.NodeScope, Setting.Property.Filtered)
key -> SecureSetting.secureString(key, null)
);

public static final String REMOTE_CLUSTER_HANDSHAKE_ACTION_NAME = "cluster:internal/remote_cluster/handshake";
Expand All @@ -143,14 +145,16 @@ public boolean isRemoteClusterServerEnabled() {

private final TransportService transportService;
private final Map<String, RemoteClusterConnection> remoteClusters = ConcurrentCollections.newConcurrentMap();
private final Set<String> credentialsProtectedRemoteClusters;

RemoteClusterService(Settings settings, TransportService transportService) {
super(settings);
this.enabled = DiscoveryNode.isRemoteClusterClient(settings);
this.remoteClusterServerEnabled = REMOTE_CLUSTER_SERVER_ENABLED.get(settings);
this.transportService = transportService;
this.credentialsProtectedRemoteClusters = REMOTE_CLUSTER_CREDENTIALS.getAsMap(settings).keySet();

if (RemoteClusterPortSettings.REMOTE_CLUSTER_SERVER_ENABLED.get(settings)) {
if (remoteClusterServerEnabled) {
registerRemoteClusterHandshakeRequestHandler(transportService);
}
}
Expand Down Expand Up @@ -320,7 +324,12 @@ synchronized void updateRemoteCluster(String clusterAlias, Settings newSettings,
if (remote == null) {
// this is a new cluster we have to add a new representation
Settings finalSettings = Settings.builder().put(this.settings, false).put(newSettings, false).build();
remote = new RemoteClusterConnection(finalSettings, clusterAlias, transportService);
remote = new RemoteClusterConnection(
finalSettings,
clusterAlias,
transportService,
credentialsProtectedRemoteClusters.contains(clusterAlias)
);
remoteClusters.put(clusterAlias, remote);
remote.ensureConnected(listener);
} else if (remote.shouldRebuildConnection(newSettings)) {
Expand All @@ -332,7 +341,12 @@ synchronized void updateRemoteCluster(String clusterAlias, Settings newSettings,
}
remoteClusters.remove(clusterAlias);
Settings finalSettings = Settings.builder().put(this.settings, false).put(newSettings, false).build();
remote = new RemoteClusterConnection(finalSettings, clusterAlias, transportService);
remote = new RemoteClusterConnection(
finalSettings,
clusterAlias,
transportService,
credentialsProtectedRemoteClusters.contains(clusterAlias)
);
remoteClusters.put(clusterAlias, remote);
remote.ensureConnected(listener);
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@
import java.util.stream.Stream;

import static org.elasticsearch.core.Strings.format;
import static org.elasticsearch.transport.RemoteClusterService.REMOTE_CLUSTER_CREDENTIALS;

public abstract class RemoteConnectionStrategy implements TransportConnectionListener, Closeable {

Expand Down Expand Up @@ -141,8 +140,8 @@ public Writeable.Reader<RemoteConnectionInfo.ModeInfo> getReader() {
connectionManager.addListener(this);
}

static ConnectionProfile buildConnectionProfile(String clusterAlias, Settings settings) {
final String transportProfile = REMOTE_CLUSTER_CREDENTIALS.getConcreteSettingForNamespace(clusterAlias).exists(settings)
static ConnectionProfile buildConnectionProfile(String clusterAlias, Settings settings, boolean credentialsProtected) {
final String transportProfile = credentialsProtected
? RemoteClusterPortSettings.REMOTE_CLUSTER_PROFILE
: TransportSettings.DEFAULT_PROFILE;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public class ProxyConnectionStrategyTests extends ESTestCase {
private final String clusterAlias = "cluster-alias";
private final String modeKey = RemoteConnectionStrategy.REMOTE_CONNECTION_MODE.getConcreteSettingForNamespace(clusterAlias).getKey();
private final Settings settings = Settings.builder().put(modeKey, "proxy").build();
private final ConnectionProfile profile = RemoteConnectionStrategy.buildConnectionProfile("cluster", settings);
private final ConnectionProfile profile = RemoteConnectionStrategy.buildConnectionProfile("cluster", settings, false);
private final ThreadPool threadPool = new TestThreadPool(getClass().getName());

@Override
Expand Down

0 comments on commit 5dc2541

Please sign in to comment.