- Notifications
You must be signed in to change notification settings - Fork 25.6k
Validate Certificate Identity provided in Cross Cluster API Key Certificate #136299
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 16 commits
99fac10 0cc2ea3 69c9a17 eb7a3f7 d63b4ec e598e69 9022c68 b4dfd67 59dbd74 649a7e4 4d07535 d1b780d 2203889 ed4093a 094c15f a71ca11 466badf 48e5321 f40b0c3 8a16d08 039e8ba 5b52cbe aec1249 566770e a758ba0 897f87a f88aecb 329ede5 File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| pr: 136299 | ||
| summary: Validate certificate identity from cross cluster creds | ||
| area: Security | ||
| type: enhancement | ||
| issues: [] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| | @@ -25,6 +25,7 @@ | |
| import org.junit.rules.TestRule; | ||
| | ||
| import java.io.IOException; | ||
| import java.io.UncheckedIOException; | ||
| import java.util.Arrays; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| | @@ -38,7 +39,7 @@ | |
| | ||
| public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRemoteClusterSecurityTestCase { | ||
| | ||
| private static final AtomicReference<Map<String, Object>> API_KEY_MAP_REF = new AtomicReference<>(); | ||
| private static final AtomicReference<Map<String, Object>> MY_REMOTE_API_KEY_MAP_REF = new AtomicReference<>(); | ||
| | ||
| static { | ||
| fulfillingCluster = ElasticsearchCluster.local() | ||
| | @@ -49,8 +50,12 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe | |
| .setting("xpack.security.remote_cluster_server.ssl.enabled", "true") | ||
| .setting("xpack.security.remote_cluster_server.ssl.key", "remote-cluster.key") | ||
| .setting("xpack.security.remote_cluster_server.ssl.certificate", "remote-cluster.crt") | ||
| .setting("xpack.security.audit.enabled", "true") | ||
| .setting( | ||
| "xpack.security.audit.logfile.events.include", | ||
| "[authentication_success, authentication_failed, access_denied, access_granted]" | ||
| ) | ||
| .configFile("signing_ca.crt", Resource.fromClasspath("signing/root.crt")) | ||
| .setting("cluster.remote.signing.certificate_authorities", "signing_ca.crt") | ||
| .keystore("xpack.security.remote_cluster_server.ssl.secure_key_passphrase", "remote-cluster-password") | ||
| .build(); | ||
| | ||
| | @@ -60,22 +65,25 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe | |
| .setting("xpack.security.remote_cluster_client.ssl.enabled", "true") | ||
| .setting("xpack.security.remote_cluster_client.ssl.certificate_authorities", "remote-cluster-ca.crt") | ||
| .configFile("signing.crt", Resource.fromClasspath("signing/signing.crt")) | ||
| .setting("cluster.remote.my_remote_cluster.signing.certificate", "signing.crt") | ||
| .configFile("signing.key", Resource.fromClasspath("signing/signing.key")) | ||
| .setting("cluster.remote.my_remote_cluster.signing.key", "signing.key") | ||
| .keystore("cluster.remote.my_remote_cluster.credentials", () -> { | ||
| if (API_KEY_MAP_REF.get() == null) { | ||
| final Map<String, Object> apiKeyMap = createCrossClusterAccessApiKey(""" | ||
| if (MY_REMOTE_API_KEY_MAP_REF.get() == null) { | ||
| final var accessJson = """ | ||
| { | ||
| "search": [ | ||
| { | ||
| "names": ["index*", "not_found_index"] | ||
| } | ||
| ] | ||
| }"""); | ||
| API_KEY_MAP_REF.set(apiKeyMap); | ||
| }"""; | ||
| MY_REMOTE_API_KEY_MAP_REF.set( | ||
| createCrossClusterAccessApiKey( | ||
| accessJson, | ||
| randomFrom("CN=instance", "^CN=instance$", "(?i)^CN=instance$", "^CN=[A-Za-z0-9_]+$") | ||
| ) | ||
| ); | ||
| } | ||
| return (String) API_KEY_MAP_REF.get().get("encoded"); | ||
| return (String) MY_REMOTE_API_KEY_MAP_REF.get().get("encoded"); | ||
| }) | ||
| .keystore("cluster.remote.invalid_remote.credentials", randomEncodedApiKey()) | ||
| .build(); | ||
| | @@ -86,33 +94,107 @@ public class RemoteClusterSecurityCrossClusterApiKeySigningIT extends AbstractRe | |
| public static TestRule clusterRule = RuleChain.outerRule(fulfillingCluster).around(queryCluster); | ||
| | ||
| public void testCrossClusterSearchWithCrossClusterApiKeySigning() throws Exception { | ||
| indexTestData(); | ||
| assertCrossClusterSearchSuccessfulWithResult(); | ||
| updateClusterSettings( | ||
| Settings.builder() | ||
| .put("cluster.remote.my_remote_cluster.signing.certificate", "signing.crt") | ||
| .put("cluster.remote.my_remote_cluster.signing.key", "signing.key") | ||
| .build() | ||
| ); | ||
| | ||
| // Change the CA to something that doesn't trust the signing cert | ||
| updateClusterSettingsFulfillingCluster( | ||
| Settings.builder().put("cluster.remote.signing.certificate_authorities", "transport-ca.crt").build() | ||
| Settings.builder().put("cluster.remote.signing.certificate_authorities", "signing_ca.crt").build() | ||
| ); | ||
| assertCrossClusterAuthFail(); | ||
| | ||
| // Update settings on query cluster to ignore unavailable remotes | ||
| updateClusterSettings(Settings.builder().put("cluster.remote.my_remote_cluster.skip_unavailable", Boolean.toString(true)).build()); | ||
| indexTestData(); | ||
| | ||
| assertCrossClusterSearchSuccessfulWithoutResult(); | ||
| // Make sure we can search if cert trusted | ||
| { | ||
| assertCrossClusterSearchSuccessfulWithResult(); | ||
| } | ||
| | ||
| // TODO add test for certificate identity configured for API key but no signature provided (should 401) | ||
| // Test CA that does not trust cert | ||
| { | ||
| // Change the CA to something that doesn't trust the signing cert | ||
| updateClusterSettingsFulfillingCluster( | ||
| Settings.builder().put("cluster.remote.signing.certificate_authorities", "transport-ca.crt").build() | ||
| ); | ||
| assertCrossClusterAuthFail("Failed to verify cross cluster api key signature certificate from [("); | ||
| | ||
| // TODO add test for certificate identity not configured for API key but signature provided (should 200) | ||
| // Change the CA to the default trust store | ||
| updateClusterSettingsFulfillingCluster(Settings.builder().putNull("cluster.remote.signing.certificate_authorities").build()); | ||
| assertCrossClusterAuthFail("Failed to verify cross cluster api key signature certificate from [("); | ||
| | ||
| // Update settings on query cluster to ignore unavailable remotes | ||
| updateClusterSettings( | ||
| Settings.builder().put("cluster.remote.my_remote_cluster.skip_unavailable", Boolean.toString(true)).build() | ||
| ); | ||
| assertCrossClusterSearchSuccessfulWithoutResult(); | ||
| | ||
| // Reset skip_unavailable | ||
| updateClusterSettings( | ||
| Settings.builder().put("cluster.remote.my_remote_cluster.skip_unavailable", Boolean.toString(false)).build() | ||
| ); | ||
| | ||
| // Reset ca cert | ||
| updateClusterSettingsFulfillingCluster( | ||
| Settings.builder().put("cluster.remote.signing.certificate_authorities", "signing_ca.crt").build() | ||
| ); | ||
| // Confirm reset was successful | ||
| assertCrossClusterSearchSuccessfulWithResult(); | ||
| } | ||
| | ||
| // Test no signature provided | ||
| { | ||
| updateClusterSettings( | ||
| Settings.builder() | ||
| .putNull("cluster.remote.my_remote_cluster.signing.certificate") | ||
| .putNull("cluster.remote.my_remote_cluster.signing.key") | ||
| .build() | ||
| ); | ||
| | ||
| // TODO add test for certificate identity not configured for API key but wrong signature provided (should 401) | ||
| assertCrossClusterAuthFail( | ||
| "API key (type:[cross_cluster], id:[" | ||
| + MY_REMOTE_API_KEY_MAP_REF.get().get("id") | ||
| + "]) requires certificate identity matching [" | ||
| ); | ||
| | ||
| // TODO add test for certificate identity regex matching (should 200) | ||
| Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have this test? Contributor Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, we do Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe I'm blind, but I can't see a case where we have
That is, I can't see where we check the identity checking rejects incorrect identities. Contributor Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, I misunderstood your comment. That was an oversight on my part - testing it only in | ||
| // Reset | ||
| updateClusterSettings( | ||
| Settings.builder() | ||
| .put("cluster.remote.my_remote_cluster.signing.certificate", "signing.crt") | ||
| .put("cluster.remote.my_remote_cluster.signing.key", "signing.key") | ||
| .build() | ||
| ); | ||
| } | ||
| | ||
| // Test API key without certificate identity and send signature anyway | ||
| { | ||
| final var accessJson = """ | ||
| { | ||
| "search": [ | ||
| { | ||
| "names": ["index*", "not_found_index"] | ||
| } | ||
| ] | ||
| }"""; | ||
| MY_REMOTE_API_KEY_MAP_REF.set(createCrossClusterAccessApiKey(accessJson)); | ||
| assertCrossClusterSearchSuccessfulWithResult(); | ||
| | ||
| // Change the CA to the default trust store to make sure untrusted signature fails auth even if it's not required | ||
| updateClusterSettingsFulfillingCluster(Settings.builder().putNull("cluster.remote.signing.certificate_authorities").build()); | ||
| Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am curious, why fail if a certificate is invalid but not required? Is it to protect against misconfiguration? Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am curious, why fail if a certificate is invalid but not required? Is it to catch misconfigurations early? Contributor Author There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, if a signature is provided I think we should try to validate it and fail if it's not correct since it's unexpected. Better to be less lenient in this case I think. Contributor There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It also gives customers a way to progressively switch over to signing. If they configure signing on the origin cluster, and it doesn't break, then they can rely on the fact that it's probably working correctly. Then they can turn on enforcement on the linked cluster. And if that first step does break, then they can just turn off signing on the origin, without needing to reconfigure both sides. | ||
| assertCrossClusterAuthFail("Failed to verify cross cluster api key signature certificate from [("); | ||
| | ||
| // Reset | ||
| updateClusterSettingsFulfillingCluster( | ||
| Settings.builder().put("cluster.remote.signing.certificate_authorities", "signing_ca.crt").build() | ||
| ); | ||
| } | ||
| } | ||
| | ||
| private void assertCrossClusterAuthFail() { | ||
| private void assertCrossClusterAuthFail(String expectedMessage) { | ||
| var responseException = assertThrows(ResponseException.class, () -> simpleCrossClusterSearch(randomBoolean())); | ||
| assertThat(responseException.getResponse().getStatusLine().getStatusCode(), equalTo(401)); | ||
| assertThat(responseException.getMessage(), containsString("Failed to verify cross cluster api key signature certificate from [(")); | ||
| assertThat(responseException.getMessage(), containsString(expectedMessage)); | ||
| } | ||
| | ||
| private void assertCrossClusterSearchSuccessfulWithoutResult() throws IOException { | ||
| | @@ -227,4 +309,25 @@ private Response performRequestWithRemoteAccessUser(final Request request) throw | |
| request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", basicAuthHeaderValue(REMOTE_SEARCH_USER, PASS))); | ||
| return client().performRequest(request); | ||
| } | ||
| | ||
| protected static Map<String, Object> createCrossClusterAccessApiKey(String accessJson, String certificateIdentity) { | ||
| initFulfillingClusterClient(); | ||
| final var createCrossClusterApiKeyRequest = new Request("POST", "/_security/cross_cluster/api_key"); | ||
| createCrossClusterApiKeyRequest.setJsonEntity(Strings.format(""" | ||
| { | ||
| "name": "cross_cluster_access_key", | ||
| "certificate_identity": "%s", | ||
| "access": %s | ||
| }""", certificateIdentity, accessJson)); | ||
| try { | ||
| final Response createCrossClusterApiKeyResponse = performRequestWithAdminUser( | ||
| fulfillingClusterClient, | ||
| createCrossClusterApiKeyRequest | ||
| ); | ||
| assertOK(createCrossClusterApiKeyResponse); | ||
| return responseAsMap(createCrossClusterApiKeyResponse); | ||
| } catch (IOException e) { | ||
| throw new UncheckedIOException(e); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.