-
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
base: main
Are you sure you want to change the base?
Changes from 8 commits
99fac10
0cc2ea3
69c9a17
eb7a3f7
d63b4ec
e598e69
9022c68
b4dfd67
59dbd74
649a7e4
4d07535
d1b780d
2203889
ed4093a
094c15f
a71ca11
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,102 @@ 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(); | ||
|
||
// Make sure we can search if cert trusted | ||
{ | ||
assertCrossClusterSearchSuccessfulWithResult(); | ||
} | ||
|
||
// 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 [("); | ||
|
||
// 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() | ||
); | ||
|
||
assertCrossClusterSearchSuccessfulWithoutResult(); | ||
// 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() | ||
); | ||
assertCrossClusterAuthFail("Expected signature for cross cluster API key, but no signature was provided"); | ||
|
||
// TODO add test for certificate identity configured for API key but no signature provided (should 401) | ||
// 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() | ||
); | ||
} | ||
|
||
// TODO add test for certificate identity not configured for API key but signature provided (should 200) | ||
// 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(); | ||
|
||
// TODO add test for certificate identity not configured for API key but wrong signature provided (should 401) | ||
// 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()); | ||
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? 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? 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. 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 [("); | ||
|
||
// TODO add test for certificate identity regex matching (should 200) | ||
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? 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 |
||
// 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 +304,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.