2525import org .junit .rules .TestRule ;
2626
2727import java .io .IOException ;
28+ import java .io .UncheckedIOException ;
2829import java .util .Arrays ;
2930import java .util .List ;
3031import java .util .Locale ;
3839
3940public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRemoteClusterSecurityTestCase {
4041
41- private static final AtomicReference <Map <String , Object >> API_KEY_MAP_REF = new AtomicReference <>();
42+ private static final AtomicReference <Map <String , Object >> MY_REMOTE_API_KEY_MAP_REF = new AtomicReference <>();
43+ private static final String TEST_ACCESS_JSON = """
44+ {
45+ "search": [
46+ {
47+ "names": ["index*", "not_found_index"]
48+ }
49+ ]
50+ }""" ;
51+ private static final String [] MATCHING_CERTIFICATE_IDENTITY_PATTERNS = new String [] {
52+ "CN=instance" ,
53+ "^CN=instance$" ,
54+ "(?i)" + "^CN=instance$" ,
55+ "^CN=[A-Za-z0-9_]+$" };
4256
4357 static {
4458 fulfillingCluster = ElasticsearchCluster .local ()
@@ -49,8 +63,12 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe
4963 .setting ("xpack.security.remote_cluster_server.ssl.enabled" , "true" )
5064 .setting ("xpack.security.remote_cluster_server.ssl.key" , "remote-cluster.key" )
5165 .setting ("xpack.security.remote_cluster_server.ssl.certificate" , "remote-cluster.crt" )
66+ .setting ("xpack.security.audit.enabled" , "true" )
67+ .setting (
68+ "xpack.security.audit.logfile.events.include" ,
69+ "[authentication_success, authentication_failed, access_denied, access_granted]"
70+ )
5271 .configFile ("signing_ca.crt" , Resource .fromClasspath ("signing/root.crt" ))
53- .setting ("cluster.remote.signing.certificate_authorities" , "signing_ca.crt" )
5472 .keystore ("xpack.security.remote_cluster_server.ssl.secure_key_passphrase" , "remote-cluster-password" )
5573 .build ();
5674
@@ -60,22 +78,14 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe
6078 .setting ("xpack.security.remote_cluster_client.ssl.enabled" , "true" )
6179 .setting ("xpack.security.remote_cluster_client.ssl.certificate_authorities" , "remote-cluster-ca.crt" )
6280 .configFile ("signing.crt" , Resource .fromClasspath ("signing/signing.crt" ))
63- .setting ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
6481 .configFile ("signing.key" , Resource .fromClasspath ("signing/signing.key" ))
65- .setting ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
6682 .keystore ("cluster.remote.my_remote_cluster.credentials" , () -> {
67- if (API_KEY_MAP_REF .get () == null ) {
68- final Map <String , Object > apiKeyMap = createCrossClusterAccessApiKey ("""
69- {
70- "search": [
71- {
72- "names": ["index*", "not_found_index"]
73- }
74- ]
75- }""" );
76- API_KEY_MAP_REF .set (apiKeyMap );
83+ if (MY_REMOTE_API_KEY_MAP_REF .get () == null ) {
84+ MY_REMOTE_API_KEY_MAP_REF .set (
85+ createCrossClusterAccessApiKey (TEST_ACCESS_JSON , randomFrom (MATCHING_CERTIFICATE_IDENTITY_PATTERNS ))
86+ );
7787 }
78- return (String ) API_KEY_MAP_REF .get ().get ("encoded" );
88+ return (String ) MY_REMOTE_API_KEY_MAP_REF .get ().get ("encoded" );
7989 })
8090 .keystore ("cluster.remote.invalid_remote.credentials" , randomEncodedApiKey ())
8191 .build ();
@@ -86,33 +96,113 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe
8696 public static TestRule clusterRule = RuleChain .outerRule (fulfillingCluster ).around (queryCluster );
8797
8898 public void testCrossClusterSearchWithCrossClusterApiKeySigning () throws Exception {
89- indexTestData ();
90- assertCrossClusterSearchSuccessfulWithResult ();
99+ updateClusterSettings (
100+ Settings .builder ()
101+ .put ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
102+ .put ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
103+ .build ()
104+ );
91105
92- // Change the CA to something that doesn't trust the signing cert
93106 updateClusterSettingsFulfillingCluster (
94- Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "transport-ca .crt" ).build ()
107+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "signing_ca .crt" ).build ()
95108 );
96- assertCrossClusterAuthFail ();
97109
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 ());
110+ indexTestData ();
100111
101- assertCrossClusterSearchSuccessfulWithoutResult ();
112+ // Make sure we can search if cert trusted
113+ {
114+ assertCrossClusterSearchSuccessfulWithResult ();
115+ }
116+
117+ // Test CA that does not trust cert
118+ {
119+ // Change the CA to something that doesn't trust the signing cert
120+ updateClusterSettingsFulfillingCluster (
121+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "transport-ca.crt" ).build ()
122+ );
123+ assertCrossClusterAuthFail ("Failed to verify cross cluster api key signature certificate from [(" );
102124
103- // TODO add test for certificate identity configured for API key but no signature provided (should 401)
125+ // Change the CA to the default trust store
126+ updateClusterSettingsFulfillingCluster (Settings .builder ().putNull ("cluster.remote.signing.certificate_authorities" ).build ());
127+ assertCrossClusterAuthFail ("Failed to verify cross cluster api key signature certificate from [(" );
104128
105- // TODO add test for certificate identity not configured for API key but signature provided (should 200)
129+ // Update settings on query cluster to ignore unavailable remotes
130+ updateClusterSettings (
131+ Settings .builder ().put ("cluster.remote.my_remote_cluster.skip_unavailable" , Boolean .toString (true )).build ()
132+ );
133+ assertCrossClusterSearchSuccessfulWithoutResult ();
106134
107- // TODO add test for certificate identity not configured for API key but wrong signature provided (should 401)
135+ // Reset skip_unavailable
136+ updateClusterSettings (
137+ Settings .builder ().put ("cluster.remote.my_remote_cluster.skip_unavailable" , Boolean .toString (false )).build ()
138+ );
108139
109- // TODO add test for certificate identity regex matching (should 200)
140+ // Reset ca cert
141+ updateClusterSettingsFulfillingCluster (
142+ Settings .builder ().put ("cluster.remote.signing.certificate_authorities" , "signing_ca.crt" ).build ()
143+ );
144+ // Confirm reset was successful
145+ assertCrossClusterSearchSuccessfulWithResult ();
146+ }
147+
148+ // Test no signature provided
149+ {
150+ updateClusterSettings (
151+ Settings .builder ()
152+ .putNull ("cluster.remote.my_remote_cluster.signing.certificate" )
153+ .putNull ("cluster.remote.my_remote_cluster.signing.key" )
154+ .build ()
155+ );
156+
157+ assertCrossClusterAuthFail (
158+ "API key (type:[cross_cluster], id:["
159+ + MY_REMOTE_API_KEY_MAP_REF .get ().get ("id" )
160+ + "]) requires certificate identity matching ["
161+ );
162+
163+ // Reset
164+ updateClusterSettings (
165+ Settings .builder ()
166+ .put ("cluster.remote.my_remote_cluster.signing.certificate" , "signing.crt" )
167+ .put ("cluster.remote.my_remote_cluster.signing.key" , "signing.key" )
168+ .build ()
169+ );
170+ }
171+
172+ // Test API key without certificate identity and send signature anyway
173+ {
174+ updateCrossClusterAccessApiKey (null );
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+ updateCrossClusterAccessApiKey (randomFrom (MATCHING_CERTIFICATE_IDENTITY_PATTERNS ));
186+ }
187+
188+ // Test API key with non-matching certificate identity is rejected
189+ {
190+ var nonMatchingCertificateIdentity = randomFrom ("" , "no-match" , "^CN= instance$" , "^CN=instance.$" , "^cn=instance$" );
191+ updateCrossClusterAccessApiKey (nonMatchingCertificateIdentity );
192+ assertCrossClusterAuthFail (
193+ "DN from provided certificate [CN=instance] does not match API Key certificate identity pattern ["
194+ + nonMatchingCertificateIdentity
195+ + "]"
196+ );
197+ // Reset
198+ updateCrossClusterAccessApiKey (randomFrom (MATCHING_CERTIFICATE_IDENTITY_PATTERNS ));
199+ }
110200 }
111201
112- private void assertCrossClusterAuthFail () {
202+ private void assertCrossClusterAuthFail (String expectedMessage ) {
113203 var responseException = assertThrows (ResponseException .class , () -> simpleCrossClusterSearch (randomBoolean ()));
114204 assertThat (responseException .getResponse ().getStatusLine ().getStatusCode (), equalTo (401 ));
115- assertThat (responseException .getMessage (), containsString ("Failed to verify cross cluster api key signature certificate from [(" ));
205+ assertThat (responseException .getMessage (), containsString (expectedMessage ));
116206 }
117207
118208 private void assertCrossClusterSearchSuccessfulWithoutResult () throws IOException {
@@ -227,4 +317,46 @@ private Response performRequestWithRemoteAccessUser(final Request request) throw
227317 request .setOptions (RequestOptions .DEFAULT .toBuilder ().addHeader ("Authorization" , basicAuthHeaderValue (REMOTE_SEARCH_USER , PASS )));
228318 return client ().performRequest (request );
229319 }
320+
321+ protected static Map <String , Object > createCrossClusterAccessApiKey (String accessJson , String certificateIdentity ) {
322+ initFulfillingClusterClient ();
323+ final var createCrossClusterApiKeyRequest = new Request ("POST" , "/_security/cross_cluster/api_key" );
324+ createCrossClusterApiKeyRequest .setJsonEntity (Strings .format ("""
325+ {
326+ "name": "cross_cluster_access_key",
327+ "certificate_identity": "%s",
328+ "access": %s
329+ }""" , certificateIdentity , accessJson ));
330+ try {
331+ final Response createCrossClusterApiKeyResponse = performRequestWithAdminUser (
332+ fulfillingClusterClient ,
333+ createCrossClusterApiKeyRequest
334+ );
335+ assertOK (createCrossClusterApiKeyResponse );
336+ return responseAsMap (createCrossClusterApiKeyResponse );
337+ } catch (IOException e ) {
338+ throw new UncheckedIOException (e );
339+ }
340+ }
341+
342+ protected static Map <String , Object > updateCrossClusterAccessApiKey (String certificateIdentity ) {
343+ initFulfillingClusterClient ();
344+ final var createCrossClusterApiKeyRequest = new Request (
345+ "PUT" ,
346+ "/_security/cross_cluster/api_key/" + MY_REMOTE_API_KEY_MAP_REF .get ().get ("id" )
347+ );
348+ createCrossClusterApiKeyRequest .setJsonEntity (
349+ "{\" certificate_identity\" : " + (certificateIdentity != null ? "\" " + certificateIdentity + "\" " : "null" ) + "}"
350+ );
351+ try {
352+ final Response createCrossClusterApiKeyResponse = performRequestWithAdminUser (
353+ fulfillingClusterClient ,
354+ createCrossClusterApiKeyRequest
355+ );
356+ assertOK (createCrossClusterApiKeyResponse );
357+ return responseAsMap (createCrossClusterApiKeyResponse );
358+ } catch (IOException e ) {
359+ throw new UncheckedIOException (e );
360+ }
361+ }
230362}
0 commit comments