Skip to content

Commit

Permalink
Deprecate certificate-based remote cluster security model (#120806)
Browse files Browse the repository at this point in the history
Today, Elasticsearch supports two models to establish secure connections
and trust between two Elasticsearch clusters:

- API key based security model
- Certificate based security model

This PR deprecates the _Certificate based security model_ in favour of *API key based security model*.
The _API key based security model_ is preferred way to configure remote clusters,
as it allows to follow security best practices when setting up remote cluster connections
and defining fine-grained access control.

Users are encouraged to migrate remote clusters from certificate to API key authentication.
  • Loading branch information
slobodanadamovic authored Jan 29, 2025
1 parent d763805 commit c5ab17c
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 6 deletions.
20 changes: 20 additions & 0 deletions docs/changelog/120806.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
pr: 120806
summary: Deprecate certificate based remote cluster security model
area: Security
type: deprecation
issues: []
deprecation:
title: Deprecate certificate based remote cluster security model
area: Authorization
details: -|
<<remote-clusters-cert,_Certificate-based remote cluster security model_>> is deprecated and will be removed
in a future major version.
Users are encouraged to <<remote-clusters-migrate, migrate remote clusters from certificate to API key authentication>>.
The <<remote-clusters-api-key,*API key-based security model*>> is preferred way to configure remote clusters,
as it allows to follow security best practices when setting up remote cluster connections
and defining fine-grained access control.
impact: -|
If you have configured remote clusters with certificate-based security model, you should
<<remote-clusters-migrate, migrate remote clusters from certificate to API key authentication>>.
Configuring a remote cluster using <<remote-clusters-cert,certificate authentication>>,
generates a warning in the deprecation logs.
2 changes: 2 additions & 0 deletions docs/reference/esql/esql-across-clusters.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ If you're using the API key authentication method, you'll see the `"cluster_cred
[[esql-ccs-security-model-certificate]]
===== TLS certificate authentication

deprecated::[9.0.0, "Use <<esql-ccs-security-model-api-key,API key authentication>> instead."]

TLS certificate authentication secures remote clusters with mutual TLS.
This could be the preferred model when a single administrator has full control over both clusters.
We generally recommend that roles and their privileges be identical in both clusters.
Expand Down
4 changes: 3 additions & 1 deletion docs/reference/modules/cluster/remote-clusters-cert.asciidoc
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[[remote-clusters-cert]]
=== Add remote clusters using TLS certificate authentication

deprecated::[9.0.0,"Certificate based authentication is deprecated. Configure <<remote-clusters-api-key,API key authentication>> instead or follow a guide on how to <<remote-clusters-migrate,migrate remote clusters from certificate to API key authentication>>."]

To add a remote cluster using TLS certificate authentication:

. <<remote-clusters-prerequisites-cert,Review the prerequisites>>
Expand Down Expand Up @@ -80,4 +82,4 @@ generate certificates for all nodes simplifies this task.
include::remote-clusters-connect.asciidoc[]
:!trust-mechanism:

include::{es-ref-dir}/security/authentication/remote-clusters-privileges-cert.asciidoc[leveloffset=+1]
include::{es-ref-dir}/security/authentication/remote-clusters-privileges-cert.asciidoc[leveloffset=+1]
2 changes: 2 additions & 0 deletions docs/reference/modules/remote-clusters.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ is performed on the local cluster and a user's role names are passed to the
remote cluster. In this model, a superuser on the local cluster gains total read
access to the remote cluster, so it is only suitable for clusters that are in
the same security domain. <<remote-clusters-cert>>.
+
deprecated::[9.0.0, "Use <<remote-clusters-api-key,API key based security model>> instead."]

[[sniff-proxy-modes]]
[discrete]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.cluster.node.DiscoveryNode;
import org.elasticsearch.common.logging.DeprecationCategory;
import org.elasticsearch.common.logging.DeprecationLogger;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.util.CollectionUtils;
import org.elasticsearch.core.Nullable;
Expand All @@ -27,10 +29,15 @@
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;

import static org.elasticsearch.transport.RemoteClusterPortSettings.REMOTE_CLUSTER_PROFILE;
import static org.elasticsearch.transport.RemoteClusterService.REMOTE_CLUSTER_HANDSHAKE_ACTION_NAME;

public class RemoteConnectionManager implements ConnectionManager {

private static final Logger logger = LogManager.getLogger(RemoteConnectionManager.class);

private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(RemoteConnectionManager.class);

private final String clusterAlias;
private final RemoteClusterCredentialsManager credentialsManager;
private final ConnectionManager delegate;
Expand All @@ -45,6 +52,12 @@ public class RemoteConnectionManager implements ConnectionManager {
@Override
public void onNodeConnected(DiscoveryNode node, Transport.Connection connection) {
addConnectedNode(node);
try {
// called when a node is successfully connected through a proxy connection
maybeLogDeprecationWarning(wrapConnectionWithRemoteClusterInfo(connection, clusterAlias, credentialsManager));
} catch (Exception e) {
logger.warn("Failed to log deprecation warning.", e);
}
}

@Override
Expand Down Expand Up @@ -102,11 +115,28 @@ public void openConnection(DiscoveryNode node, @Nullable ConnectionProfile profi
node,
profile,
listener.delegateFailureAndWrap(
(l, connection) -> l.onResponse(wrapConnectionWithRemoteClusterInfo(connection, clusterAlias, credentialsManager))
(l, connection) -> l.onResponse(
maybeLogDeprecationWarning(wrapConnectionWithRemoteClusterInfo(connection, clusterAlias, credentialsManager))
)
)
);
}

private InternalRemoteConnection maybeLogDeprecationWarning(InternalRemoteConnection connection) {
if (connection.getClusterCredentials() == null
&& (false == REMOTE_CLUSTER_PROFILE.equals(this.getConnectionProfile().getTransportProfile()))) {
deprecationLogger.warn(
DeprecationCategory.SECURITY,
"remote_cluster_certificate_access-" + connection.getClusterAlias(),
"The remote cluster connection to [{}] is using the certificate-based security model. "
+ "The certificate-based security model is deprecated and will be removed in a future major version. "
+ "Migrate the remote cluster from the certificate-based to the API key-based security model.",
connection.getClusterAlias()
);
}
return connection;
}

@Override
public Transport.Connection getConnection(DiscoveryNode node) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ public void testGetConnection() {
proxyNodes.add(((ProxyConnection) remoteConnectionManager.getConnection(node4)).getConnection().getNode().getId());

assertThat(proxyNodes, containsInAnyOrder("node-2"));

assertWarnings(
"The remote cluster connection to [remote-cluster] is using the certificate-based security model. "
+ "The certificate-based security model is deprecated and will be removed in a future major version. "
+ "Migrate the remote cluster from the certificate-based to the API key-based security model."
);
}

public void testDisconnectedException() {
Expand All @@ -124,19 +130,28 @@ public void testResolveRemoteClusterAlias() throws ExecutionException, Interrupt
assertTrue(future.isDone());

Transport.Connection remoteConnection = remoteConnectionManager.getConnection(remoteNode1);
assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(remoteConnection).get(), equalTo("remote-cluster"));
final String remoteClusterAlias = "remote-cluster";
assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(remoteConnection).get(), equalTo(remoteClusterAlias));

Transport.Connection localConnection = mock(Transport.Connection.class);
assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(localConnection).isPresent(), equalTo(false));

DiscoveryNode remoteNode2 = DiscoveryNodeUtils.create("remote-node-2", address);
Transport.Connection proxyConnection = remoteConnectionManager.getConnection(remoteNode2);
assertThat(proxyConnection, instanceOf(ProxyConnection.class));
assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(proxyConnection).get(), equalTo("remote-cluster"));
assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(proxyConnection).get(), equalTo(remoteClusterAlias));

PlainActionFuture<Transport.Connection> future2 = new PlainActionFuture<>();
remoteConnectionManager.openConnection(remoteNode1, null, future2);
assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(future2.get()).get(), equalTo("remote-cluster"));
assertThat(RemoteConnectionManager.resolveRemoteClusterAlias(future2.get()).get(), equalTo(remoteClusterAlias));

assertWarnings(
"The remote cluster connection to ["
+ remoteClusterAlias
+ "] is using the certificate-based security model. "
+ "The certificate-based security model is deprecated and will be removed in a future major version. "
+ "Migrate the remote cluster from the certificate-based to the API key-based security model."
);
}

public void testRewriteHandshakeAction() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ public enum LogType {
SERVER_JSON("%s_server.json"),
AUDIT("%s_audit.json"),
SEARCH_SLOW("%s_index_search_slowlog.json"),
INDEXING_SLOW("%s_index_indexing_slowlog.json");
INDEXING_SLOW("%s_index_indexing_slowlog.json"),
DEPRECATION("%s_deprecation.json");

private final String filenameFormat;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
* 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.remotecluster;

import org.elasticsearch.client.Request;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.Response;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.core.Strings;
import org.elasticsearch.test.cluster.ElasticsearchCluster;
import org.elasticsearch.test.cluster.LogType;
import org.junit.ClassRule;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;

import static org.hamcrest.Matchers.containsString;

/**
* Tests the deprecation of RCS1.0 (certificate-based) security model.
*/
public class RemoteClusterSecurityRCS1DeprecationIT extends AbstractRemoteClusterSecurityTestCase {

public static final String REMOTE_CLUSTER_ALIAS = "my_remote_cluster";

static {
fulfillingCluster = ElasticsearchCluster.local().name("fulfilling-cluster").nodes(1).apply(commonClusterConfig).build();
queryCluster = ElasticsearchCluster.local().nodes(1).name("query-cluster").apply(commonClusterConfig).build();
}

@ClassRule
public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster);

public void testUsingRCS1GeneratesDeprecationWarning() throws Exception {
final boolean rcs1 = true;
final boolean useProxyMode = randomBoolean();
configureRemoteCluster(REMOTE_CLUSTER_ALIAS, fulfillingCluster, rcs1, useProxyMode, randomBoolean());

{
// Query cluster -> add role for test user
var putRoleRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE);
putRoleRequest.setJsonEntity("""
{
"indices": [
{
"names": ["local_index"],
"privileges": ["read"]
}
]
}""");
assertOK(adminClient().performRequest(putRoleRequest));

// Query cluster -> create user and assign role
var putUserRequest = new Request("PUT", "/_security/user/" + REMOTE_SEARCH_USER);
putUserRequest.setJsonEntity("""
{
"password": "x-pack-test-password",
"roles" : ["remote_search"]
}""");
assertOK(adminClient().performRequest(putUserRequest));

// Query cluster -> create test index
var indexDocRequest = new Request("POST", "/local_index/_doc?refresh=true");
indexDocRequest.setJsonEntity("{\"local_foo\": \"local_bar\"}");
assertOK(client().performRequest(indexDocRequest));

// Fulfilling cluster -> create test indices
Request bulkRequest = new Request("POST", "/_bulk?refresh=true");
bulkRequest.setJsonEntity(Strings.format("""
{ "index": { "_index": "index1" } }
{ "foo": "bar" }
{ "index": { "_index": "secretindex" } }
{ "bar": "foo" }
"""));
assertOK(performRequestAgainstFulfillingCluster(bulkRequest));

// Fulfilling cluster -> add role for remote search user
var putRoleOnRemoteClusterRequest = new Request("PUT", "/_security/role/" + REMOTE_SEARCH_ROLE);
putRoleOnRemoteClusterRequest.setJsonEntity("""
{
"indices": [
{
"names": ["index*"],
"privileges": ["read", "read_cross_cluster"]
}
]
}""");
assertOK(performRequestAgainstFulfillingCluster(putRoleOnRemoteClusterRequest));
}
{
// perform a simple search request, so we can ensure the remote cluster is connected
final Request searchRequest = new Request(
"GET",
String.format(
Locale.ROOT,
"/%s:index1/_search?ccs_minimize_roundtrips=%s",
randomFrom(REMOTE_CLUSTER_ALIAS, "*", "my_remote_*"),
randomBoolean()
)
);
assertOK(performRequestWithRemoteSearchUser(searchRequest));
}
{
// verify that the deprecation warning is logged
try (InputStream log = queryCluster.getNodeLog(0, LogType.DEPRECATION)) {
Streams.readAllLines(
log,
line -> assertThat(
line,
containsString(
"The remote cluster connection to ["
+ REMOTE_CLUSTER_ALIAS
+ "] is using the certificate-based security model. "
+ "The certificate-based security model is deprecated and will be removed in a future major version. "
+ "Migrate the remote cluster from the certificate-based to the API key-based security model."
)
)
);
}
}
}

private Response performRequestWithRemoteSearchUser(final Request request) throws IOException {
request.setOptions(
RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", headerFromRandomAuthMethod(REMOTE_SEARCH_USER, PASS))
);
return client().performRequest(request);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.common.UUIDs;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.core.Strings;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchResponseUtils;
import org.elasticsearch.test.cluster.ElasticsearchCluster;
import org.elasticsearch.test.cluster.LogType;
import org.elasticsearch.test.cluster.local.distribution.DistributionType;
import org.elasticsearch.test.cluster.util.resource.Resource;
import org.elasticsearch.test.junit.RunnableTestRuleAdapter;
Expand All @@ -31,6 +33,7 @@
import org.junit.rules.TestRule;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
Expand Down Expand Up @@ -607,6 +610,7 @@ public void testCrossClusterSearch() throws Exception {
assertThat(exception6.getMessage(), containsString("invalid cross-cluster API key value"));
}
}
assertNoRcs1DeprecationWarnings();
}

@SuppressWarnings("unchecked")
Expand Down Expand Up @@ -681,4 +685,23 @@ private static void selectTasksWithOpaqueId(
}
}
}

private void assertNoRcs1DeprecationWarnings() throws IOException {
for (int i = 0; i < queryCluster.getNumNodes(); i++) {
try (InputStream log = queryCluster.getNodeLog(i, LogType.DEPRECATION)) {
Streams.readAllLines(
log,
line -> assertThat(
line,
not(
containsString(
"The certificate-based security model is deprecated and will be removed in a future major version. "
+ "Migrate the remote cluster from the certificate-based to the API key-based security model."
)
)
)
);
}
}
}
}

0 comments on commit c5ab17c

Please sign in to comment.