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,8 +49,12 @@ 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" )
5559 .build ();
5660
@@ -60,22 +64,25 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe
6064 .setting ("xpack.security.remote_cluster_client.ssl.enabled" , "true" )
6165 .setting ("xpack.security.remote_cluster_client.ssl.certificate_authorities" , "remote-cluster-ca.crt" )
6266 .configFile ("signing.crt" , Resource .fromClasspath ("signing/signing.crt" ))
63- .setting ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
6467 .configFile ("signing.key" , Resource .fromClasspath ("signing/signing.key" ))
65- .setting ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
6668 .keystore ("cluster.remote.my_remote_cluster.credentials" , () -> {
67- if (API_KEY_MAP_REF .get () == null ) {
68- final Map < String , Object > apiKeyMap = createCrossClusterAccessApiKey ( """
69+ if (MY_REMOTE_API_KEY_MAP_REF .get () == null ) {
70+ final var accessJson = """
6971 {
7072 "search": [
7173 {
7274 "names": ["index*", "not_found_index"]
7375 }
7476 ]
75- }""" );
76- API_KEY_MAP_REF .set (apiKeyMap );
77+ }""" ;
78+ MY_REMOTE_API_KEY_MAP_REF .set (
79+ createCrossClusterAccessApiKey (
80+ accessJson ,
81+ randomFrom ("CN=instance" , "^CN=instance$" , "(?i)^CN=instance$" , "^CN=[A-Za-z0-9_]+$" )
82+ )
83+ );
7784 }
78- return (String ) API_KEY_MAP_REF .get ().get ("encoded" );
85+ return (String ) MY_REMOTE_API_KEY_MAP_REF .get ().get ("encoded" );
7986 })
8087 .keystore ("cluster.remote.invalid_remote.credentials" , randomEncodedApiKey ())
8188 .build ();
@@ -86,33 +93,102 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe
8693 public static TestRule clusterRule = RuleChain .outerRule (fulfillingCluster ).around (queryCluster );
8794
8895 public void testCrossClusterSearchWithCrossClusterApiKeySigning () throws Exception {
89- indexTestData ();
90- assertCrossClusterSearchSuccessfulWithResult ();
96+ updateClusterSettings (
97+ Settings .builder ()
98+ .put ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
99+ .put ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
100+ .build ()
101+ );
91102
92- // Change the CA to something that doesn't trust the signing cert
93103 updateClusterSettingsFulfillingCluster (
94- Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "transport-ca .crt" ).build ()
104+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "signing_ca .crt" ).build ()
95105 );
96- assertCrossClusterAuthFail ();
97106
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 ());
107+ indexTestData ();
100108
101- assertCrossClusterSearchSuccessfulWithoutResult ();
109+ // Make sure we can search if cert trusted
110+ {
111+ assertCrossClusterSearchSuccessfulWithResult ();
112+ }
102113
103- // TODO add test for certificate identity configured for API key but no signature provided (should 401)
114+ // Test CA that does not trust cert
115+ {
116+ // Change the CA to something that doesn't trust the signing cert
117+ updateClusterSettingsFulfillingCluster (
118+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "transport-ca.crt" ).build ()
119+ );
120+ assertCrossClusterAuthFail ("Failed to verify cross cluster api key signature certificate from [(" );
121+
122+ // Change the CA to the default trust store
123+ updateClusterSettingsFulfillingCluster (Settings .builder ().putNull ("cluster.remote.signing.certificate_authorities" ).build ());
124+ assertCrossClusterAuthFail ("Failed to verify cross cluster api key signature certificate from [(" );
125+
126+ // Update settings on query cluster to ignore unavailable remotes
127+ updateClusterSettings (
128+ Settings .builder ().put ("cluster.remote.my_remote_cluster.skip_unavailable" , Boolean .toString (true )).build ()
129+ );
130+ assertCrossClusterSearchSuccessfulWithoutResult ();
104131
105- // TODO add test for certificate identity not configured for API key but signature provided (should 200)
132+ // Reset skip_unavailable
133+ updateClusterSettings (
134+ Settings .builder ().put ("cluster.remote.my_remote_cluster.skip_unavailable" , Boolean .toString (false )).build ()
135+ );
106136
107- // TODO add test for certificate identity not configured for API key but wrong signature provided (should 401)
137+ // Reset ca cert
138+ updateClusterSettingsFulfillingCluster (
139+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "signing_ca.crt" ).build ()
140+ );
141+ // Confirm reset was successful
142+ assertCrossClusterSearchSuccessfulWithResult ();
143+ }
144+
145+ // Test no signature provided
146+ {
147+ updateClusterSettings (
148+ Settings .builder ()
149+ .putNull ("cluster.remote.my_remote_cluster.signing.certificate" )
150+ .putNull ("cluster.remote.my_remote_cluster.signing.key" )
151+ .build ()
152+ );
153+ assertCrossClusterAuthFail ("Expected signature for cross cluster API key, but no signature was provided" );
108154
109- // TODO add test for certificate identity regex matching (should 200)
155+ // Reset
156+ updateClusterSettings (
157+ Settings .builder ()
158+ .put ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
159+ .put ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
160+ .build ()
161+ );
162+ }
163+
164+ // Test API key without certificate identity and send signature anyway
165+ {
166+ final var accessJson = """
167+ {
168+ "search": [
169+ {
170+ "names": ["index*", "not_found_index"]
171+ }
172+ ]
173+ }""" ;
174+ MY_REMOTE_API_KEY_MAP_REF .set (createCrossClusterAccessApiKey (accessJson ));
175+ assertCrossClusterSearchSuccessfulWithResult ();
176+
177+ // Change the CA to the default trust store to make sure untrusted signature fails auth even if it's not required
178+ updateClusterSettingsFulfillingCluster (Settings .builder ().putNull ("cluster.remote.signing.certificate_authorities" ).build ());
179+ assertCrossClusterAuthFail ("Failed to verify cross cluster api key signature certificate from [(" );
180+
181+ // Reset
182+ updateClusterSettingsFulfillingCluster (
183+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "signing_ca.crt" ).build ()
184+ );
185+ }
110186 }
111187
112- private void assertCrossClusterAuthFail () {
188+ private void assertCrossClusterAuthFail (String expectedMessage ) {
113189 var responseException = assertThrows (ResponseException .class , () -> simpleCrossClusterSearch (randomBoolean ()));
114190 assertThat (responseException .getResponse ().getStatusLine ().getStatusCode (), equalTo (401 ));
115- assertThat (responseException .getMessage (), containsString ("Failed to verify cross cluster api key signature certificate from [(" ));
191+ assertThat (responseException .getMessage (), containsString (expectedMessage ));
116192 }
117193
118194 private void assertCrossClusterSearchSuccessfulWithoutResult () throws IOException {
@@ -227,4 +303,25 @@ private Response performRequestWithRemoteAccessUser(final Request request) throw
227303 request .setOptions (RequestOptions .DEFAULT .toBuilder ().addHeader ("Authorization" , basicAuthHeaderValue (REMOTE_SEARCH_USER , PASS )));
228304 return client ().performRequest (request );
229305 }
306+
307+ protected static Map <String , Object > createCrossClusterAccessApiKey (String accessJson , String certificateIdentity ) {
308+ initFulfillingClusterClient ();
309+ final var createCrossClusterApiKeyRequest = new Request ("POST" , "/_security/cross_cluster/api_key" );
310+ createCrossClusterApiKeyRequest .setJsonEntity (Strings .format ("""
311+ {
312+ "name": "cross_cluster_access_key",
313+ "certificate_identity": "%s",
314+ "access": %s
315+ }""" , certificateIdentity , accessJson ));
316+ try {
317+ final Response createCrossClusterApiKeyResponse = performRequestWithAdminUser (
318+ fulfillingClusterClient ,
319+ createCrossClusterApiKeyRequest
320+ );
321+ assertOK (createCrossClusterApiKeyResponse );
322+ return responseAsMap (createCrossClusterApiKeyResponse );
323+ } catch (IOException e ) {
324+ throw new UncheckedIOException (e );
325+ }
326+ }
230327}
0 commit comments