Skip to content

Commit e39f6f8

Browse files
jfredenelasticsearchmachinetvernum
authored andcommitted
Validate Certificate Identity provided in Cross Cluster API Key Certificate (elastic#136299)
* Validate certificate identity from cross cluster creds Co-authored-by: elasticsearchmachine <[email protected]> Co-authored-by: Tim Vernum <[email protected]>
1 parent 7ebad4a commit e39f6f8

File tree

15 files changed

+679
-120
lines changed

15 files changed

+679
-120
lines changed

docs/changelog/136299.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 136299
2+
summary: Validate certificate identity from cross cluster creds
3+
area: Security
4+
type: enhancement
5+
issues: []

x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityCrossClusterApiKeySigningIT.java

Lines changed: 161 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import org.junit.rules.TestRule;
2626

2727
import java.io.IOException;
28+
import java.io.UncheckedIOException;
2829
import java.util.Arrays;
2930
import java.util.List;
3031
import java.util.Locale;
@@ -38,7 +39,20 @@
3839

3940
public 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
}

x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/transport/CrossClusterApiKeySignatureManagerIntegTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ public void testSignWithPemKeyConfig() throws GeneralSecurityException {
4545
var verifier = manager.verifier();
4646

4747
assertThat(signature.algorithm(), equalToIgnoringCase(keyConfig.getKeys().getFirst().v2().getSigAlgName()));
48-
assertEquals(signature.certificates()[0], keyConfig.getKeys().getFirst().v2());
48+
assertEquals(signature.leafCertificate(), keyConfig.getKeys().getFirst().v2());
4949
assertTrue(verifier.verify(signature, testHeaders));
5050
}
5151

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,6 +1643,8 @@ public static List<Setting<?>> getSettings(
16431643
settingsList.add(ApiKeyService.CACHE_MAX_KEYS_SETTING);
16441644
settingsList.add(ApiKeyService.CACHE_TTL_SETTING);
16451645
settingsList.add(ApiKeyService.DOC_CACHE_TTL_SETTING);
1646+
settingsList.add(ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_TTL_SETTING);
1647+
settingsList.add(ApiKeyService.CERTIFICATE_IDENTITY_PATTERN_CACHE_MAX_KEYS_SETTING);
16461648
settingsList.add(NativePrivilegeStore.CACHE_MAX_APPLICATIONS_SETTING);
16471649
settingsList.add(NativePrivilegeStore.CACHE_TTL_SETTING);
16481650
settingsList.add(OPERATOR_PRIVILEGES_ENABLED);

0 commit comments

Comments
 (0)