88package org .elasticsearch .xpack .remotecluster ;
99
1010import io .netty .handler .codec .http .HttpMethod ;
11-
1211import org .elasticsearch .action .search .SearchResponse ;
1312import org .elasticsearch .client .Request ;
1413import org .elasticsearch .client .RequestOptions ;
2524import org .junit .rules .TestRule ;
2625
2726import java .io .IOException ;
27+ import java .io .UncheckedIOException ;
2828import java .util .Arrays ;
2929import java .util .List ;
3030import java .util .Locale ;
3838
3939public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRemoteClusterSecurityTestCase {
4040
41- private static final AtomicReference <Map <String , Object >> API_KEY_MAP_REF = new AtomicReference <>();
41+ private static final AtomicReference <Map <String , Object >> MY_REMOTE_API_KEY_MAP_REF = new AtomicReference <>();
4242
4343 static {
4444 fulfillingCluster = ElasticsearchCluster .local ()
@@ -49,9 +49,14 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe
4949 .setting ("xpack.security.remote_cluster_server.ssl.enabled" , "true" )
5050 .setting ("xpack.security.remote_cluster_server.ssl.key" , "remote-cluster.key" )
5151 .setting ("xpack.security.remote_cluster_server.ssl.certificate" , "remote-cluster.crt" )
52+ .setting ("xpack.security.audit.enabled" , "true" )
53+ .setting (
54+ "xpack.security.audit.logfile.events.include" ,
55+ "[authentication_success, authentication_failed, access_denied, access_granted]"
56+ )
5257 .configFile ("signing_ca.crt" , Resource .fromClasspath ("signing/root.crt" ))
53- .setting ("cluster.remote.signing.certificate_authorities" , "signing_ca.crt" )
5458 .keystore ("xpack.security.remote_cluster_server.ssl.secure_key_passphrase" , "remote-cluster-password" )
59+ .setting ("logger.org.elasticsearch.xpack.security" , "trace" ) // useful for human debugging
5560 .build ();
5661
5762 queryCluster = ElasticsearchCluster .local ()
@@ -60,22 +65,25 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe
6065 .setting ("xpack.security.remote_cluster_client.ssl.enabled" , "true" )
6166 .setting ("xpack.security.remote_cluster_client.ssl.certificate_authorities" , "remote-cluster-ca.crt" )
6267 .configFile ("signing.crt" , Resource .fromClasspath ("signing/signing.crt" ))
63- .setting ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
6468 .configFile ("signing.key" , Resource .fromClasspath ("signing/signing.key" ))
65- .setting ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
6669 .keystore ("cluster.remote.my_remote_cluster.credentials" , () -> {
67- if (API_KEY_MAP_REF .get () == null ) {
68- final Map < String , Object > apiKeyMap = createCrossClusterAccessApiKey ( """
70+ if (MY_REMOTE_API_KEY_MAP_REF .get () == null ) {
71+ final var accessJson = """
6972 {
7073 "search": [
7174 {
7275 "names": ["index*", "not_found_index"]
7376 }
7477 ]
75- }""" );
76- API_KEY_MAP_REF .set (apiKeyMap );
78+ }""" ;
79+ MY_REMOTE_API_KEY_MAP_REF .set (
80+ createCrossClusterAccessApiKey (
81+ accessJson ,
82+ randomFrom ("CN=instance" , "^CN=instance$" , "(?i)^CN=instance$" , "^CN=[A-Za-z0-9_]+$" )
83+ )
84+ );
7785 }
78- return (String ) API_KEY_MAP_REF .get ().get ("encoded" );
86+ return (String ) MY_REMOTE_API_KEY_MAP_REF .get ().get ("encoded" );
7987 })
8088 .keystore ("cluster.remote.invalid_remote.credentials" , randomEncodedApiKey ())
8189 .build ();
@@ -86,33 +94,102 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe
8694 public static TestRule clusterRule = RuleChain .outerRule (fulfillingCluster ).around (queryCluster );
8795
8896 public void testCrossClusterSearchWithCrossClusterApiKeySigning () throws Exception {
89- indexTestData ();
90- assertCrossClusterSearchSuccessfulWithResult ();
97+ updateClusterSettings (
98+ Settings .builder ()
99+ .put ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
100+ .put ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
101+ .build ()
102+ );
91103
92- // Change the CA to something that doesn't trust the signing cert
93104 updateClusterSettingsFulfillingCluster (
94- Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "transport-ca .crt" ).build ()
105+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "signing_ca .crt" ).build ()
95106 );
96- assertCrossClusterAuthFail ();
97107
98- // Update settings on query cluster to ignore unavailable remotes
99- updateClusterSettings (Settings .builder ().put ("cluster.remote.my_remote_cluster.skip_unavailable" , Boolean .toString (true )).build ());
108+ indexTestData ();
100109
101- assertCrossClusterSearchSuccessfulWithoutResult ();
110+ // Make sure we can search if cert trusted
111+ {
112+ assertCrossClusterSearchSuccessfulWithResult ();
113+ }
102114
103- // TODO add test for certificate identity configured for API key but no signature provided (should 401)
115+ // Test CA that does not trust cert
116+ {
117+ // Change the CA to something that doesn't trust the signing cert
118+ updateClusterSettingsFulfillingCluster (
119+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "transport-ca.crt" ).build ()
120+ );
121+ assertCrossClusterAuthFail ("Failed to verify cross cluster api key signature certificate from [(" );
122+
123+ // Change the CA to the default trust store
124+ updateClusterSettingsFulfillingCluster (Settings .builder ().putNull ("cluster.remote.signing.certificate_authorities" ).build ());
125+ assertCrossClusterAuthFail ("Failed to verify cross cluster api key signature certificate from [(" );
126+
127+ // Update settings on query cluster to ignore unavailable remotes
128+ updateClusterSettings (
129+ Settings .builder ().put ("cluster.remote.my_remote_cluster.skip_unavailable" , Boolean .toString (true )).build ()
130+ );
131+ assertCrossClusterSearchSuccessfulWithoutResult ();
104132
105- // TODO add test for certificate identity not configured for API key but signature provided (should 200)
133+ // Reset skip_unavailable
134+ updateClusterSettings (
135+ Settings .builder ().put ("cluster.remote.my_remote_cluster.skip_unavailable" , Boolean .toString (false )).build ()
136+ );
106137
107- // TODO add test for certificate identity not configured for API key but wrong signature provided (should 401)
138+ // Reset ca cert
139+ updateClusterSettingsFulfillingCluster (
140+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "signing_ca.crt" ).build ()
141+ );
142+ // Confirm reset was successful
143+ assertCrossClusterSearchSuccessfulWithResult ();
144+ }
145+
146+ // Test no signature provided
147+ {
148+ updateClusterSettings (
149+ Settings .builder ()
150+ .putNull ("cluster.remote.my_remote_cluster.signing.certificate" )
151+ .putNull ("cluster.remote.my_remote_cluster.signing.key" )
152+ .build ()
153+ );
154+ assertCrossClusterAuthFail ("Expected signature for cross cluster api key but no signature provided" );
108155
109- // TODO add test for certificate identity regex matching (should 200)
156+ // Reset
157+ updateClusterSettings (
158+ Settings .builder ()
159+ .put ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
160+ .put ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
161+ .build ()
162+ );
163+ }
164+
165+ // Test API key without certificate identity and send signature anyway
166+ {
167+ final var accessJson = """
168+ {
169+ "search": [
170+ {
171+ "names": ["index*", "not_found_index"]
172+ }
173+ ]
174+ }""" ;
175+ MY_REMOTE_API_KEY_MAP_REF .set (createCrossClusterAccessApiKey (accessJson ));
176+ assertCrossClusterSearchSuccessfulWithResult ();
177+
178+ // Change the CA to the default trust store to make sure untrusted signature fails auth even if it's not required
179+ updateClusterSettingsFulfillingCluster (Settings .builder ().putNull ("cluster.remote.signing.certificate_authorities" ).build ());
180+ assertCrossClusterAuthFail ("Failed to verify cross cluster api key signature certificate from [(" );
181+
182+ // Reset
183+ updateClusterSettingsFulfillingCluster (
184+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "signing_ca.crt" ).build ()
185+ );
186+ }
110187 }
111188
112- private void assertCrossClusterAuthFail () {
189+ private void assertCrossClusterAuthFail (String expectedMessage ) {
113190 var responseException = assertThrows (ResponseException .class , () -> simpleCrossClusterSearch (randomBoolean ()));
114191 assertThat (responseException .getResponse ().getStatusLine ().getStatusCode (), equalTo (401 ));
115- assertThat (responseException .getMessage (), containsString ("Failed to verify cross cluster api key signature certificate from [(" ));
192+ assertThat (responseException .getMessage (), containsString (expectedMessage ));
116193 }
117194
118195 private void assertCrossClusterSearchSuccessfulWithoutResult () throws IOException {
@@ -227,4 +304,25 @@ private Response performRequestWithRemoteAccessUser(final Request request) throw
227304 request .setOptions (RequestOptions .DEFAULT .toBuilder ().addHeader ("Authorization" , basicAuthHeaderValue (REMOTE_SEARCH_USER , PASS )));
228305 return client ().performRequest (request );
229306 }
307+
308+ protected static Map <String , Object > createCrossClusterAccessApiKey (String accessJson , String certificateIdentity ) {
309+ initFulfillingClusterClient ();
310+ final var createCrossClusterApiKeyRequest = new Request ("POST" , "/_security/cross_cluster/api_key" );
311+ createCrossClusterApiKeyRequest .setJsonEntity (Strings .format ("""
312+ {
313+ "name": "cross_cluster_access_key",
314+ "certificate_identity": "%s",
315+ "access": %s
316+ }""" , certificateIdentity , accessJson ));
317+ try {
318+ final Response createCrossClusterApiKeyResponse = performRequestWithAdminUser (
319+ fulfillingClusterClient ,
320+ createCrossClusterApiKeyRequest
321+ );
322+ assertOK (createCrossClusterApiKeyResponse );
323+ return responseAsMap (createCrossClusterApiKeyResponse );
324+ } catch (IOException e ) {
325+ throw new UncheckedIOException (e );
326+ }
327+ }
230328}
0 commit comments